chore: seminarhof customer project

This commit is contained in:
e2e
2026-06-21 22:23:43 +02:00
commit e0331cbb1c
161 changed files with 18517 additions and 0 deletions

BIN
docs/.DS_Store vendored Normal file

Binary file not shown.

168
docs/booking-lifecycle.md Normal file
View File

@@ -0,0 +1,168 @@
# Booking Lifecycle — Design Spec
Status: **draft for sign-off** · Date: 2026-05-21 · Phase: lifecycle state model + admin overview UI + email service shell + daily cron.
Decisions locked with the owner:
- **Milestone columns** model (lean status enum; contract/deposit tracked as columns + the existing `invoice` row) — not extra statuses, not a long-running workflow.
- **Admin records everything.** No client-facing UI this phase.
- **Deadline anchor = contract email send date** (the 1-year-before-start mark = day 0).
- **On 14-day unpaid: flag for admin review only.** No auto-cancel, no auto-revert; soft reservation stays.
- Email = **service shell** (named methods that log + no-op). Real templates later.
---
## 1. Status enum (the "bucket")
Reshaped from the current 7 states down to 4 active + 1 terminal. `form_received` and `deposit_paid` are **removed** — those moments become milestone columns / derived facts.
| State | Meaning |
|---|---|
| `enquiry` | Client submitted an enquiry; awaiting admin approve/decline. |
| `reserved` | Dates soft-reserved (block others). The whole contract/deposit dance happens **inside** this state. |
| `confirmed` | Admin recorded deposit received → reservation guaranteed. |
| `completed` | Event has taken place. |
| `cancelled` | Terminal. Declined enquiry, or cancelled at any non-terminal stage. |
### Transitions (server-enforced, audited)
| From | To | Driver |
|---|---|---|
| `enquiry` | `reserved` | MANUAL — admin approves |
| `enquiry` | `cancelled` | MANUAL — admin declines |
| `reserved` | `confirmed` | MANUAL — admin records deposit received |
| `reserved` | `cancelled` | MANUAL — admin cancels (e.g. after overdue flag, or contract declined) |
| `confirmed` | `completed` | AUTOMATIC — daily cron when `endDate < today` (admin may also force) |
| `confirmed` | `cancelled` | MANUAL — late cancellation (refunds out of scope) |
| `completed` / `cancelled` | — | terminal |
`cancelled` reachable from every non-terminal state. Every transition writes an `auditLog` row (existing format).
---
## 2. New milestone columns on `booking` (migration 0006)
All nullable ISO-8601 TEXT (matching `created_at` style). camelCase ↔ snake_case.
| Column (TS) | Type | Meaning |
|---|---|---|
| `contractSentAt` | TEXT NULL | When contract+deposit email was sent. **Day-0 anchor** + cron idempotency guard. |
| `contractResponse` | TEXT NULL | `'approved' \| 'declined' \| null` (null = pending). Admin-recorded. |
| `contractResponseAt` | TEXT NULL | When admin recorded the response. |
| `depositReminderSentAt` | TEXT NULL | Day-7 reminder sent (idempotency guard). |
| `flaggedAt` | TEXT NULL | Set when something needs admin review (deposit overdue / short window / contract declined). |
| `flagReason` | TEXT NULL | `'deposit_overdue' \| 'short_window' \| 'contract_declined'`. |
**Deposit lives on the existing `invoice` table** (no duplicate columns):
- Deposit invoice: `kind='deposit'`, created by cron R1.
- Due date = `invoice.dueOn` = `contractSentAt + 14d`.
- Received = `invoice.paidOn IS NOT NULL` (+ `status='paid'`).
On contract approval, also stamp the existing `agbAcceptedAt` / `agbYear` (the contract IS the AGB acceptance).
**Data remap** (migration + seed): `form_received → reserved`; `deposit_paid → confirmed` (with its deposit invoice marked paid). Migration includes `UPDATE booking SET status=... WHERE status IN ('form_received','deposit_paid')` for safety on any non-reset DB.
---
## 3. Daily cron — `booking-lifecycle-daily`
`wireScheduler({ name: 'booking-lifecycle-daily', schedule: '0 6 * * *', func })`. All date math in **Europe/Berlin** local calendar days. Idempotent (safe to run twice) via the `*SentAt IS NULL` / `flaggedAt IS NULL` guards.
For each booking where `status NOT IN ('cancelled','completed')`:
```
R1 Contract dispatch (≈1yr before start, evaluated daily with <=, not ==)
if reserved and contractSentAt is null and (startDate - today) <= 365d:
create deposit invoice (dueOn = today + 14d)
email.sendContractAndDepositEmail(bookingId)
set contractSentAt = now
R2 Day-7 reminder
if reserved and contractSentAt set and depositReminderSentAt is null
and deposit unpaid and (today - contractSentAt) >= 7d:
email.sendDepositReminderEmail(bookingId)
set depositReminderSentAt = now
R3 Day-14 expiry → FLAG ONLY
if reserved and contractSentAt set and deposit unpaid
and flaggedAt is null and (today - contractSentAt) >= 14d:
set flaggedAt = now, flagReason = 'deposit_overdue'
R4 Auto-complete
if confirmed and endDate < today:
setBookingStatus(bookingId, 'completed') // system
```
Using `<=` for R1 means a booking approved *inside* the 1-year window sends on the next daily tick (handles "approved late → send soon" for free). If `startDate` is < ~14d away at approval, flag `short_window` instead of running a deadline that lands past the event.
---
## 4. Email service shell
A singleton `email` service (registered in `services.ts`, added to `SingletonServices`), each method logs + no-ops for now.
| Method | Fires when | Driver |
|---|---|---|
| `sendEnquiryReceivedEmail(bookingId)` | enquiry submitted | enquiry-create RPC |
| `sendEnquiryDeclinedEmail(bookingId)` | admin declines enquiry | `setBookingStatus enquiry→cancelled` |
| `sendReservationConfirmedEmail(bookingId)` | admin approves → reserved | `setBookingStatus enquiry→reserved` |
| `sendContractAndDepositEmail(bookingId)` | day 0 (cron R1) | cron |
| `sendDepositReminderEmail(bookingId)` | day 7 unpaid (cron R2) | cron |
| `sendBookingConfirmedEmail(bookingId)` | admin records deposit received | `recordDepositReceived` |
| `sendCancellationEmail(bookingId)` | admin cancels a non-enquiry booking | `setBookingStatus → cancelled` |
Day-14 expiry sends **no** client email (flag only).
---
## 5. Backend RPCs
| Action | RPC | New/reuse | Effects |
|---|---|---|---|
| Approve enquiry | `setBookingStatus(id,'reserved')` | reshape | + reservation email; flag `short_window` if start < ~14d |
| Decline enquiry | `setBookingStatus(id,'cancelled')` | reshape | + declined email |
| Record contract response | `recordContractResponse(id, 'approved'\|'declined')` | **NEW** | sets `contractResponse(_at)`; approved → `agbAcceptedAt`/`agbYear`; declined → `flaggedAt`/`flagReason='contract_declined'` |
| Mark deposit received | `recordDepositReceived(id)` | **NEW** | `invoice.paidOn`+`status='paid'`; clear `flaggedAt`; → `confirmed`; + confirmed email |
| Cancel (any stage) | `setBookingStatus(id,'cancelled')` | reshape | deposit invoice → `cancelled`; + cancellation email |
| Mark completed (override) | `setBookingStatus(id,'completed')` | reshape | normally cron R4 |
| (Optional) Send contract now | `triggerContractEmail(id)` | **NEW (small)** | runs R1 immediately — useful for testing + early sends; otherwise the 1-yr wait blocks all manual flows |
`setBookingStatus` fires the email matching the transition it owns; milestone side-effects (deposit, contract response, AGB) live in the dedicated `record*` RPCs. All `record*` RPCs write audit rows.
`getAdminBookingDetail` output extends with: `contractSentAt`, `contractResponse`, `contractResponseAt`, `depositReminderSentAt`, `flaggedAt`, `flagReason`, and a deposit-invoice summary (`dueOn`, `paidOn`) to drive the UI time cues.
### `nextAction(booking, depositInvoice)` — derived, drives the UI CTA
Priority order: enquiry→"Approve or decline" (admin) · flagged→`flagReason` (admin) · reserved+no contractSentAt→"Awaiting 1-yr mark" (system) · contractSentAt+no response→"Client to sign contract" (client) · approved+unpaid→"Client to pay deposit" (client, due `invoice.dueOn`) · declined→"Cancel — contract declined" (admin) · confirmed→"Awaiting event" (system) · terminal→none.
---
## 6. Admin overview UI — `BookingStatusPanel`
New `<Paper>` panel mounted **above the Tabs**, below the header, in `admin.bookings.$bookingId.tsx`. Header `Badge` swapped for `StatusPill`. Lift the transition map into `lib/status.ts` (`BOOKING_TRANSITIONS`) and reuse in both the panel and the existing `StatusChanger`; trim `BookingStatus` type + `BOOKING_STATUS` palette to the 5 states.
Layout: `SimpleGrid cols={{ base:1, md:2 }}`**Timeline left**, **Next-action + transitions right**.
- **Timeline (Mantine `Timeline`)** shows milestone progression (richer than the enum): `Enquiry → Reserved → Contract sent → Contract signed → Deposit paid → Confirmed → Completed`, each completed step with its date; current = plum ring; upcoming = gray hollow; `cancelled` = terminal red item replacing the unreached tail.
- **Next-action `Alert`** — single most important outstanding item from `nextAction()`, color/icon by urgency, with the inline action button + "Owner: Admin/Organiser".
- **Transition controls** — one `Button` per valid transition (data-driven off `BOOKING_TRANSITIONS`); destructive (cancel/decline) separated below a `Divider`, red outline; reuse the existing confirm `Modal` for terminal transitions.
- **Time cues** (from `contractSentAt`): "Day X of 14" + `Progress` bar (plum ≤6, yellow 713, red ≥14 unpaid); reminder-sent `Badge`; overdue → prominent red `Alert` at top of panel + "Needs review" pill by the header, booking stays `reserved`.
- **Empty/early states**: contract not yet due (start >1yr out) → gray info alert "Contract email scheduled ~{date}"; cancelled/completed → terminal note, no actions; reserved but no deposit invoice yet → link to invoices tab.
i18n under `pages.adminBooking.status.*` (panelTitle, nextAction.*, actions.*, cues.*, empty.*, party.*), reusing existing `pages.bookingStatus.label.*`. Mirror en + de. Responsive: timeline stacks above actions on `base`; transition buttons `fullWidth` on narrow.
---
## 7. Build order
1. Migration 0006 (milestone columns + status remap) + seed remap → `db reset` + regen.
2. `lib/status.ts` trim + `BOOKING_TRANSITIONS`; update `set-booking-status` enum/transitions/emails.
3. Email service shell (`services/email.ts`) + register + `SingletonServices` type.
4. New RPCs: `recordContractResponse`, `recordDepositReceived`, `triggerContractEmail`; extend `getAdminBookingDetail`.
5. Daily cron `booking-lifecycle-daily` + scheduler wiring.
6. `BookingStatusPanel` component + mount + header pill swap + i18n (en/de).
7. Typecheck, smoke-test cron + transitions.
## Open points (defaults chosen; flag if you disagree)
- **Auto-complete** `confirmed → completed` via cron when the event has passed (admin can still force). Default: **yes, automatic.**
- **`triggerContractEmail`** manual admin action included (otherwise the 1-year wait blocks all manual/testing flows). Default: **include.**
- `deposit_paid` seed/data rows remap to **`confirmed`** (deposit was received).

