diff --git a/client/src/App.tsx b/client/src/App.tsx index a1f2bf6..bd7573d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -18,6 +18,7 @@ import { useNavigate } from 'react-router-dom'; import Loader from './components/Loader'; import { getHumanDateTime, isInTheFuture, formatDateString } from './Utils'; import NoteModal from './components/modals/NoteModal'; +import ConfirmModal from './components/modals/ConfirmModal'; import PayForAllModal from './components/modals/PayForAllModal'; import { useEasterEgg } from './context/eggs'; import { ClientData, Food, MealSlot, PendingQr, 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'; @@ -77,6 +78,7 @@ function App() { const [dayIndex, setDayIndex] = useState(); const [loadingPizzaDay, setLoadingPizzaDay] = useState(false); const [noteModalOpen, setNoteModalOpen] = useState(false); + const [dismissQrId, setDismissQrId] = useState(null); const [payForAllLocationKey, setPayForAllLocationKey] = useState(null); const [eggImage, setEggImage] = useState(); const eggRef = useRef(null); @@ -698,15 +700,15 @@ function App() { {locationPickCount >= 2 && auth.login && loginObject[auth.login] !== undefined && locationKey !== LunchChoice.PIZZA && locationKey !== LunchChoice.NEOBEDVAM && locationKey !== LunchChoice.ROZHODUJI && settings?.bankAccount && settings?.holderName && ( - - setPayForAllLocationKey(locationKey)} - className='action-icon' - style={{ cursor: 'pointer' }} - /> - - )} + + setPayForAllLocationKey(locationKey)} + className='action-icon' + style={{ cursor: 'pointer' }} + /> + + )} @@ -909,18 +911,12 @@ function App() { {data.pendingQrs.map(qr => (

- {formatDateString(qr.date)} — {qr.creator} ({qr.totalPrice} Kč) + {formatDateString(qr.date)} — {qr.creator} ({qr.totalPrice / 100} Kč) {qr.purpose && <>
{qr.purpose}}

QR kód
-
@@ -936,6 +932,24 @@ function App() { /> */}
setNoteModalOpen(false)} onSave={saveNote} /> + setDismissQrId(null)} + onConfirm={async () => { + if (!dismissQrId) return; + const id = dismissQrId; + setDismissQrId(null); + await dismissQr({ body: { id } }); + const response = await getData({ query: { dayIndex } }); + if (response.data) { + setData(response.data); + } + }} + /> {payForAllLocationKey && data && ( void; + onClose: () => void; +}; + +export default function ConfirmModal({ isOpen, title, message, confirmLabel = "Potvrdit", confirmVariant = "primary", onConfirm, onClose }: Readonly) { + return ( + + + {title} + + {message} + + + + + + ); +} diff --git a/client/src/components/modals/PayForGroupModal.tsx b/client/src/components/modals/PayForGroupModal.tsx index d8571c1..55eea18 100644 --- a/client/src/components/modals/PayForGroupModal.tsx +++ b/client/src/components/modals/PayForGroupModal.tsx @@ -5,6 +5,7 @@ import { generateQr, OrderGroup, OrderGroupMember, QrRecipient } from "../../../ type Props = { isOpen: boolean; onClose: () => void; + onSuccess?: () => void; group: OrderGroup; payerLogin: string; bankAccount: string; @@ -18,7 +19,7 @@ type DinerEntry = { included: boolean; }; -export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, bankAccount, bankAccountHolder, groupId }: Readonly) { +export default function PayForGroupModal({ isOpen, onClose, onSuccess, group, payerLogin, bankAccount, bankAccountHolder, groupId }: Readonly) { const [diners, setDiners] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); @@ -73,9 +74,10 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b setError(`Celková částka pro ${d.login} musí být kladná`); return; } + const note = d.member.note?.trim(); recipients.push({ login: d.login, - purpose: `Objednávka ${group.name}`.substring(0, 60), + purpose: note ? note.replace(/[^\x00-\xff*]/g, '').replace(/\*/g, '').substring(0, 60) : `Objednávka ${group.name}`.substring(0, 60), amount: total, }); } @@ -94,6 +96,7 @@ export default function PayForGroupModal({ isOpen, onClose, group, payerLogin, b setError((response.error as any).error || 'Nastala chyba při generování QR kódů'); } else { setSuccess(true); + onSuccess?.(); setTimeout(() => onClose(), 2000); } } catch (e: any) { diff --git a/client/src/pages/OrderGroupsPage.tsx b/client/src/pages/OrderGroupsPage.tsx index d2f314e..26cd03c 100644 --- a/client/src/pages/OrderGroupsPage.tsx +++ b/client/src/pages/OrderGroupsPage.tsx @@ -209,7 +209,7 @@ export default function OrderGroupsPage() { )}
-
+
{/* Vytvoření nové skupiny */}
Vytvořit skupinu
@@ -295,7 +295,7 @@ export default function OrderGroupsPage() { )} {isCreator && isOrdered && ( <> - {settings?.bankAccount && settings?.holderName && ( + {settings?.bankAccount && settings?.holderName && !group.qrGenerated && (
- + - + @@ -537,7 +537,6 @@ export default function OrderGroupsPage() { Doručení v: {group.deliveryAt ?? '—'} - {isCreator && (upravit)} )} @@ -579,6 +578,7 @@ export default function OrderGroupsPage() { setPayModal(null)} + onSuccess={fetchData} group={payModal} groupId={payModal.id} payerLogin={auth.login} diff --git a/server/src/groups.ts b/server/src/groups.ts index 196a336..9c7e77a 100644 --- a/server/src/groups.ts +++ b/server/src/groups.ts @@ -131,6 +131,7 @@ export async function setGroupState(login: string, groupId: string, newState: Gr await removePendingQrsByGroupId(memberLogins, groupId); group.orderedAt = undefined; group.deliveryAt = undefined; + group.qrGenerated = undefined; for (const ml of memberLogins) { group.members[ml] = { ...group.members[ml], paid: undefined }; } @@ -139,6 +140,16 @@ export async function setGroupState(login: string, groupId: string, newState: Gr return saveExtraData(data, date); } +export async function markGroupQrGenerated(login: string, groupId: string, date?: Date): Promise { + const data = await getExtraData(date); + const group = findGroup(data, groupId); + if (!group) throw new Error('Skupina nebyla nalezena'); + if (group.creatorLogin !== login) throw new Error('QR kódy může generovat pouze zakladatel'); + if (group.state !== GroupState.ORDERED) throw new Error('QR kódy lze generovat pouze ve stavu "objednáno"'); + group.qrGenerated = true; + await saveExtraData(data, date); +} + export async function markGroupMemberPaid(login: string, groupId: string, date?: Date): Promise { const data = await getExtraData(date); const group = findGroup(data, groupId); diff --git a/server/src/qr.ts b/server/src/qr.ts index e848028..b1838b2 100644 --- a/server/src/qr.ts +++ b/server/src/qr.ts @@ -56,10 +56,8 @@ function createStorageKey(customerName: string, id: string): string { * @param id unikátní identifikátor (UUID) tohoto QR kódu */ export async function generateQr(customerName: string, bankAccountNumber: string, bankAccountHolder: string, amount: number, message: string, id: string): Promise { - // Zpráva pro příjemce nesmí dle standardu obsahovat '*' a být delší než 60 znaků - if (message.indexOf('*') >= 0) { - message = message.replace(/\*/g, ''); - } + // Zpráva nesmí obsahovat '*' a znaky mimo ISO 8859-1; délka max. 60 znaků + message = message.replace(/[^\x00-\xff]/g, '').replace(/\*/g, ''); if (message.length > 60) { message = message.substring(0, 60); } diff --git a/server/src/routes/qrRoutes.ts b/server/src/routes/qrRoutes.ts index cd2c074..20ad256 100644 --- a/server/src/routes/qrRoutes.ts +++ b/server/src/routes/qrRoutes.ts @@ -3,6 +3,7 @@ import { getLogin } from "../auth"; import { parseToken, formatDate } from "../utils"; import { generateQr } from "../qr"; import { addPendingQr } from "../pizza"; +import { markGroupQrGenerated } from "../groups"; import { emitToUser } from "../websocket"; import { GenerateQrData } from "../../../types"; import crypto from "crypto"; @@ -57,6 +58,10 @@ router.post("/generate", async (req: Request<{}, any, GenerateQrData["body"]>, r emitToUser(recipient.login, 'pendingQr', pendingQr); } + if (groupId) { + await markGroupQrGenerated(login, groupId); + } + res.status(200).json({ success: true, count: recipients.length }); } catch (e: any) { next(e); diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml index 381d2af..755c543 100644 --- a/types/schemas/_index.yml +++ b/types/schemas/_index.yml @@ -768,6 +768,9 @@ OrderGroup: discountValue: description: Hodnota slevy (procenta pro typ 'percent', haléře pro typ 'fixed') type: integer + qrGenerated: + description: Příznak, zda byly pro skupinu vygenerovány QR kódy (blokuje opakované generování) + type: boolean # --- NEVYŘÍZENÉ QR KÓDY --- PendingQr:
ČlenČástka (Kč)Částka (bez slev) Příplatek PoznámkaCelkemCelkem (s poplatky)