chore: seminarhof customer project
This commit is contained in:
BIN
docs/.DS_Store
vendored
Normal file
BIN
docs/.DS_Store
vendored
Normal file
Binary file not shown.
168
docs/booking-lifecycle.md
Normal file
168
docs/booking-lifecycle.md
Normal 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 7–13, 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
255
docs/design/admin.md
Normal 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 3–10 / 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
161
docs/design/client.md
Normal 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
131
docs/design/public.md
Normal 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
172
docs/design/staff.md
Normal 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)
|
||||
446
docs/process/discovery-playbook.md
Normal file
446
docs/process/discovery-playbook.md
Normal 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>?
|
||||
[2–4 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.
|
||||
Reference in New Issue
Block a user