scaffold festival drink tracker (pnpm workspace, Fastify + SQLite, Preact tablet UI, admin)
This commit is contained in:
parent
cf33e6ea04
commit
e0898bea22
34 changed files with 5446 additions and 0 deletions
134
client/src/tablet/Sale.tsx
Normal file
134
client/src/tablet/Sale.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { useMemo, useState } from 'preact/hooks';
|
||||
import type { BarConfig, CartItem } from '@wutzcalc/shared';
|
||||
import { api, formatCents, uuid } from '../api';
|
||||
|
||||
interface Props {
|
||||
config: BarConfig;
|
||||
onChangeBar: () => void;
|
||||
}
|
||||
|
||||
type Line = { drink_id: number; qty: number };
|
||||
|
||||
export function Sale({ config, onChangeBar }: Props) {
|
||||
const { bar, drinks } = config;
|
||||
const [lines, setLines] = useState<Line[]>([]);
|
||||
const [pfandReturns, setPfandReturns] = useState(0);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const drinkById = useMemo(() => {
|
||||
const m = new Map<number, typeof drinks[number]>();
|
||||
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) {
|
||||
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 + 1 };
|
||||
return copy;
|
||||
}
|
||||
return [...prev, { drink_id: drinkId, qty: 1 }];
|
||||
});
|
||||
}
|
||||
|
||||
function addPfandReturn() {
|
||||
setPfandReturns(n => n + 1);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
setLines([]);
|
||||
setPfandReturns(0);
|
||||
}
|
||||
|
||||
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: uuid(),
|
||||
bar_id: bar.id,
|
||||
crew,
|
||||
items,
|
||||
pfand_returns: pfandReturns,
|
||||
});
|
||||
clear();
|
||||
} catch (e) {
|
||||
alert(`Fehler beim Speichern: ${e}`);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="tablet">
|
||||
<div class="topbar">
|
||||
<div class="bar-name">{bar.name}</div>
|
||||
<button class="change" onClick={onChangeBar}>Bar wechseln</button>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
{drinks.map(d => (
|
||||
<button key={d.id} class="drink" onClick={() => addDrink(d.id)}>
|
||||
<div class="name">{d.name}</div>
|
||||
<div class="price">{formatCents(d.price_cents + bar.pfand_cents)}</div>
|
||||
</button>
|
||||
))}
|
||||
<button class="drink pfand-return" onClick={addPfandReturn}>
|
||||
<div class="name">Pfand zurück</div>
|
||||
<div class="price">−{formatCents(bar.pfand_cents)}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cart">
|
||||
<div class="cart-items">
|
||||
{isEmpty && <div class="cart-line muted">Keine Einträge</div>}
|
||||
{lines.map((l, i) => {
|
||||
const d = drinkById.get(l.drink_id);
|
||||
if (!d) return null;
|
||||
const unit = d.price_cents + bar.pfand_cents;
|
||||
return (
|
||||
<div key={i} class="cart-line">
|
||||
<span>{l.qty}× {d.name}</span>
|
||||
<span>{formatCents(unit * l.qty)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{pfandReturns > 0 && (
|
||||
<div class="cart-line return">
|
||||
<span>{pfandReturns}× Pfand zurück</span>
|
||||
<span>{formatCents(-bar.pfand_cents * pfandReturns)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class={`cart-total ${total < 0 ? 'negative' : ''}`}>
|
||||
{formatCents(total)}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="cancel" onClick={clear} disabled={isEmpty || submitting}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button class="crew" onClick={() => confirm(true)} disabled={isEmpty || submitting}>
|
||||
Crew
|
||||
</button>
|
||||
<button class="confirm" onClick={() => confirm(false)} disabled={isEmpty || submitting}>
|
||||
Bestätigen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue