feat: úhrada za všechny jednou osobou (issue #29, SINGLE_PAYMENT)
ci/woodpecker/push/workflow Pipeline was canceled
ci/woodpecker/push/workflow Pipeline was canceled
Přidává možnost, aby jeden strávník zaplatil celý účet v restauraci a ostatní
obdrželi QR kód pro refundaci.
Prerekvizita — podpora více QR kódů na (příjemce, den):
- PendingQr.id (UUID) nahrazuje deduplikaci podle data; každý QR má vlastní klíč
- QR obrázky uloženy do Redis/storage (base64) místo tmpdir — přežijí redeploy
- GET /api/qr vyžaduje ?id= parametr; dismissQr přijímá {id} místo {date}
Feature:
- Ikona 'Zaplatit za všechny' v choices-table pro každou LunchChoice (kromě
PIZZA/NEOBEDVAM/ROZHODUJI); viditelná jen při ≥2 strávnících a vyplněném účtu
- PayForAllModal: tabulka strávníků s prefillovanými cenami z menu, příplatky
per-diner, celkové dýško rozpočtené rovnoměrně, generování QR přes POST /api/qr/generate
- parsePriceCzk() helper pro parsing 'N Kč' → number
Co se nemění: POST /api/qr/generate API kontrakt, PizzaOrder.hasQr boolean
Co se mění v OpenAPI: PendingQr.id (required), getPizzaQr ?id param, dismissQr body
Co-Authored-By: opmrdkazkrtkaus <opmrdkazkrtkaus@melancholik.eu>
This commit was merged in pull request #53.
This commit is contained in:
+42
-12
@@ -13,12 +13,13 @@ import './App.scss';
|
||||
import { faCircleCheck, faNoteSticky, faTrashCan, faComment } from '@fortawesome/free-regular-svg-icons';
|
||||
import { useSettings } from './context/settings';
|
||||
import Footer from './components/Footer';
|
||||
import { faBasketShopping, faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBasketShopping, faChainBroken, faChevronLeft, faChevronRight, faGear, faMoneyBillTransfer, faSatelliteDish, faSearch, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
|
||||
import Loader from './components/Loader';
|
||||
import { getHumanDateTime, isInTheFuture, formatDateString } from './Utils';
|
||||
import NoteModal from './components/modals/NoteModal';
|
||||
import PayForAllModal from './components/modals/PayForAllModal';
|
||||
import { useEasterEgg } from './context/eggs';
|
||||
import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr } from '../../types';
|
||||
import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, LocationLunchChoicesMap, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr, generateQr } from '../../types';
|
||||
import { getLunchChoiceName } from './enums';
|
||||
// import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves';
|
||||
// import './FallingLeaves.scss';
|
||||
@@ -74,6 +75,7 @@ function App() {
|
||||
const [dayIndex, setDayIndex] = useState<number>();
|
||||
const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false);
|
||||
const [noteModalOpen, setNoteModalOpen] = useState<boolean>(false);
|
||||
const [payForAllLocationKey, setPayForAllLocationKey] = useState<LunchChoice | null>(null);
|
||||
const [eggImage, setEggImage] = useState<Blob>();
|
||||
const eggRef = useRef<HTMLImageElement>(null);
|
||||
// Prazvláštní workaround, aby socket.io listener viděl aktuální hodnotu
|
||||
@@ -679,6 +681,18 @@ function App() {
|
||||
<td>
|
||||
{locationName}
|
||||
{(locationPickCount ?? 0) > 1 && <span className="ms-1">({locationPickCount})</span>}
|
||||
{locationPickCount >= 2 && auth.login && loginObject[auth.login] !== undefined
|
||||
&& locationKey !== LunchChoice.PIZZA && locationKey !== LunchChoice.NEOBEDVAM && locationKey !== LunchChoice.ROZHODUJI
|
||||
&& settings?.bankAccount && settings?.holderName && (
|
||||
<span title='Zaplatit za všechny a vygenerovat QR kódy ostatním' className="ms-2">
|
||||
<FontAwesomeIcon
|
||||
icon={faMoneyBillTransfer}
|
||||
onClick={() => setPayForAllLocationKey(locationKey)}
|
||||
className='action-icon'
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className='p-0'>
|
||||
<Table className="nested-table">
|
||||
@@ -856,11 +870,15 @@ function App() {
|
||||
}
|
||||
<PizzaOrderList state={data.pizzaDay.state!} orders={data.pizzaDay.orders!} onDelete={handlePizzaDelete} creator={data.pizzaDay.creator!} />
|
||||
{
|
||||
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr &&
|
||||
<div className='qr-code'>
|
||||
<h3>QR platba</h3>
|
||||
<img src={`/api/qr?login=${auth.login}`} alt='QR kód' />
|
||||
</div>
|
||||
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr && (() => {
|
||||
const pizzaQr = data.pendingQrs?.find(qr => qr.creator === data.pizzaDay?.creator);
|
||||
return pizzaQr ? (
|
||||
<div className='qr-code'>
|
||||
<h3>QR platba</h3>
|
||||
<img src={`/api/qr?login=${auth.login}&id=${pizzaQr.id}`} alt='QR kód' />
|
||||
</div>
|
||||
) : null;
|
||||
})()
|
||||
}
|
||||
</>
|
||||
}
|
||||
@@ -870,18 +888,17 @@ function App() {
|
||||
{data.pendingQrs && data.pendingQrs.length > 0 &&
|
||||
<div className='pizza-section fade-in mt-4'>
|
||||
<h3>Nevyřízené platby</h3>
|
||||
<p>Máte neuhrazené platby z předchozích dní.</p>
|
||||
<p>Máte neuhrazené platby.</p>
|
||||
{data.pendingQrs.map(qr => (
|
||||
<div key={qr.date} className='qr-code mb-3'>
|
||||
<div key={qr.id} className='qr-code mb-3'>
|
||||
<p>
|
||||
<strong>{formatDateString(qr.date)}</strong> — {qr.creator} ({qr.totalPrice} Kč)
|
||||
{qr.purpose && <><br /><span className="text-muted">{qr.purpose}</span></>}
|
||||
</p>
|
||||
<img src={`/api/qr?login=${auth.login}`} alt='QR kód' />
|
||||
<img src={`/api/qr?login=${auth.login}&id=${qr.id}`} alt='QR kód' />
|
||||
<div className='mt-2'>
|
||||
<Button variant="success" onClick={async () => {
|
||||
await dismissQr({ body: { date: qr.date } });
|
||||
// Přenačteme data pro aktualizaci
|
||||
await dismissQr({ body: { id: qr.id } });
|
||||
const response = await getData({ query: { dayIndex } });
|
||||
if (response.data) {
|
||||
setData(response.data);
|
||||
@@ -902,6 +919,19 @@ function App() {
|
||||
/> */}
|
||||
<Footer />
|
||||
<NoteModal isOpen={noteModalOpen} onClose={() => setNoteModalOpen(false)} onSave={saveNote} />
|
||||
{payForAllLocationKey && data && (
|
||||
<PayForAllModal
|
||||
isOpen
|
||||
onClose={() => setPayForAllLocationKey(null)}
|
||||
locationKey={payForAllLocationKey}
|
||||
locationName={getLunchChoiceName(payForAllLocationKey)}
|
||||
locationChoices={data.choices[payForAllLocationKey as keyof typeof data.choices] as LocationLunchChoicesMap}
|
||||
menu={food?.[payForAllLocationKey as Restaurant]}
|
||||
payerLogin={auth.login ?? ''}
|
||||
bankAccount={settings?.bankAccount ?? ''}
|
||||
bankAccountHolder={settings?.holderName ?? ''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user