# RFC-TIMETABLE — Artist Timetable Module ## 1. Status - **State:** Draft for review - **Created:** 2026-05-08 - **Version:** v0.1 - **Owner:** Bert Hausmans - **Origin:** UX brainstorm session 2026-05-08 (Claude Chat) — concept + three PoC iterations + six locked decisions - **Related:** - `SCHEMA.md` §3.5.7 (artists, performances, stages, stage_days, advance_sections) - `BACKLOG.md` ARCH-09 (Artist model — hard prerequisite), ART-02 (Timetable backlog item) - `ARCH-FORM-BUILDER.md` §17 (artist_advance purpose subject_type) - `CLAUDE.md` "Order of work" (17-step module-generation sequence) - `design-document.md` §3.5.10 Database design rules - `dev-docs/PurposeRegistry` — artist_advance purpose with subject_type=artist ## 2. Why this RFC exists The Artist Timetable is the central planning surface for festival programming. It is operationally critical: production managers spend hours per week on it, last-minute changes are common, and a bug here visibly breaks the show. Three properties make this module non-trivial enough to warrant up-front spec: 1. It depends on a model (Artist) that does not exist yet — see ARCH-09. 2. It bridges three concerns that festival operators conflate but Crewli must keep distinct: stages (physical), festival_sections (organisational), and advancing (per-section workflow). 3. The interaction surface (drag-drop Gantt with conflict detection, cross-day stage variance, popover with advancing summary) does not exist off-the-shelf in the current frontend stack — the build-vs-buy choice for the rendering library has multi-week consequences. This RFC captures every architectural decision so implementation does not re-litigate them under time pressure. PoC iterations (chat artefacts v1/v2/v3) are visual references, not authoritative — this document is. ## 3. Scope & non-scope ### In scope (v1) - CRUD for stages and stage_days (which stage runs which day) - CRUD for performances (artist on stage at time) - Horizontal Gantt timetable: stages as rows, time as x-axis, day tabs - Drag/drop performance: re-time and re-stage in one gesture - Resize performance duration (snap to 15 min) - Click-to-add performance from empty grid cell - Detail popover with avatar, status pill, advancing aggregate, status switch, delete, manage-booking link - Conflict detection: same-stage same-day overlapping non-cancelled performances - Back-to-back marker: ≤5 min gap between consecutive performances on same stage - "Edit lineup" matrix: bulk toggle stages × days - Empty-day state: copy-from-other-day affordance - Activity log on all mutations - Multi-tenancy via OrganisationScope (FK-chain through event) ### Out of scope (v1) - **Artist Handling / show-day check-in view** — separate Mission Control module - **Capacity warning** on performance blocks — requires a new column (`performances.expected_attendance` or similar). Defer to v2; backlog as ART-04. - **Advance section CRUD** — covered by separate Artist Advancing module (BACKLOG ART-01). The popover only *reads* advance_sections aggregate. - **PDF / print export** of running order — backlog as ART-05. - **Multi-select bulk shift** of performances ("move everything on Mainstage +15 min") — backlog as ART-06. - **Undo/redo stack** — backlog as ART-07. - **Mobile-optimised view** — timetable is a desktop tool. Mobile gets a read-only list view in a later iteration. - **Stage templates across events** ("copy stages from last year's event") — backlog as ARCH-10, parallel to ARCH-03 (festival_section templates). ## 4. Locked design decisions ### D1 — Block visual: stage-stripe + status-fill Each performance block carries: - Background fill = booking_status colour (6 colours, see §10). - 3px left stripe = `stages.color` for stage-grouping cue. - Right grab handle for resize. Rationale: status-fill alone (PoC v2) lost stage grouping when many stages shared similar status. The stripe restores the visual stage-cluster signal at zero cost. (Bert decision 2026-05-08.) ### D2 — Advancing aggregate, not per-section dots The detail popover shows `n/m completed` (e.g. "3/5 advancing complete") and the underlying section list as a tooltip on hover. No per-section dots on the block itself. Rationale: `advance_sections` is configurable per artist — fixed 5-dot icons break the moment one artist has 4 sections and another has 7. Aggregate fraction degrades gracefully across configurations and reads cleanly at small sizes. (Bert decision 2026-05-08.) The aggregate is computed as: ``` n = count(advance_sections WHERE artist_id = X AND submission_status = 'accepted') m = count(advance_sections WHERE artist_id = X) ``` Implementation: cached on `artists` model as a denormalised `advancing_completed_count` + `advancing_total_count` pair, recomputed via observer on `advance_section` create/update/delete. Avoids N+1 on timetable load. To be added in same migration as Artist model (ARCH-09). ### D3 — Manage booking opens artist page (not modal) Click "Manage booking" in popover → navigate to `/events/{event}/artists/{artist}?return=timetable&day=fri&t=210000`. The artist page surfaces all related records (performances, advance_sections, contacts, riders, itinerary) as tabs. A prominent return banner at top: "← Back to Timetable (Vr 21:00)" restores scroll/tab context. Rationale: artist record is too large for modal (5+ relation tables). Page navigation matches every other Crewli edit surface (shifts, persons, festival_sections). Bookmarkability + cmd-click-to-new-tab are bonuses no modal can provide. (Bert decision 2026-05-08.) ### D4 — Stage-day filtering is enforced everywhere The timetable for day D shows only stages where `stage_days(stage_id, D)` exists. Performances scheduled on `(stage_id, D)` where `stage_days(...)` does not exist are **hidden but not deleted** — toggling a stage off a day must be reversible without data loss. This is enforced at: - API list endpoint (filter at SQL level) - Frontend rendering (defensive — trust API but verify) - Performance creation — the FormRequest validates that `stage_days` exists for the requested `(stage_id, performance.date)` and rejects with 422 otherwise. ### D5 — Conflict detection scope: same-stage same-day overlap only Cross-stage overlap (same artist on two stages simultaneously) is **not** flagged in v1. Reason: an artist on two stages at once is rare but legitimate at small festivals where one DJ runs main set + silent disco simultaneously. Surface this on the artist page in a future iteration, not on the timetable. Cancelled performances participate in **no** conflict check. ### D6 — B2B marker rule Two consecutive non-cancelled performances on the same stage with `p2.start - p1.end ∈ [0, 5]` minutes get a B2B marker (small dot at the boundary). Useful for stage managers planning changeover windows. Pure rendering concern — no schema impact. ### D7 — Drag-drop interaction model - Drag block horizontally → re-time (snap to 15 min) - Drag block vertically → re-stage (only across stages active on current day) - Drag right edge → resize duration (snap to 15 min, min 15 min) - Drag is committed on mouseup with single PATCH request - Failed PATCH (validation error, conflict the user wants to refuse) reverts block to its origin position with a toast ### D8 — Rendering library: custom Vue components, not FullCalendar **Recommendation: build custom.** FullCalendar timeline view (`resourceTimelinePlugin`) was the obvious choice on paper but has three blockers: 1. Premium-licensed (~€600/yr commercial). Acceptable cost but adds procurement friction and a vendor dependency. 2. The advancing-aggregate badge, stage-stripe, B2B marker, and conflict ring all require `eventDidMount` render hooks — at that point we're reimplementing rendering inside FC's render cycle, fighting its DOM. 3. Cross-day stage filtering (D4) does not map cleanly to FC's resource model. Workaround is per-day resource list rebuild on tab switch, which defeats incremental-render benefits. Custom Vue components (TimetableGrid, StageRow, PerformanceBlock, PerformancePopover, LineupMatrix) on top of native HTML5 drag-and-drop give total control at ~3 days additional frontend cost. The PoC v3 is already ~80% of the rendering logic. PoC code must be rewritten as proper Vue 3 components (Composition API, `