255
docs/design/admin.md Normal file
View File

@@ -0,0 +1,255 @@
# Admin Portal — UI Design
Audience: Sarah (owner) and Christina (admin assistant). Christina is the
heaviest user (booking admin all day). Sarah uses it for oversight,
pricing/AGB updates, and high-stakes status decisions.
Theme: shared Mantine tokens with the client portal (dark surface, red
primary), but **denser components** — admin is data-heavy, not card-warm.
## Navigation
Sidebar (collapsible to icons-only on smaller screens):
- Dashboard (default landing)
- Bookings
- Calendar
- Cooks (rota + roster)
- Organisations (incl. Stammgruppen)
- Pricing (year-versioned)
- AGB (versioned PDFs)
- Venues (multi-tenant — switch venue here)
- Settings
## 1. Dashboard (landing)
Static layout for v1. Configurable widget mosaic deferred to v1.5.
```
┌──────────────────────────────────────────────────────────────┐
│ Action queue [N items] │ full-width
│ Overdue Orga-Mails, missing deposits, missing room plans, │
│ missing dietary info. Each row → one-click action. │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────┐ ┌──────────────────────────────┐
│ This week │ │ Recent client activity │
│ Arrivals + departures │ │ Last 48h booking edits │
│ next 7 days, completeness│ │ across all bookings │
│ %, flags │ │ │
└──────────────────────────┘ └──────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
│ Pipeline / │ │ Cook rota │ │ Quick actions │
│ financial │ │ status │ │ + New booking │
│ KPIs │ │ unassigned │ │ + New enquiry │
└──────────────┘ └──────────────┘ │ + Send Orga-Mail │
└──────────────────────┘
```
12-column CSS Grid. Top hero spans 12. Mid row 6+6. Bottom row 4+4+4.
Mobile collapses to 1 column.
### Widget contracts (v1.5 readiness)
Each section is built as a self-contained React component with a stable
key. When the configurable mosaic ships, these become draggable widgets
with no contract changes.
## 2. Bookings list
Table-first. Replaces the Buchungsübersicht spreadsheet.
- **Library:** Mantine `DataTable` or `mantine-react-table`
- **Columns:** Year · Date range · Group / Organisation · Status pill ·
Persons · Half-house · Orga-Mail sent · Deposit · Final invoice · Notes
(truncated)
- **Filters** (sidebar drawer): status, year, half-house, has-deposit,
has-final-invoice, organisation, missing-info (room plan / dietary /
headcount)
- **Sticky header** + virtualised rows
- **Bulk actions** (with confirmation modals): send Orga-Mail to
selected, export DATEV CSV, mark deposit received
- **Full-text search:** organisation name + course name
- **Default sort:** date ASC, current year first
- **Row click:** opens booking detail in same tab
## 3. Calendar view
Replaces the Belegungsplan spreadsheet.
- **Views:** month (default), quarter, year
- **Status colouring** mapped to theme tokens:
- `enquiry` — pink
- `reserved` — amber
- `form_received` — yellow
- `deposit_paid` — light green
- `confirmed` — green
- `completed` — grey
- `cancelled` — strikethrough red
- **Half-house** rendering: day cell splits visually into top/bottom
halves when two groups overlap
- **Click cell or booking** → open booking detail
- **Drag-and-drop reschedule** opens a modal that surfaces the
cancellation-policy implications (e.g. "this falls within 12 weeks of
arrival — fees may apply")
- **Year header strip:** occupancy %, free-day count
- **Filter chips above:** org, status (multi-select)
## 4. Single booking detail
The workhorse. Tabbed organisation.
### Sticky header
- Group name · status pill · dates (large) · headcount · half-house badge
- Status-transition button (contextual)
- Quick actions: edit, send Orga-Mail, send reminder, attach invoice,
cancel
### Main column (left, ~70%) — tabs
1. **Overview**
- Completeness checklist (Orga-Mail sent · deposit · room plan ·
dietary · headcount · schedule)
- Key dates timeline
- Flags (flying changeover · dogs · wheelchair · individual invoicing)
- Pricing summary (line items + total)
2. **Participants**
- Editable table: name · age · `kind` (overnight | day_guest) ·
dietary tags · allergies · room (if overnight)
3. **Room plan** — split view, grid as primary editor + live floor-plan
visualisation. Both backed by the same `room_assignments` model.
- **Left — assignment grid:** rows = overnight participants;
columns = name · age · dietary tags · room (combobox with live
capacity counter) · notes. Bulk-select participants → assign to a
room. Keyboard-fast (Tab / type-to-search / Enter to commit).
Inline validation: overcapacity, conflicting flags (e.g.
wheelchair user only assignable to Rooms 1 and 2).
- **Right — floor plan visualisation:** SVG overlay on top of
`grundrisse.pdf`, hot-spotted with room rectangles. Each room
shows number, capacity dots filled per occupant, flag icons.
Live-updates as the grid changes. Click a room → highlights its
occupants in the grid. Hover a participant → highlights their room.
- **v1.5 enrichment:** click-room-to-assign-selected (spatial
click-select pattern matching the rest of admin)
- **Post-MVP:** drag a participant card onto a room on the floor plan
4. **Schedule & meals**
- Structured times for Frühstück / Mittagessen / Abendessen
(validated against allowed-window config)
- Free-text daily plan
5. **Extras**
- Booking-time extras (cake, second seminar room, linen, early
arrival, etc.)
- On-site extras logged by staff (sauna, drinks, coffee) with
`payment_method` flag (`cash` default · `invoice`)
6. **Invoices**
- Deposit invoice (PDF + DATEV ref + paid status)
- Final invoice (PDF + DATEV ref + paid status)
- DATEV line-item export button (CSV/JSON)
7. **Audit log**
- Full change history: who · when · field · before → after
### Side column (right, ~30%) — always visible
- Organiser contact (name · email · phone · billing address ·
organisation link)
- Internal notes (Bemerkungen) — markdown, per-booking
- Recent activity (last 5 changes, link to full audit)
- Linked enquiry (if any)
- Assigned KOCH/IN per event day
## 5. Cook rota grid
Replaces the Küchenplan spreadsheet.
- **Layout:** one row per day, one column per cook
- **Left columns** (locked / sticky): day-of-month · seminar room
occupants (Mandala · Yogaraum) · PAX · KOCH/IN · KOMMENTARE
- **Right columns:** one per active staff cook + EXTERN
- **Cells:** edit-on-click, code-aware text input with autocomplete from
the **`shift_code` enum**
- **Shift code enum** (DB-backed, not freeform):
- `F` — Frühstück
- `M` — Mittagessen
- `A` — Abendessen
- `S` — Spät / extra shift
- `R-K` — Reinigung Küche
- `R-HH` — Reinigung Haupthaus
- `R-HH-Zi` — Reinigung Haupthaus Zimmer
- `GR-K` — Grundreinigung Küche
- `nur-A` — only dinner
- `off` — explicit off
- Plus optional explicit time (`HH:mm`) and per-cell free-text note
- **Multiple codes per cell:** combine via `/` (e.g. `F/M`); UI renders
each as a coloured chip
- **Bracket convention (`(...)`)** marking event start/end is
**auto-rendered** by the grid from each booking's start/end dates —
not typed
- **Filters above grid:** month, cook, event
- **Conflict highlighting:** when a cook has overlapping shifts on the
same day, cell shows a warning badge
### Schema implication
```
shift_codes
code text PK -- 'F' | 'M' | 'A' | 'S' | 'R-K' | …
label text
color text -- theme token
cook_assignments
id uuid
venue_id uuid → venues
booking_id uuid → bookings (nullable for non-event days e.g. GR-K)
cook_id uuid → cooks
date date
shift_codes text[] → shift_codes.code
explicit_time time
note text
```
## 6. Auxiliary admin screens (CRUD, low design effort)
- **Organisations** — list + detail; Stammgruppe boolean toggle; merge
duplicates
- **Pricing** — year-versioned editor (one form per year, fields for EZ
/ DZ / MBZ / day guest / kids 310 / cake / sauna / linen / etc.)
- **AGB** — upload PDF per year; list previous versions
- **Venues** — switch active venue (multi-tenant); edit venue settings
(allowed meal-time windows, default Half House split, etc.)
- **Cooks** — roster: name · kind (`staff` | `external_vendor` |
`inactive`) · compensation model + amount · notes
- **Settings** — current user profile, notifications preferences,
language preference, email signature
## Styling & component approach
**Mantine** as the component library. Theme tokens (red primary on dark
surface from the client-portal screenshot) shared across all four
surfaces — same tokens, surface-specific compositions. Admin uses
denser Mantine compositions; client uses warmer card-based ones.
Mantine ecosystem packages used:
- `@mantine/core` — components
- `@mantine/dates` — date pickers (`de` locale via `dayjs`)
- `@mantine/hooks` — utilities
- `mantine-react-table` (or `mantine-datatable`) — bookings list
virtualisation, filtering, bulk select
**No drag-and-drop in MVP.** Interactions previously sketched with DnD
become click-select-then-target. DnD is a post-MVP enrichment, not a
load-bearing primitive — the click pattern works without it.
- **Calendar reschedule** — "Reschedule" button → date-picker modal
that surfaces cancellation-policy implications inline. Drag-to-
reschedule possible later as enrichment.
- **Room plan** — split view: assignment grid + live floor-plan SVG.
Grid is the primary editor; floor plan visualises live. v1.5 adds
click-room-to-assign; post-MVP adds participant drag onto the plan.
- **Cook rota grid** — click cell → autocomplete input for shift codes.
## Non-goals (defer to v1.5+)
- Configurable widget mosaic dashboard
- Real-time websocket updates (polled refresh in v1)
- Per-user dashboard layout
- WhatsApp staff notifications (email + in-app only)
- HR / applicant tracking

161
docs/design/client.md Normal file
View File

@@ -0,0 +1,161 @@
# Client Portal — UI Design
Audience: retreat organisers (e.g. Yoga Retreat e.V.) and any colleagues
they invite (multi-user per org, locked earlier).
Theme: Mantine, dark surface, red primary — the warm, card-based tone
from the reference screenshot. Same tokens as admin/staff but
distinctly card-driven layouts (admin is denser).
## Sidebar
Two-group sidebar. The **MY EVENT** group is conditionally visible —
shown only when the user has selected an active event.
```
SEMINARHOF DRAWEHN
Client Portal
─────────────────
[org-switcher] Yoga Retreat e.V. ▾ ← visible if user is in >1 org
MY BOOKINGS
Overview
My bookings
Invoices
MY EVENT — Summer Yoga 2026 ▾ ← only when active event set
Participants
Room plan
Dietary info
Schedule & extras
─────────────────
[avatar] User name (role on the org)
```
Active event is set by clicking a booking in Overview or My bookings.
The dropdown switches between events the user has access to.
## 1. Overview (landing)
Reference screenshot is canonical. Cards: upcoming bookings (with
completeness %), invoices preview, current event details snapshot.
Booking info-complete % drives prompts elsewhere in the portal.
## 2. My bookings
Three vertical sections:
- **Upcoming** — full card per booking, large dates, status pill,
completeness %, primary CTA "Complete booking info →"
- **Past** — collapsed list, one compact row each. Tap to expand
+ download invoices
- **Cancelled** — only rendered if any exist; collapsed by default
No filters, no bulk actions, no search. Org clients have small lists.
## 3. Invoices
- Two tabs: **Deposit** · **Final**
- Per-row: booking name · invoice number · issue date · amount · status
(paid / outstanding) · download PDF
- Implicitly grouped by booking (booking title as subheader)
- Empty state: "No invoices yet — your invoice will appear once Christina
has issued it."
## 4. Participants (per event)
Where the team builds the participant list.
- Top: progress bar `17 / 28 added`
- Add row form: name (req) · email (optional) · age (req if child) ·
kind (overnight | day_guest) · notes
- **Bulk paste from CSV/TSV** — common for organisers with a Google
Sheet. Paste into modal, map columns, import.
- Per-row inline editing, dietary chip preview, allergy preview
- **"Send dietary self-form to all guests"** — sends each guest a
tokenised link to fill in their own dietary preferences without
needing an account. Organiser sees status per guest (pending / done).
## 5. Room plan (per event)
Split view, same model as admin's room-plan tab.
- **Left — assignment grid:** rows = overnight participants. Click a
participant to select, click a room to assign. Capacity counter
per room.
- **Right — floor-plan visualisation:** SVG overlay on `grundrisse.pdf`,
hot-spotted with room rectangles. Room number, capacity dots filled
per occupant, flag icons.
- **Top banner** explaining the click-select pattern (no DnD in v1).
- **Auto-suggest button** ("Suggest a layout") — assigns participants
by priority: special-needs flags first, couples grouped if marked,
fill smallest rooms last.
- **Flag legend** above the floor plan (wheelchair, dogs allowed, quiet,
shared bath, ground floor, allergy-friendly bedding).
- **Validation inline:** can't assign over capacity; wheelchair-flagged
participants only assignable to wheelchair-accessible rooms (1, 2).
- Implicit autosave, "Saved" indicator.
## 6. Dietary info (per event)
Aggregate view + per-participant editor. Reuses the kitchen view's
dietary breakdown component, but editable.
- **Top:** dietary breakdown donut + missing-info count
("3 participants haven't told us their diet yet")
- **Quick action:** "Mark everyone as default vegan" — bulk set
- **Allergies block** — flat list with "+ Add allergy" picker
- **Pending list** — participants without dietary info, with prompt
- **Self-form CTA** mirrored from Participants page
## 7. Schedule & extras (per event)
### Schedule card
- Three time pickers: Frühstück · Mittagessen · Abendessen —
validated against venue's allowed-window config
- Free-text daily plan textarea
- Hint: "Kitchen needs your meal times to plan portions."
### Extras card
Toggle list with descriptions and gross (brutto) prices:
- Cake (€3 / person / day) → per-day calendar picker
- Second seminar room (€80 / day for the whole stay)
- Early arrival before lunch (€30 / person flat)
- Bedlinen & towel set (€14 / person) — toggle "provided by Seminarhof"
vs "guests bring their own"
- Massage bench hire (€15 each + qty)
- Sauna note: "Logged on departure — €10 / person, min €50 total"
- Subtotal preview at bottom
- Special requests textarea — "Anything else we should know?"
## Cross-page UX
- **Completeness pill** in the corner of every page — same "Booking
info complete %" indicator as the Overview, so the client always
knows what's left
- **Implicit autosave** — no manual save button, "Saved" indicator
next to the field after each change
- **Per-event switcher** at the top of the sidebar, lists upcoming
events first, past events below
- **Empty states everywhere** — written empty state with a one-line
"what should I do" prompt
- **Locale-aware** — German default, EN per user `locale` field
- **Mobile** — sidebar collapses to a bottom-nav-style icon strip; all
pages stack to single column
## Accessibility
- Keyboard navigation across all interactive elements (no DnD means no
keyboard fallback gap)
- ARIA-correct selects, dialogs, tabs (Mantine handles most by default)
- Sufficient contrast on the dark surface (verify with theme tokens
before shipping)
- Floor-plan SVG has text labels per room, not just visual rectangles
## Components shared with admin/staff
- Dietary breakdown donut + counts (kitchen view also uses)
- Floor-plan SVG component (admin booking detail also uses)
- Status pill with venue-theme colour tokens
- Completeness pill / progress component
- Room-flag icons set

131
docs/design/public.md Normal file
View File

@@ -0,0 +1,131 @@
# Public Surfaces — UI Design
Audience: prospective clients browsing for dates, returning organisers
checking the calendar, individual guests of a confirmed event filling in
their own dietary info via a tokenised link.
Theme: same Mantine tokens as admin/client/staff (dark surface, red
primary), but **lighter compositions** — more whitespace, larger
typography, hero-style sections. Public is read-only or single-form.
All public pages support an `?embed=1` query param that strips the
shell (header/footer) for WordPress iframe inclusion. Embedded pages
emit `postMessage('resize', { height })` so the WP iframe wrapper can
auto-fit content height.
DE default, EN toggle in the header. Browser locale auto-detected on
first visit; preference persisted.
## 1. Availability calendar — `/availability`
Replaces the public Google Sheet.
- 12 months ahead by default, horizontally scrollable
- Day cells coloured by status (token palette shared with admin):
- `free` — neutral pale
- `enquiry` — soft pink (semi-transparent)
- `reserved` — amber
- `confirmed` — green
- **Privacy:** event name visible on hover/tap **only** for bookings
with `online-ad=true` consent; all other booked days show status
only, never group name. (Q1 lock: "Status + opt-in event names")
- Click a `free` day → "Reserve this date" CTA → opens `/enquiry`
with that date pre-filled
- View toggle: month · quarter · year
- Year-header strip: occupancy %, free-day count
- Embeddable: `/availability?embed=1` returns just the grid
## 2. Events page — `/events`
Lists upcoming events whose organiser opted into public listing
(`online-ad=true`).
- Card per event:
- Cover image (organiser-uploaded, optional)
- Event name
- Dates
- Organiser name
- Short description (`course-info` form field)
- External link to organiser's website (`your-website`)
- Filter chips: This year · Next year · All
- Search (event name / organiser)
- SEO: server-rendered, meta tags, JSON-LD `Event` schema
- Empty state: "No events listed at this moment — please check our
retreat calendar for available dates."
- Cross-link to `/availability` in header
## 3. Booking form — `/enquiry`
The iframe-able binding booking form. Replaces the current WordPress
form long-term; short-term we accept WP webhook posts to the same
backend (Phase 1 of public-entry plan).
### Sections (all sticky on scroll)
1. **Your event** — name, description, website, dates, est. participants
2. **Organiser** — name, email, phone, full legal name, organisation
3. **Billing** — invoice address, payment preference (cash | transfer)
4. **Schedule** — desired arrival / departure times, structured meal
times (Frühstück / Mittag / Abend, validated against venue's
allowed-window config), free-text daily plan
5. **Extras** — cake / 2nd seminar room / early arrival / bedlinen /
massage bench (gross prices)
6. **Setup** — equipment needed (beamer, flipchart, piano, chair count)
7. **Public listing consent** — opt in to show on `/events`
8. **Terms** — AGB acceptance (versioned by event year), privacy
notice acceptance
### Behaviour
- Inline validation; submit failure highlights incomplete sections
- On submit:
- Status = `form_received` (per locked status enum)
- €300 deposit due notice within 14 days
- Auto confirmation email in user's locale (DE/EN)
- Embeddable: `/enquiry?embed=1`, postMessage height resize
## 4. Guest self-form — `/d/:token`
Tokenised link a guest receives from the organiser to fill in their
own dietary info — no account required.
- Single page: name (pre-filled, editable), dietary tag picker,
allergy list, free-text "anything else"
- Token expires when booking is `completed`
- DE/EN locale-aware, auto-detected from browser
- One-time submit with confirmation page; re-openable until cutoff
- Mobile-first (most guests fill from phones)
## 5. Authenticated landing — `/`
- Unauthenticated users → thin welcome page with links to
`/availability`, `/events`, sign-in CTA
- Authenticated users → role-based redirect to their portal
No marketing copy on the platform itself — Sarah keeps the WordPress
site as the brand front door.
## SEO and structured data
- `/availability` — meta description, no indexable detail
- `/events` — JSON-LD `Event` schema per event card; `<title>`
per-card on detail expansion if added later
- `/enquiry``noindex` (form, not content)
- `/d/:token``noindex` (private)
- `/` — minimal `<title>` and `og:` tags
## Components shared with the rest of the platform
- Status pill (theme tokens)
- Day-cell calendar component (admin calendar reuses, with edit
affordances)
- AGB / privacy acceptance component (also used in client portal
on first login)
- DE/EN locale toggle
## Non-goals
- No marketing copy / about / blog — handled by WordPress
- No public booking detail page (events page is summary-only; clients
see full detail in their portal once authenticated)
- No public reviews / testimonials in v1
- No payment processing (deposits are bank transfer, per §6)

172
docs/design/staff.md Normal file
View File

@@ -0,0 +1,172 @@
# Staff Portal — UI Design
Two sub-views: **Kitchen** and **Cleaning**. Same mobile-first shell,
different content compositions.
Audience: kitchen cooks, housekeepers. Read-only. Mobile/tablet first
(phones over the counter, tablets in the housekeeping cupboard).
Polled refresh, no websockets in v1.
Theme: same Mantine tokens as admin/client (red primary on dark
surface), but with a simpler layout — large touch targets, generous
spacing, fewer columns.
## Auth and routing
- Same `js-auth` flow as admin/client
- After login, role-based redirect: kitchen → `/staff/kitchen`,
cleaning → `/staff/cleaning`
- A staff user can hold both roles (small team); UI offers a switcher
in the header
## Shell
- Top bar: venue name · current date · user avatar · role switcher (if
applicable) · refresh button
- Body: list / detail
- No sidebar (mobile-first)
---
## Kitchen view
### 1. Event list (landing)
- One card per upcoming event, chronological
- Card content: dates · group name · headcount · today's meal-count
badge · "info missing" alert badge if any dietary / headcount
fields are still pending
- Filter chips: Today · This week · Upcoming
- Tap a card → event detail
### 2. Event detail
The workhorse. Visual-heavy.
#### Sticky header
- Group name · course name · headcount · dates
- Meal times: Frühstück / Mittagessen / Abendessen (single line)
#### Dietary breakdown card (top of body)
- `RingProgress` or `DonutChart` showing % per dietary tag (vegan,
vegetarian, gluten-free, lactose-free, etc.)
- Counts per slice
- Same colour tokens reused on the participant list below for visual
continuity
#### Today's meals card
- Small KPI tiles per meal: Frühstück / Mittagessen / Abendessen
- Each tile shows: count for today, delta vs. headcount when
participants arrive late or leave early ("26 (2)")
- Cake tile shows yes/no for today + €3/person/day total when on
#### Allergies & specials card
- Flat list grouped by allergen (nuts, soy, strict gluten, dairy, etc.)
- Each row: icon · allergen · count · participant initials
- Severity tier:
- ⚠ standard allergy (avoid in dishes)
- ★ requires fully separate prep (the rare 1% that triggers the
manual special-diet surcharge)
#### Cake days card
- Calendar strip (5 days for a 5-night booking)
- Cake-on days highlighted; €3/person/day shown
#### Notes card
- Per-booking admin notes filtered to kitchen-relevant only
(`notes.audience=kitchen`). Personnel notes never appear here.
#### Daily plan (free-text)
- The free-text portion of the schedule the client submitted
- Read-only
### Data sources
All read-side aggregations on existing tables — no new schema:
- Dietary breakdown: `SUM(participants) GROUP BY dietary_tag` for the
booking
- Today's meals: `participants WHERE booking_id = X AND kind IN
(overnight, day_guest) AND arrival_date <= today AND departure_date >
today`
- Allergies: flat list from `participant_allergies` joined to allergen
reference table
- Cake days, meal times: from `bookings.extras` and
`bookings.meal_times`
---
## Cleaning view
### 1. Event list (landing)
- One row per event sorted by changeover date (departure or arrival)
- Row content: changeover type icon (`departure` / `arrival` /
`flying`) · date · group name · room count · flying-changeover badge
- Tap → cleaning detail
### 2. Cleaning detail
Spatial / temporal, not dietary.
#### Sticky header
- Group · dates · room count · flying-changeover badge if applicable
("Rooms cleared by 10:00, ready by 17:00")
#### Arrivals / departures card
- Arrival time and departure time
- Number of rooms in use
- Whether the previous group's departure overlaps (flying changeover)
#### Room status table (the workhorse)
| Room | Type | Beds | Flags | Status |
|------|------|------|-------|--------|
| 5 | MBZ | 3 | quiet | clean for arrival 15:00 |
| 8 | DZ | 2 | shared bath, quiet | clean for arrival 15:00 |
| 10 | DZ | 2 | dogs allowed | extra deep clean |
- Driven by the booking's room assignments
- Flags surfaced as icons (dogs, wheelchair, allergy-friendly,
shared bath, double bed 140cm)
- "Extra deep clean" auto-shown for `dogs_allowed` rooms when occupied
- Per-room admin note shown inline if present
#### Notes card
- Per-booking admin notes filtered to cleaning-relevant only
(`notes.audience=cleaning`)
### Data sources
All on existing tables:
- Room assignments → which rooms are occupied
- Room flags → from `rooms`
- Flying changeover → derived from booking start_date == previous
booking end_date
---
## Shared shell components
- `<StaffEventCard />` — card used in both kitchen and cleaning
landing lists
- `<StickyEventHeader />` — same shell, different content props
- `<NotesCard audience="kitchen|cleaning" />` — filters notes by
audience tag
## Non-goals
- No editing anywhere in the staff view (read-only by definition)
- No real-time updates in v1 — manual refresh button + a "last fetched
X minutes ago" indicator
- No WhatsApp integration in v1 — handover §6 explicit
- No HR features (cook rota assignment is admin-side)

View File

@@ -0,0 +1,446 @@
# Discovery & Scoping Playbook
A repeatable method for going from "handover document + a folder of legacy
spreadsheets" to "buildable platform with locked decisions, persisted memory,
and a vetted business-owner sign-off". Designed to be driven by a human
with an AI coding assistant (Claude Code, Cursor, etc.) — not by the AI
alone.
The method works because each phase produces a **durable artifact** that
the next phase consumes. No phase relies on conversation history; if a
session ends, the next one resumes from the artifacts.
A worked example using this method is in `seminarhof-walkthrough.md`.
---
## Phase 0 — Repo init from template
**Goal.** Get a working repo on disk with the template's stack already
wired, plus a private working tree where production data can land
without ever touching git.
**Steps**
1. Create the empty remote repo (org-scoped, private by default).
2. Copy the template into a temp dir, init git, push to the new remote.
3. Move the working copy out of the temp dir into a permanent location.
4. **Before anything else**: extend `.gitignore` to cover all paths that
will hold production data, handover documents, or scrubbed data
derived from production data:
```
data/seed/
data/seed-scrubbed/
docs/handover/
```
5. Create empty `data/seed/` and `data/seed-scrubbed/` directories so
the AI can drop scripts referencing them without errors.
**Artifacts.** A pushed-to-main empty-template repo + gitignore lockdown.
**Anti-pattern.** Letting the AI commit a CSV before gitignore is set up.
Once production data hits a public remote, you can't un-leak it — even
after a force-push, the data is in clones and CI logs.
---
## Phase 1 — Inventory the inputs
**Goal.** Convert every source document (handover PDF, sample
spreadsheets, screenshots, hand-drawn diagrams) into a single
**structured inventory document** before designing anything.
**Why first.** Every later phase references the inventory. If you skip
straight to schema design, you'll model what you assume the source data
looks like rather than what it actually contains, and you'll re-discover
columns by accident two weeks in.
**Steps**
1. For each spreadsheet / source workbook, capture:
- Tab names and tab purposes
- Per-tab column list with your best-guess interpretation
- Cell value vocabulary if the cells aren't simple values (codes,
bracketed conventions, abbreviations, etc.)
2. For each source PDF (floor plans, AGB, etc.), extract structure into
tables — e.g. one row per room with whatever attributes are visible.
3. For each artifact, record what's **confirmed** vs **best-guess**.
Best-guesses become open questions later.
4. Cross-reference between artifacts to surface contradictions (e.g.
"this name appears here but not on the floor plan").
**Artifacts.**
- `docs/handover/csv-inventory.md` — structure of every source spreadsheet
- `docs/handover/rooms-inventory.md` (or similar per major artifact)
- Scribbled best-guess tables marked clearly as such
**Tip — anchoring.** When you later ask the business owner about a
column or value, anchor every question to its source artifact ("In
**Belegungsplan** the column `auto-renewal-2`…"). It eliminates
ambiguity for the owner and lets you trace each decision back to its
trigger.
---
## Phase 2 — Privacy-first data handling
**Goal.** Get useful structural insight from production data without
ever letting raw personal data reach an LLM API.
**Why this matters.** Most LLM APIs retain inputs for ~30 days for
trust & safety. A single conversational `cat` of a customer CSV
becomes a GDPR Art. 9 (special-category) breach if dietary, allergy,
or health data is in it. The fix is structural, not policy: the AI
cannot leak data it never saw.
**Method**
1. Owner exports CSVs / Excel from source systems and drops them in
`data/seed/` (gitignored).
2. AI writes a **scrubber script** — pure stdlib, no deps — that:
- Reads every CSV in `data/seed/`
- Detects PII columns by header heuristics in the local language
(German + English in our case)
- Detects PII patterns regardless of column (email, phone, IBAN
regex)
- Replaces values with **deterministic fakes** (hash → fake, so the
same input maps to the same fake — preserves foreign-key integrity
and repeated-organisation grouping)
- Blanks notes/comments columns by default (highest-risk free text)
- Outputs to `data/seed-scrubbed/` (also gitignored)
3. AI never reads `data/seed/`. Only ever reads `data/seed-scrubbed/`.
**What survives in scrubbed output:** column names, row counts, value
patterns, dates, statuses, room numbers, dietary text values, prices.
Enough to design schema honestly.
**What never reaches the AI:** real names, emails, phones, addresses,
IBANs, free-text notes.
**Artifacts.**
- `scripts/scrub-csvs.mjs` (or equivalent)
- `data/seed/` and `data/seed-scrubbed/` populated locally; both
gitignored
**Anti-pattern.** "I'll just paste a few rows in chat to show structure."
A few rows is enough to identify individuals via correlation; resist.
Structure can always be conveyed via column names + 2 obviously-fake
sample rows (`Max Mustermann`).
**Edge case the owner will raise.** "But the structure is messy and you
need to see real values to make sense of it." This is when the scrubber
earns its keep — they get to keep the messiness, you get to read the
output safely.
---
## Phase 3 — Question discipline
**Goal.** Convert ambiguity into locked decisions one at a time, with
rationale, anchored to source.
**The core rule.** Ask **one** question per turn. Multi-question batches
encourage thin answers and mask cross-question contradictions.
**Question shape that works**
```
In <source artifact>, <specific anchor> — <plain question>?
[24 options, each with a one-line consequence]
```
Each option must spell out what *changes in the platform* if it's
chosen. "Cleaner" is not a consequence; "no `house_half` column on
rooms" is.
**When the owner picks an option** → lock it. Move on.
**When the owner says "I don't know"** → log to a single deferred-items
file with the consequence, and move on. Don't argue.
**When the owner gives a free-text answer that doesn't match the
options** → restate it in one line ("Got it — X."), confirm, lock,
move on. The options were a forcing function, not a constraint.
**When the owner asks "what would you recommend?"** → answer in one
sentence with the trade-off, then bring them back to deciding. Don't
make the decision for them.
**Artifacts.**
- Running `docs/handover/open-questions.md` — every deferred question
with its rationale (so anyone can pick it up later without re-deriving
the context)
- Memory entries for locked decisions (Phase 4)
**Anti-pattern.** Asking "are you sure?" after every answer. Lock the
decision and move forward; if it turns out wrong, the audit log of
locked decisions makes it cheap to revisit.
**Anti-pattern.** Asking infrastructure questions (DB engine, hosting,
file storage) when the chosen framework already provides them. Skip
those entirely; they aren't business decisions.
---
## Phase 4 — Lock decisions in memory
**Goal.** Make decisions survive session boundaries.
**Why.** Conversation context disappears when the session ends. A
locked decision that lives only in conversation is a decision that
will be re-litigated the next time someone (you, the AI, a teammate)
opens a new session.
**Steps**
1. As decisions are locked in Phase 3, mirror them into a memory file
under `~/.claude/projects/<project-key>/memory/`.
2. The memory file is a *living* artifact — append new locks, update
superseded ones (don't pile contradictions), remove decisions that
were rolled back.
3. Reference the memory file from `MEMORY.md` so future sessions
auto-load it.
4. Distinguish **business decisions** (locked, in memory) from
**infra decisions** (defer to framework, don't pretend to lock).
**Artifacts.**
- `~/.claude/projects/<project-key>/memory/project_<name>.md`
- `MEMORY.md` link
**What goes in memory.** Locked rules: status enums, pricing logic,
auth model, multi-tenancy stance, scope exclusions, deferred items,
privacy guardrails. The shape of decisions, not the implementation.
**What does *not* go in memory.** Code, file paths, line numbers, the
contents of any source data file. Those rot fast and you can re-read
them from disk any time.
---
## Phase 5 — Generate a business-owner interview prompt
**Goal.** Hand the open questions back to the non-technical business
owner in a form they can answer without a developer babysitting them.
**The artifact.** A single self-contained markdown file the owner
pastes into a fresh AI session. The file contains:
1. A how-to-use header (paste this whole file into Claude/ChatGPT, etc.)
2. Instructions to the LLM (interview tone, ask one at a time, restate
in plain language, push back on vagueness, don't accept "we'll
figure it out")
3. Background section: what the business is, what the platform replaces,
what's already decided (so the LLM doesn't re-open settled ground)
4. Each open question, complete with a *why-it-matters* rationale the
LLM reuses verbatim
5. Output spec — the LLM must produce a single answers document at the
end, with a specific structure, that the owner sends back
**Why this works.** The owner can answer at their own pace, in their
own language. The LLM acts as a patient interviewer rather than a
form. Vague answers get probed automatically. The output document is
mechanical to consume back into the project.
**Artifacts.**
- `docs/handover/business-requirements-prompt.md`
- (later) `docs/handover/business-requirements-answers.md`
**Anti-pattern.** Sending a 20-row spreadsheet to the owner and asking
them to fill it in. Spreadsheets get half-answered or skipped on the
hard items. A guided conversational interview surfaces edge cases the
owner wouldn't have thought to flag.
---
## Phase 6 — Receive answers, propagate, re-lock
**Goal.** Take the owner's answers and propagate them through every
artifact built so far.
**Steps**
1. Save the raw answers verbatim as `business-requirements-answers.md`.
2. For each answered question:
- If it overrides a best-guess in an inventory document, update the
inventory with the authoritative value (mark the source: "confirmed
by owner on <date>").
- Move the question out of `open-questions.md`.
- Add the locked rule to memory (Phase 4 file).
3. Items marked `DEFER` stay in `open-questions.md` with the next
decision-maker named. They are the only thing that file should
contain after Phase 6.
**Artifacts.**
- `business-requirements-answers.md` (verbatim)
- Updated inventories with `confirmed by` notes
- Slimmed-down `open-questions.md`
- Updated memory file
**The discipline test.** After Phase 6, a brand-new AI session opening
this repo should be able to design the schema purely from the
artifacts in the repo and memory — no conversation history needed. If
that's not true, an artifact is missing.
---
## Phase 7 — UI design before code
**Goal.** Decide what each portal *looks like* and *contains* before
writing components.
**Why this is its own phase.** The data model is now locked, but a
data model alone doesn't tell you what the UI is. Two valid designs
can sit on the same schema and feel like different products.
**Method**
1. Identify the surfaces (in this template's case: client portal, admin,
staff, public).
2. For each surface, decide:
- Audience and primary jobs-to-be-done
- Default landing screen (what they see when they log in)
- Density and tone (warm/dense/minimal/marketing)
- Mobile vs desktop priority
- Shared theme tokens vs surface-specific overrides
3. For the **highest-stakes surface** (usually admin), drill into the
landing screen's sections one at a time, the same way Phase 3 drills
into business questions.
**Artifacts.**
- `docs/design/<surface>.md` per portal — sections, components,
interactions
- Reference screenshots from the owner where they exist
**Anti-pattern.** "Same theme, same components everywhere." A
client-facing card-based dashboard makes admin Christina cry when she
needs to manage 200 bookings across 5 years. Same tokens; different
component vocabularies per surface.
---
## Phase 7.5 — Acceptance scenarios before code
**Goal.** Every workflow that's about to be built has a `.feature` file
*before* a line of implementation code is written.
**Why this is its own phase.** Acceptance scenarios written *after* the
code rationalise whatever shipped — they validate the implementation,
not the requirement. Written *before*, they pin the requirement and
double as the build's definition of done. They also surface
contradictions in the locked decisions that schema design and UI
mockups didn't catch (e.g. "the feature says clients can do X, but role
matrix in memory says only admins can").
**Method**
1. For each surface from Phase 7, list the workflows that have to work
end-to-end (login, view-own-bookings, room-assignment,
create-invoice, etc.).
2. For each workflow, write a Cucumber `.feature` file with at least
one happy-path scenario and one role-gating / permission scenario.
Use the generic step library from the e2e harness; project-specific
steps come later.
3. Auth is **always the first feature**. Without it the rest of the
scenarios can't even reach a logged-in state.
4. Don't write the step *implementations* yet beyond the generic ones
(`I visit`, `I log in as`, `I see`). Project-specific steps land in
Phase 8 alongside the code they exercise.
**Artifacts.**
- `e2e/tests/features/auth.feature` — first, always
- `e2e/tests/features/<workflow>.feature` — one per locked workflow
- A failing test run (red) — the baseline against which Phase 8
progress is measured
**Anti-pattern.** "We'll add tests once it's working." Tests added
after-the-fact tend to mirror the code's shape rather than the
requirement's shape, and they don't catch the gaps the requirement
process should have caught. The whole point of Phase 7.5 is that the
test is the requirement, in executable form.
**Anti-pattern.** Writing 40 detailed scenarios up front. Cover the
locked workflows with **one happy path + one critical edge each**.
Exhaustive scenario coverage is a Phase 9 activity (regression as the
product grows), not a Phase 7.5 one.
---
## Phase 8 — Build (vertical slice)
**Goal.** Ship a slice that touches every layer (DB → backend → API →
UI) for one real workflow, instead of completing a full layer at a
time.
**Why.** Layer-at-a-time builds discover integration problems last,
when they're most expensive. Vertical-slice builds discover them in
week one.
**Build auth first.** Before any feature work: real login, real
sessions, real role enforcement. Every later function takes its
`userId` and `role` from the session, never from a `DEMO_USER`
constant or an input parameter. Skipping this and "wiring auth at the
end" sounds cheap but isn't — every function signature, every UI page,
every permission check has to be retrofitted, contracts re-versioned,
seeds re-hashed, and route guards added across the whole app. Doing it
first costs a day; doing it last costs a week and leaves footguns in
every file you didn't revisit.
**What "auth first" means concretely:**
1. Sessions table + cookie middleware (or equivalent for the chosen
stack) live before the first feature function is written.
2. Password hashing uses a runtime-portable primitive (WebCrypto
PBKDF2, Argon2-wasm, etc.) — not bcrypt/node:crypto if the target
includes edge runtimes.
3. Seed data includes hashed passwords for every demo role from day
one. Demo passwords are documented in seed comments only, never in
memory or repo docs.
4. `pikkuFunc` (or the equivalent session-required handler) is the
default; sessionless handlers are the explicit, justified
exception.
5. UI has a `RequireAuth` wrapper with a `roles` prop *before* the
first protected page is built.
**Steps**
1. Land auth as the first vertical slice (login → cookie → `me` → role
guard on one trivial admin page).
2. Pick the next workflow that exercises the trickiest parts of the
schema (in our case: client logs in → views their booking → fills
room plan → admin sees the update → kitchen view reflects it).
3. Build only what that workflow needs at every layer.
4. Once it's working end-to-end, expand sideways.
**Artifacts.**
- A demoable URL
- A short loom-style "what works, what doesn't" note for the owner
**Anti-pattern.** "We'll wire real auth at the end, just hard-code
`DEMO_USER` for now." Every function written under that assumption
takes a `userId` input parameter that has to be deleted later, and
every UI mutation passes it explicitly. Retrofitting means touching
every file twice and re-versioning every contract.
---
## Cross-phase rules
**Auto mode.** When the owner has set the AI to "auto" / "execute
without confirming", still pause for irreversible actions: deleting
data, force-pushing, or actions that touch shared systems. Authorisation
to "go fast" is not authorisation to be careless.
**Memory hygiene.** When a locked decision is later overridden, *update*
the memory file rather than appending a contradiction. Stale memory is
worse than no memory.
**Confidentiality.** Treat any owner-shared spreadsheet, screenshot, or
PDF as containing data the org has not authorised to share with an AI
provider. Either scrub it (Phase 2) or extract structure manually before
ingesting.
**Documentation order.** Inventory → questions → answers → memory →
design → acceptance scenarios → code. Skipping inventory or questions
costs more downstream than the time saved. Skipping acceptance
scenarios means the build's definition of done is "feels right when I
click around" — which is how `DEMO_USER` constants ship to production.