wutzcalc/client/src/tablet/Sale.tsx
müde 6a0d30c8b5 add svg favicon + toolbar logo; set theme before paint
- 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
2026-06-14 22:55:15 +02:00

149 lines
4.6 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="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>
);
}