wutzcalc/client/src/tablet/Sale.tsx
müde 2a43b59947 tablet: split price/pfand, light mode, bigger bold text
- show drink price and Pfand separately on tiles and in cart
- add light/dark toggle on the bar-picker page (persisted)
- theme via CSS variables, applied on load for tablet + admin
- larger, bolder text throughout; much bigger total sum
2026-06-14 21:57:52 +02:00

144 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="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)}</div>
{bar.pfand_cents > 0 && (
<div class="pfand">+ {formatCents(bar.pfand_cents)} Pfand</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;
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>
);
}