feat: úhrada za všechny jednou osobou (issue #29, SINGLE_PAYMENT)
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:
2026-04-28 22:35:15 +02:00
parent e5999852b7
commit 1e1e23df80
11 changed files with 430 additions and 66 deletions
+11 -8
View File
@@ -5,6 +5,7 @@ import getStorage from "./storage";
import { downloadPizzy, downloadSalaty } from "./chefie";
import { getClientData, getToday, initIfNeeded } from "./service";
import { Pizza, Salat, ClientData, PizzaDayState, PizzaSize, PizzaOrder, PizzaVariant, UdalostEnum, PendingQr } from "../../types/gen/types.gen";
import crypto from "crypto";
const storage = getStorage();
const PENDING_QR_PREFIX = 'pending_qr';
@@ -337,13 +338,15 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b
if (bankAccount?.length && bankAccountHolder?.length) {
for (const order of clientData.pizzaDay.orders!) {
if (order.customer !== login) { // zatím platí creator = objednávající, a pro toho nemá QR kód smysl
const id = crypto.randomUUID();
let message = order.pizzaList!.map(item =>
item.category === 'salat' ? `Salát ${item.name}` : `Pizza ${item.name} (${item.size})`
).join(', ');
await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice, message);
item.category === 'salat' ? `Salát ${item.name}` : `Pizza ${item.name} (${item.size})`
).join(', ');
await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice, message, id);
order.hasQr = true;
// Uložíme nevyřízený QR kód pro persistentní zobrazení
await addPendingQr(order.customer, {
id,
date: today,
creator: login,
totalPrice: order.totalPrice,
@@ -430,8 +433,8 @@ function getPendingQrKey(login: string): string {
export async function addPendingQr(login: string, pendingQr: PendingQr): Promise<void> {
const key = getPendingQrKey(login);
const existing = await storage.getData<PendingQr[]>(key) ?? [];
// Nepřidáváme duplicity pro stejný den
if (!existing.some(qr => qr.date === pendingQr.date)) {
// Deduplikace podle id (ne podle data — jeden den může mít uživatel víc QR kódů)
if (!existing.some(qr => qr.id === pendingQr.id)) {
existing.push(pendingQr);
await storage.setData(key, existing);
}
@@ -445,11 +448,11 @@ export async function getPendingQrs(login: string): Promise<PendingQr[]> {
}
/**
* Označí QR kód pro daný den jako uhrazený (odstraní ho ze seznamu nevyřízených).
* Označí QR kód jako uhrazený (odstraní ho ze seznamu nevyřízených).
*/
export async function dismissPendingQr(login: string, date: string): Promise<void> {
export async function dismissPendingQr(login: string, id: string): Promise<void> {
const key = getPendingQrKey(login);
const existing = await storage.getData<PendingQr[]>(key) ?? [];
const filtered = existing.filter(qr => qr.date !== date);
const filtered = existing.filter(qr => qr.id !== id);
await storage.setData(key, filtered);
}