wutzcalc/PLAN.md

138 lines
6.8 KiB
Markdown
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.

# wutzcalc — Implementation Plan
Festival drink-sale tracker. 3 bars, ~10k attendees over 1 week, old iPads (iOS 12),
local network only, central linux server.
## Decisions (locked for v1)
- **Stack:** Preact + Vite (client), Node + Fastify (server), SQLite (better-sqlite3).
- **Deployment:** single Node process serves API + static client + opens one `.db` file.
- **Sync model:** online-only. Confirm requires server reachable on LAN.
- **Auth:** none on tablets in v1 (trusted LAN); `/admin` gated by a shared password env var.
- **Tablet identity:** server records source IP per transaction (`client_ip`) to attribute transactions to a tablet. Assumes static DHCP leases or stable IPs on the festival LAN.
- **Drink catalog:** shared across bars; each bar picks which drinks it serves.
- **Pfand:** single global pfand value per bar; added on top of drink price.
- **Pfandrückgabe:** dedicated minus/return buttons per drink; endbetrag may go negative.
- **Crew/freebies:** a separate "Bestätigen (Crew)" confirm button — logs transaction with `crew=true`, paid amount = 0, items still recorded.
- **Undo:** only before confirming, fully local (edit the current cart). No undo of confirmed transactions in v1.
- **iOS 12 target:** Vite build with `legacy` plugin → ES2017 + polyfills. No CSS features past Safari 12.
## Architecture
```
┌──────────────┐ HTTP/JSON ┌────────────────────────┐
│ Tablet (PWA) │ ───────────────► │ Node + Fastify │
│ Preact SPA │ │ /api/... │
│ per-bar UI │ │ /admin (pw-gated) │
└──────────────┘ │ static client assets │
│ ───────────────────── │
│ better-sqlite3 ── wutz.db │
└────────────────────────┘
```
- Single port, single binary-ish (`node server.js`). Systemd unit for autostart.
- SQLite WAL mode for concurrent reads from dashboard while writes happen.
## Data model (SQLite)
```
bars(id, name, pfand_cents)
drinks(id, name, price_cents, archived)
bar_drinks(bar_id, drink_id, sort_order) -- which drinks each bar sells
transactions(
id, bar_id, created_at, total_cents,
crew INTEGER NOT NULL DEFAULT 0,
client_ip TEXT, -- which tablet (per-bar device id)
client_uuid TEXT UNIQUE -- idempotency from tablet
)
transaction_items(
id, transaction_id, drink_id, qty, unit_price_cents, pfand_cents_per_unit,
is_return INTEGER NOT NULL DEFAULT 0 -- pfandrückgabe line
)
settings(key, value) -- admin password hash etc.
```
- All money in integer cents.
- `client_uuid` makes confirm idempotent against double-tap / retry.
- Returns stored as separate line items with `is_return=1` and negative contribution to total.
## API surface
- `GET /api/config?bar=<id>` → bar + its drinks + pfand value.
- `GET /api/bars` → list of bars (for tablet first-run picker).
- `POST /api/transactions` → body: `{ client_uuid, bar_id, crew, items: [{drink_id, qty, is_return}] }`. Server recomputes prices from current catalog, captures source IP (`req.ip`, trust proxy off) as `client_ip`, stores, returns `{id, total_cents}`.
- `GET /admin` → password form → session cookie.
- `GET /admin/api/stats` → totals per bar, per drink, crew vs paid.
- `GET /admin/api/export.csv?what=transactions|items` → CSV download.
- `POST /admin/api/drinks` / `PATCH` / `DELETE` → catalog config.
- `POST /admin/api/bars/:id` → pfand + drink selection.
Server is the source of truth for prices — tablet sends drink IDs + qty only.
## Client (tablet) UX
- **First run:** pick which bar this tablet is. Stored in `localStorage`.
- **Main screen:** grid of large buttons, one per drink served at this bar.
- Tap → adds 1 to cart.
- Long-press / "" sub-button → returns 1 pfand for that drink.
- **Cart strip:** running total (price + pfand returns), per-line qty.
- **Actions:** `Abbrechen` (clear cart), `Bestätigen` (send, crew=false), `Bestätigen (Crew)` (send, crew=true, paid=0).
- All cart edits are local until confirm — undo = remove from cart.
- Tap targets ≥ 64px, no hover states, no small text.
## Backoffice (v1)
- `/admin` login (single password from env `ADMIN_PASSWORD`).
- Pages:
1. **Drinks**: CRUD table (name, price, archived).
2. **Bars**: per bar — pfand value + checklist of drinks served + sort order.
3. **Stats**: totals per bar, top drinks, crew freebie count/value.
4. **Export**: CSV of transactions and transaction_items.
## Repo layout
```
wutzcalc/
├── server/
│ ├── src/
│ │ ├── index.ts # fastify bootstrap, static serving
│ │ ├── db.ts # better-sqlite3 + migrations
│ │ ├── routes/
│ │ │ ├── public.ts # /api/* (tablets)
│ │ │ └── admin.ts # /admin/*
│ │ └── csv.ts
│ └── migrations/
├── client/ # Preact + Vite
│ ├── src/
│ │ ├── main.tsx
│ │ ├── pages/{Bar,Setup}.tsx
│ │ ├── components/{DrinkButton,Cart}.tsx
│ │ └── api.ts
│ └── vite.config.ts # @vitejs/plugin-legacy → iOS 12 target
├── admin/ # could share Preact app or be plain HTML forms
├── package.json # workspaces
├── README.md
├── NOTES.md
├── TODO.md
└── PLAN.md
```
(Admin can be a second entry in the same Vite build, or server-rendered HTML — leaning toward second Vite entry for consistency.)
## Implementation order
1. Repo scaffold: pnpm workspaces, Fastify server skeleton, Vite client skeleton, shared types package.
2. SQLite schema + migrations + seed (3 bars, a few drinks).
3. `/api/config` + `/api/transactions` with idempotency on `client_uuid`.
4. Tablet UI: bar picker → drink grid → cart → confirm/cancel/crew-confirm.
5. Admin password gate + drinks/bars config UI.
6. Stats endpoint + minimal dashboard.
7. CSV export.
8. Legacy build verification on an iOS 12 Safari (or BrowserStack equivalent).
9. Systemd unit + README deploy instructions + backup cron note.
## Open risks
- iOS 12 Safari is the main constraint — need to verify Preact + legacy plugin output actually runs there early (after step 1, before building more).
- Local network reliability: if the WiFi at the venue is flaky, online-only model bites. Tracked in `TODO.md` (offline-capable client).
- SQLite single-writer is fine at ~10k attendees / week, but document WAL mode and a backup cron.