import { useMemo, useRef, useState } from 'preact/hooks'; import type { BarConfig, CartItem, Drink } from '@wutzcalc/shared'; import { api, formatCents, uuid } from '../api'; interface Props { config: BarConfig; onChangeBar: () => void; } type Line = { drink_id: number; qty: number }; const LONG_PRESS_MS = 450; const PICKER_COUNTS = [1, 2, 3, 4, 5]; export function Sale({ config, onChangeBar }: Props) { const { bar, drinks } = config; const [lines, setLines] = useState([]); const [pfandReturns, setPfandReturns] = useState(0); const [submitting, setSubmitting] = useState(false); const [pickerDrink, setPickerDrink] = useState(null); const longPressTimer = useRef(null); const longPressFired = useRef(false); // One idempotency key per pending cart, reused across retries of the same // logical submission (e.g. after a timeout/dropped-connection error) so // the server's client_uuid dedup can actually recognise a resend. Reset // whenever the cart is cleared (submitted successfully or cancelled), so // the *next* cart gets its own fresh id. const cartUuidRef = useRef(null); function cartUuid() { if (!cartUuidRef.current) cartUuidRef.current = uuid(); return cartUuidRef.current; } const drinkById = useMemo(() => { const m = new Map(); drinks.forEach(d => m.set(d.id, d)); return m; }, [drinks]); const total = useMemo(() => { const drinksSum = lines.reduce((sum, l) => { const d = drinkById.get(l.drink_id); if (!d) return sum; return sum + (d.price_cents + bar.pfand_cents) * l.qty; }, 0); return drinksSum - bar.pfand_cents * pfandReturns; }, [lines, drinkById, bar.pfand_cents, pfandReturns]); function addDrink(drinkId: number, qty = 1) { setLines(prev => { const i = prev.findIndex(l => l.drink_id === drinkId); if (i >= 0) { const copy = prev.slice(); copy[i] = { ...copy[i]!, qty: copy[i]!.qty + qty }; return copy; } return [...prev, { drink_id: drinkId, qty }]; }); } function incLine(drinkId: number, delta: number) { setLines(prev => { const i = prev.findIndex(l => l.drink_id === drinkId); if (i < 0) return prev; const nextQty = prev[i]!.qty + delta; if (nextQty <= 0) return prev.filter(l => l.drink_id !== drinkId); const copy = prev.slice(); copy[i] = { ...copy[i]!, qty: nextQty }; return copy; }); } function addPfandReturn() { setPfandReturns(n => n + 1); } function incPfandReturns(delta: number) { setPfandReturns(n => Math.max(0, n + delta)); } function clear() { setLines([]); setPfandReturns(0); cartUuidRef.current = null; } function startLongPress(d: Drink) { longPressFired.current = false; cancelLongPress(); longPressTimer.current = window.setTimeout(() => { longPressFired.current = true; setPickerDrink(d); }, LONG_PRESS_MS); } function cancelLongPress() { if (longPressTimer.current != null) { clearTimeout(longPressTimer.current); longPressTimer.current = null; } } function handleDrinkClick(d: Drink) { // a long press already opened the count picker for this tap — swallow // the click that pointerup/click also fires, don't add a plain 1×. if (longPressFired.current) { longPressFired.current = false; return; } addDrink(d.id); } function pickCount(n: number) { if (pickerDrink) addDrink(pickerDrink.id, n); setPickerDrink(null); } const pfandCount = useMemo(() => lines.reduce((sum, l) => sum + l.qty, 0), [lines]); const isEmpty = lines.length === 0 && pfandReturns === 0; async function confirm(crew: boolean) { if (isEmpty || submitting) return; setSubmitting(true); const items: CartItem[] = lines.map(l => ({ drink_id: l.drink_id, qty: l.qty })); try { await api.createTransaction({ client_uuid: cartUuid(), bar_id: bar.id, crew, items, pfand_returns: pfandReturns, }); clear(); } catch (e) { alert(`Fehler beim Speichern: ${e}`); } finally { setSubmitting(false); } } return (
{bar.name}
{drinks.map(d => ( ))} {bar.pfand_cents > 0 && ( )}
{pickerDrink && (
setPickerDrink(null)}>
e.stopPropagation()}>
{pickerDrink.name}
{PICKER_COUNTS.map(n => ( ))}
)}
{isEmpty &&
Keine Einträge
} {lines.map((l, i) => { const d = drinkById.get(l.drink_id); if (!d) return null; return (
{l.qty}× {d.name}
{formatCents(d.price_cents * l.qty)}
); })} {bar.pfand_cents > 0 && pfandCount > 0 && (
{pfandCount}× Pfand {formatCents(bar.pfand_cents * pfandCount)}
)} {pfandReturns > 0 && (
{pfandReturns}× Pfand zurück
{formatCents(-bar.pfand_cents * pfandReturns)}
)}
{formatCents(total)}
); }