feat: schema v1.7 + sections/shifts frontend

- Universeel festival/event model (parent_event_id, event_type)
- event_person_activations pivot tabel
- Event model: parent/children relaties + helper scopes
- DevSeeder: festival structuur met sub-events
- Sections & Shifts frontend (twee-kolom layout)
- BACKLOG.md aangemaakt met 22 gedocumenteerde wensen
This commit is contained in:
2026-04-08 07:23:56 +02:00
parent 6f69b30fb6
commit 6848bc2c49
19 changed files with 2560 additions and 87 deletions

View File

@@ -1,14 +1,21 @@
# Crewli — Core Database Schema
> Source: Design Document v1.3 — Section 3.5
> **Version: 1.6** — Updated April 2026
> **Version: 1.7** — Updated April 2026
>
> **Changelog:**
>
> - v1.3: Original — 12 database review findings incorporated
> - v1.4: Competitor analysis amendments (Crescat, WeezCrew, In2Event)
> - v1.5: Concept Event Structure review + final decisions
> - v1.6: Removed `festival_sections.shift_follows_events` — feature does not fit Crewli's vision (staff planning is independent of artist/timetable planning)
> - v1.6: Removed `festival_sections.shift_follows_events`
> - v1.7: Festival/Event architecture — universal event model supporting
> single events, multi-day festivals, multi-location events, event series
> and periodic operations (recurrence). Added `parent_event_id`,
> `event_type`, `sub_event_label`, `is_recurring`, `recurrence_rule`,
> `recurrence_exceptions` to `events`. Added `event_person_activations`
> pivot. Changed `persons.event_id` to reference festival-level event.
> Added `event_type_label` for UI terminology customisation.
---
@@ -110,23 +117,72 @@
### `events`
| Column | Type | Notes |
| ----------------- | ------------------ | ------------------------------------------------------------------------- |
| `id` | ULID | PK |
| `organisation_id` | ULID FK | → organisations |
| `name` | string | |
| `slug` | string | |
| `start_date` | date | |
| `end_date` | date | |
| `timezone` | string | default: Europe/Amsterdam |
| `status` | enum | `draft\|published\|registration_open\|buildup\|showday\|teardown\|closed` |
| `deleted_at` | timestamp nullable | Soft delete |
> **v1.7:** Universal event model supporting all event types:
> single events, multi-day festivals, multi-location events,
> event series, and periodic operations (schaatsbaan use case).
>
> **Architecture:**
>
> - A **flat event** has no parent and no children → behaves as a normal single event
> - A **festival/series** has no parent but has children → container level
> - A **sub-event** has a `parent_event_id` → operational unit within a festival
> - A **single event** = flat event where festival and operational unit are the same
>
> **UI behaviour:** If an event has no children, all tabs are shown at the
> event level (flat mode). Once children are added, the event becomes a
> festival container and children get the operational tabs.
**Relations:** `belongsTo` organisation, `hasMany` festival_sections, time_slots, persons, artists, briefings
**Indexes:** `(organisation_id, status)`, `UNIQUE(organisation_id, slug)`
| Column | Type | Notes |
| ----------------------- | ------------------ | ------------------------------------------------------------------------------------------ |
| `id` | ULID | PK |
| `organisation_id` | ULID FK | → organisations |
| `parent_event_id` | ULID FK nullable | **v1.7** → events (nullOnDelete). NULL = top-level event or festival |
| `name` | string | |
| `slug` | string | |
| `start_date` | date | |
| `end_date` | date | |
| `timezone` | string | default: Europe/Amsterdam |
| `status` | enum | `draft\|published\|registration_open\|buildup\|showday\|teardown\|closed` |
| `event_type` | enum | **v1.7** `event\|festival\|series` — default: event |
| `event_type_label` | string nullable | **v1.7** UI label chosen by organiser: "Festival", "Evenement", "Serie" |
| `sub_event_label` | string nullable | **v1.7** How to call children: "Dag", "Programmaonderdeel", "Editie" |
| `is_recurring` | bool | **v1.7** default: false. True = generated from recurrence rule |
| `recurrence_rule` | string nullable | **v1.7** RRULE (RFC 5545): "FREQ=WEEKLY;BYDAY=SA,SU;UNTIL=20270126" |
| `recurrence_exceptions` | JSON nullable | **v1.7** Array of {date, type: cancelled\|modified, overrides: {}}. JSON OK: opaque config |
| `deleted_at` | timestamp nullable | Soft delete |
**Relations:**
- `belongsTo` Organisation
- `belongsTo` Event as parent (`parent_event_id`)
- `hasMany` Event as children (`parent_event_id`)
- `hasMany` FestivalSection, TimeSlot, Artist, Briefing (on sub-event or flat event)
- `hasMany` Person (on festival/top-level event)
**Indexes:** `(organisation_id, status)`, `(parent_event_id)`, `UNIQUE(organisation_id, slug)`
**Soft delete:** yes
> **v1.5 note:** `volunteer_min_hours_for_pass` removed — not applicable for Crewli use cases.
**Helper scopes (Laravel):**
```php
scopeTopLevel() // WHERE parent_event_id IS NULL
scopeChildren() // WHERE parent_event_id IS NOT NULL
scopeWithChildren() // includes self + all children
scopeFestivals() // WHERE event_type IN ('festival', 'series')
```
**Event type behaviour:**
| event_type | Has parent? | Description |
|---|---|---|
| `event` | No | Flat single event — all modules at this level |
| `event` | Yes | Sub-event (operational unit within festival) |
| `festival` | No | Multi-day festival — children are the days |
| `series` | No | Recurring series — children are the editions |
> **Recurrence note (BACKLOG ARCH-01):** `recurrence_rule` and
> `recurrence_exceptions` are reserved for the future recurrence generator.
> For now, sub-events are created manually. The generator will auto-create
> sub-events from the RRULE when built.
---
@@ -537,21 +593,26 @@ $effectiveDate = $shift->end_date ?? $shift->timeSlot->date;
### `persons`
| Column | Type | Notes |
| ---------------- | ------------------ | -------------------------------------------------------------------- |
| `id` | ULID | PK |
| `user_id` | ULID FK nullable | → users — nullable: external guests/artists have no platform account |
| `event_id` | ULID FK | → events |
| `crowd_type_id` | ULID FK | → crowd_types |
| `company_id` | ULID FK nullable | → companies |
| `name` | string | |
| `email` | string | Indexed deduplication key |
| `phone` | string nullable | |
| `status` | enum | `invited\|applied\|pending\|approved\|rejected\|no_show` |
| `is_blacklisted` | bool | |
| `admin_notes` | text nullable | |
| `custom_fields` | JSON | Event-specific fields — not queryable |
| `deleted_at` | timestamp nullable | Soft delete |
> **v1.7:** `event_id` now always references the top-level event (festival or
> flat event). For sub-events, persons register at the festival level.
> Activation per sub-event is tracked via `event_person_activations` pivot
> and/or derived from shift assignments.
| Column | Type | Notes |
| ---------------- | ------------------ | ------------------------------------------------------------------------------ |
| `id` | ULID | PK |
| `user_id` | ULID FK nullable | → users — nullable: external guests/artists have no platform account |
| `event_id` | ULID FK | → events — **v1.7** always references top-level event (festival or flat event) |
| `crowd_type_id` | ULID FK | → crowd_types |
| `company_id` | ULID FK nullable | → companies |
| `name` | string | |
| `email` | string | Indexed deduplication key |
| `phone` | string nullable | |
| `status` | enum | `invited\|applied\|pending\|approved\|rejected\|no_show` |
| `is_blacklisted` | bool | |
| `admin_notes` | text nullable | |
| `custom_fields` | JSON | Event-specific fields — not queryable |
| `deleted_at` | timestamp nullable | Soft delete |
**Unique constraint:** `UNIQUE(event_id, user_id) WHERE user_id IS NOT NULL`
**Indexes:** `(event_id, crowd_type_id, status)`, `(email, event_id)`, `(user_id, event_id)`
@@ -610,6 +671,29 @@ $effectiveDate = $shift->end_date ?? $shift->timeSlot->date;
---
### `event_person_activations`
> **v1.7 New table.** Tracks which sub-events a person is active on,
> independent of shift assignments. Used for:
>
> - Suppliers/crew present at all sub-events without shifts
> - Festival-wide crew who need accreditation per day
> - Persons manually activated on specific sub-events by coordinator
>
> For volunteers: activation is derived from shift assignments (no manual entry needed).
> For fixed crew and suppliers: use this pivot for explicit activation.
| Column | Type | Notes |
| ----------- | ------- | --------------------------------- |
| `id` | int AI | PK — integer for join performance |
| `event_id` | ULID FK | → events (the sub-event) |
| `person_id` | ULID FK | → persons |
**Unique constraint:** `UNIQUE(event_id, person_id)`
**Indexes:** `(person_id)`, `(event_id)`
---
## 3.5.6 Accreditation Engine
### `accreditation_categories`
@@ -1236,10 +1320,42 @@ $effectiveDate = $shift->end_date ?? $shift->timeSlot->date;
- Every query on event data **MUST** scope on `organisation_id` via `OrganisationScope` Eloquent Global Scope
- Use Laravel policies — never direct id-checks in controllers
- **v1.7:** For festival queries, use `scopeWithChildren()` to include parent + all sub-events
- **Audit log:** Spatie `laravel-activitylog` on: `persons`, `accreditation_assignments`, `shift_assignments`, `check_ins`, `production_requests`
---
### Rule 8 — Festival/Event Model (v1.7)
```
Registration level → top-level event (festival or flat event)
Operational level → sub-event (child event)
Planning level → festival_section + shift
A person:
- Registers once at festival/top-level event
- Is active on 1 or more sub-events
- Has shifts within those sub-events
Determined by:
- Volunteer: via shift assignments (automatic)
- Fixed crew: via event_person_activations (manual)
- Supplier crew: via event_person_activations (manual)
- Artist: always linked to one sub-event
Flat event (no children):
- All modules at event level
- persons.event_id = the event itself
- No sub-event navigation shown in UI
Festival/series (has children):
- persons.event_id = the parent (festival) event
- festival_sections, shifts, artists = on child events
- UI shows festival overview + child event tabs
```
---
### Rule 6 — Shift Time Resolution
```php