- favicon.svg (beer mug), linked from both entry points and shown in the tablet topbar next to the bar name - inline data-theme in <head> kills the flash of dark on light-mode load - hide the "Pfand zurück" tile when the bar has no Pfand
149 lines
4.6 KiB
TypeScript
149 lines
4.6 KiB
TypeScript
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 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: 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="brand">
|
||
<img class="logo" src="/favicon.svg" alt="" width="28" height="28" />
|
||
<span class="bar-name">{bar.name}</span>
|
||
</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)}</div>
|
||
{bar.pfand_cents > 0 && (
|
||
<div class="pfand">+ {formatCents(bar.pfand_cents)} Pfand</div>
|
||
)}
|
||
</button>
|
||
))}
|
||
{bar.pfand_cents > 0 && (
|
||
<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;
|
||
return (
|
||
<div key={i} class="cart-line">
|
||
<span>{l.qty}× {d.name}</span>
|
||
<span>{formatCents(d.price_cents * l.qty)}</span>
|
||
</div>
|
||
);
|
||
})}
|
||
{bar.pfand_cents > 0 && pfandCount > 0 && (
|
||
<div class="cart-line pfand">
|
||
<span>{pfandCount}× Pfand</span>
|
||
<span>{formatCents(bar.pfand_cents * pfandCount)}</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>
|
||
);
|
||
}
|