chore: seminarhof customer project

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

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)