Files
band-management/.cursor/ARCHITECTURE.md
bert.hausmans 1cb7674d52 refactor: align codebase with EventCrew domain and trim legacy band stack
- Update API: events, users, policies, routes, resources, migrations
- Remove deprecated models/resources (customers, setlists, invitations, etc.)
- Refresh admin app and docs; remove apps/band

Made-with: Cursor
2026-03-29 23:19:06 +02:00

450 lines
18 KiB
Markdown

# EventCrew - Architecture
> Multi-tenant SaaS platform for event- and festival management.
> Source of truth: `/resources/design/EventCrew_Design_Document_v1.3.docx`
## System Overview
```
┌─────────────────────────────────────────────────────────────────────────┐
│ INTERNET │
└─────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Admin SPA │ │ Organizer │ │ Portal SPA │
│ (Super Admin)│ │ SPA (Main) │ │ (External) │
│ :5173 │ │ :5174 │ │ :5175 │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└───────────────────────────┼───────────────────────────┘
│ CORS + Sanctum tokens
┌───────────────────────┐
│ Laravel 12 REST API │
│ (JSON only, no │
│ Blade views) │
│ :8000 │
└───────────┬───────────┘
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ MySQL 8 │ │ Redis │ │ Mailpit │
│ :3306 │ │ :6379 │ │ :8025 │
└───────────┘ └───────────┘ └───────────┘
```
**Golden Rule:** Laravel is exclusively a JSON REST API. No Blade views, no Mix, no Inertia. Every response is `application/json`. Vue handles ALL UI via three SPAs.
---
## Applications
### Admin Dashboard (`apps/admin/`)
**Purpose**: Super Admin platform management.
**Users**: Platform owner only (super_admin role).
**Features**:
- Organisation management (CRUD, billing status)
- Platform user management
- Global settings
**Vuexy Version**: `typescript-version/full-version`
---
### Organizer App (`apps/app/`)
**Purpose**: Main application for event management per organisation.
**Users**: Organisation Admins, Event Managers, Staff Coordinators, Artist Managers, Volunteer Coordinators.
**Features**:
- Event lifecycle management (Draft through Closed)
- Festival Sections, Time Slots, Shift planning
- Person & Crowd management (Crew, Volunteers, Artists, Guests, Press, Partners, Suppliers)
- Accreditation engine (categories, items, access zones)
- Artist booking & advancing
- Timetable & stage management
- Briefing builder & communication hub
- Mission Control (show day operations)
- Form builder with conditional logic
- Supplier & production management
- Reporting & insights
**Vuexy Version**: `typescript-version/full-version` (customized navigation)
---
### Portal (`apps/portal/`)
**Purpose**: External-facing portal with two access modes.
**Users**: Volunteers, Crew (login-based), Artists, Suppliers, Press (token-based).
**Access Modes**:
| User | Access Mode | Rationale |
|------|-------------|-----------|
| Volunteer / Crew | Login (`auth:sanctum`) | Long-term relationship, festival passport, shift history |
| Artist / Tour Manager | Token (`portal.token` middleware) | Per-event, advance portal via signed URL |
| Supplier / Partner | Token (`portal.token` middleware) | Per-event, production request via token |
| Press / Media | Token (`portal.token` middleware) | Per-event accreditation, no recurring relationship |
**Router guard logic**: If `route.query.token` -> token mode. If `authStore.isAuthenticated` -> login mode. Otherwise -> redirect to `/login`.
**Vuexy Version**: `typescript-version/starter-kit` (stripped: no sidebar, no customizer, no dark mode toggle; uses Vuetify components + Vuexy SCSS)
---
## Multi-Tenant Data Model
Shared database schema with organisation scoping on all tables. No row-level security at DB level; scoping enforced via Laravel Policies and Eloquent Global Scopes.
**Scoping Rule**: EVERY query on event data MUST have an `organisation_id` scope via `OrganisationScope` Global Scope.
**Tenancy Hierarchy**:
```
Platform (Super Admin)
└─ Organisation (client A)
└─ Event (event 1)
└─ Festival Section (Bar, Hospitality, Technical, ...)
├─ Time Slots (DAY1-EARLY-CREW, DAY1-EARLY-VOLUNTEER, ...)
└─ Shifts (Bar x DAY1-EARLY-VOLUNTEER, 5 slots)
```
---
## Three-Level Role & Permission Model
Managed via Spatie `laravel-permission` with team-based permissions.
| Level | Scope | Roles | Implementation |
|-------|-------|-------|----------------|
| App Level | Whole platform | `super_admin`, `support_agent` | Spatie role |
| Organisation Level | Within one org | `org_admin`, `org_member`, `org_readonly` | Spatie team = organisation |
| Event Level | Within one event | `event_manager`, `artist_manager`, `staff_coordinator`, `volunteer_coordinator`, `accreditation_officer` | `event_user_roles` pivot table |
**Middleware**: `OrganisationRoleMiddleware` and `EventRoleMiddleware` check per route.
---
## Event Lifecycle
| Phase | Description |
|-------|-------------|
| `draft` | Created but not published. Only admin sees it. |
| `published` | Active in planning. Internal modules available. External portals closed. |
| `registration_open` | Volunteer registration and artist advance portals open. |
| `buildup` | Setup days. Crew shifts begin. Accreditation distribution starts. |
| `showday` | Active event days. Mission Control active. Real-time check-in. |
| `teardown` | Breakdown days. Inventory return. Shift closure. |
| `closed` | Event completed. Read-only. Reports available. |
---
## API Structure
### Base URL
- Development: `http://localhost:8000/api/v1`
### Route Groups
```
# Public (no auth)
POST /auth/login
POST /portal/token-auth Token-based portal access
POST /portal/form-submit Public form submission
# Protected (auth:sanctum)
POST /auth/logout
GET /auth/me Returns user + organisations + event roles
# Organisations
GET/POST /organisations
GET/PUT /organisations/{id}
POST /organisations/{id}/invite
GET /organisations/{id}/members
# Events (nested under organisations)
GET/POST /organisations/{org}/events
GET/PUT/DELETE /events/{id}
PUT /events/{id}/status
# Festival Sections
GET/POST /events/{event}/sections
GET/PUT/DELETE /sections/{id}
GET /sections/{id}/dashboard
# Time Slots
GET/POST /events/{event}/time-slots
PUT/DELETE /time-slots/{id}
# Shifts
GET/POST /sections/{section}/shifts
PUT/DELETE /shifts/{id}
POST /shifts/{id}/assign
POST /shifts/{id}/claim Volunteer self-service
# Persons
GET/POST /events/{event}/persons
GET/PUT /persons/{id}
POST /persons/{id}/approve
POST /persons/{id}/checkin
# Crowd Types & Lists
GET/POST /organisations/{org}/crowd-types
GET/POST /events/{event}/crowd-lists
# Artists & Advancing
GET/POST /events/{event}/artists
GET/PUT /artists/{id}
GET/POST /artists/{id}/sections Advance sections
POST /sections/{id}/submit Advance submission
# Accreditation
GET/POST /events/{event}/accreditation-items
POST /persons/{id}/accreditations
GET/POST /events/{event}/access-zones
# Briefings & Communication
GET/POST /events/{event}/briefings
POST /briefings/{id}/send
GET/POST /events/{event}/campaigns
POST /campaigns/{id}/send
# Mission Control
GET /events/{event}/mission-control
POST /persons/{id}/checkin-item
# Scanners & Inventory
GET/POST /events/{event}/scanners
POST /scan
GET/POST /events/{event}/inventory
# Reports
GET /events/{event}/reports/{type}
# Portal (token-based, portal.token middleware)
GET /portal/artist
POST /portal/advancing
GET /portal/supplier
POST /portal/production-request
```
### API Response Format
```json
{
"data": { ... },
"meta": {
"pagination": {
"current_page": 1,
"per_page": 15,
"total": 100,
"last_page": 7
}
}
}
```
---
## Core Database Schema
**Primary Keys**: ULID on all business tables via `HasUlids` trait. Pure pivot tables use auto-increment integer PK.
**Soft Deletes ON**: organisations, events, festival_sections, shifts, shift_assignments, persons, artists, companies, production_requests.
**Soft Deletes OFF** (audit records): check_ins, briefing_sends, message_replies, shift_waitlist, volunteer_festival_history.
**JSON Columns**: ONLY for opaque config (blocks, fields, settings, items). NEVER for dates, status values, foreign keys, booleans, or anything filtered/sorted/aggregated.
### 1. Foundation
| Table | Key Columns | Notes |
|-------|-------------|-------|
| `users` | id (ULID), name, email, password, timezone, locale, avatar, deleted_at | Platform-wide, unique per email. |
| `organisations` | id (ULID), name, slug, billing_status, settings (JSON: display prefs only), deleted_at | hasMany events, crowd_types. |
| `organisation_user` | id (int AI), user_id, organisation_id, role | Pivot. Integer PK. |
| `user_invitations` | id (ULID), email, invited_by_user_id, organisation_id, event_id (nullable), role, token (ULID unique), status, expires_at | INDEX: (token), (email, status). |
| `events` | id (ULID), organisation_id, name, slug, start_date, end_date, timezone, status (enum), deleted_at | INDEX: (organisation_id, status). |
| `event_user_roles` | id (int AI), user_id, event_id, role | Pivot. Integer PK. |
### 2. Locations
| Table | Key Columns | Notes |
|-------|-------------|-------|
| `locations` | id (ULID), event_id, name, address, lat, lng, description, access_instructions | INDEX: (event_id). |
### 3. Festival Sections, Time Slots & Shifts
| Table | Key Columns | Notes |
|-------|-------------|-------|
| `festival_sections` | id (ULID), event_id, name, sort_order, deleted_at | INDEX: (event_id, sort_order). |
| `time_slots` | id (ULID), event_id, name, person_type (CREW/VOLUNTEER/PRESS/...), date, start_time, end_time | INDEX: (event_id, person_type, date). |
| `shifts` | id (ULID), festival_section_id, time_slot_id, location_id, slots_total, slots_open_for_claiming, status, deleted_at | INDEX: (festival_section_id, time_slot_id). |
| `shift_assignments` | id (ULID), shift_id, person_id, time_slot_id (denormalized), status (pending_approval/approved/rejected/cancelled/completed), auto_approved, deleted_at | UNIQUE(person_id, time_slot_id). |
| `volunteer_availabilities` | id (ULID), person_id, time_slot_id, submitted_at | UNIQUE(person_id, time_slot_id). |
| `shift_waitlist` | id (ULID), shift_id, person_id, position, added_at | UNIQUE(shift_id, person_id). |
| `shift_swap_requests` | id (ULID), from_assignment_id, to_person_id, status, auto_approved | |
| `shift_absences` | id (ULID), shift_assignment_id, person_id, reason, status | |
### 4. Volunteer Profile & History
| Table | Key Columns | Notes |
|-------|-------------|-------|
| `volunteer_profiles` | id (ULID), user_id (unique), bio, tshirt_size, first_aid, driving_licence, reliability_score (0.00-5.00) | Platform-wide, 1:1 with users. |
| `volunteer_festival_history` | id (ULID), user_id, event_id, hours_planned, hours_completed, no_show_count, coordinator_rating, would_reinvite | UNIQUE(user_id, event_id). Never visible to volunteer. |
| `post_festival_evaluations` | id (ULID), event_id, person_id, overall_rating, would_return, feedback_text | |
| `festival_retrospectives` | id (ULID), event_id (unique), KPI columns, top_feedback (JSON) | |
### 5. Crowd Types, Persons & Crowd Lists
| Table | Key Columns | Notes |
|-------|-------------|-------|
| `crowd_types` | id (ULID), organisation_id, name, system_type (CREW/GUEST/ARTIST/VOLUNTEER/PRESS/PARTNER/SUPPLIER), color, icon | Org-level config. |
| `persons` | id (ULID), user_id (nullable), event_id, crowd_type_id, company_id (nullable), name, email, phone, status, is_blacklisted, custom_fields (JSON), deleted_at | user_id nullable for externals. UNIQUE(event_id, user_id) WHERE user_id IS NOT NULL. |
| `companies` | id (ULID), organisation_id, name, type, contact_*, deleted_at | Shared across events within org. |
| `crowd_lists` | id (ULID), event_id, crowd_type_id, name, type (internal/external), auto_approve, max_persons | |
| `crowd_list_persons` | id (int AI), crowd_list_id, person_id | Pivot. |
### 6. Accreditation Engine
| Table | Key Columns | Notes |
|-------|-------------|-------|
| `accreditation_categories` | id (ULID), organisation_id, name, sort_order, icon | Org-level. |
| `accreditation_items` | id (ULID), accreditation_category_id, name, is_date_dependent, barcode_type, cost_price | Org-level items. |
| `event_accreditation_items` | id (ULID), event_id, accreditation_item_id, max_quantity_per_person, is_active | Activates item per event. UNIQUE(event_id, accreditation_item_id). |
| `accreditation_assignments` | id (ULID), person_id, accreditation_item_id, event_id, date, quantity, is_handed_out | |
| `access_zones` | id (ULID), event_id, name, zone_code (unique per event) | |
| `access_zone_days` | id (int AI), access_zone_id, day_date | UNIQUE(access_zone_id, day_date). |
| `person_access_zones` | id (int AI), person_id, access_zone_id, valid_from, valid_to | |
### 7. Artists & Advancing
| Table | Key Columns | Notes |
|-------|-------------|-------|
| `artists` | id (ULID), event_id, name, booking_status (concept/requested/option/confirmed/contracted/cancelled), portal_token (ULID unique), deleted_at | |
| `performances` | id (ULID), artist_id, stage_id, date, start_time, end_time, check_in_status | INDEX: (stage_id, date, start_time). |
| `stages` | id (ULID), event_id, name, color, capacity | |
| `stage_days` | id (int AI), stage_id, day_date | UNIQUE(stage_id, day_date). |
| `advance_sections` | id (ULID), artist_id, name, type, is_open, sort_order | |
| `advance_submissions` | id (ULID), advance_section_id, data (JSON), status | |
| `artist_contacts` | id (ULID), artist_id, name, email, role | |
| `artist_riders` | id (ULID), artist_id, category (technical/hospitality), items (JSON) | |
### 8. Communication & Briefings
| Table | Key Columns | Notes |
|-------|-------------|-------|
| `briefing_templates` | id (ULID), event_id, name, type, blocks (JSON) | |
| `briefings` | id (ULID), event_id, briefing_template_id, name, target_crowd_types (JSON), status | |
| `briefing_sends` | id (ULID), briefing_id, person_id, status (queued/sent/opened/downloaded) | NO soft delete. |
| `communication_campaigns` | id (ULID), event_id, type (email/sms/whatsapp), status | |
| `messages` | id (ULID), event_id, sender_user_id, recipient_person_id, urgency (normal/urgent/emergency) | |
| `broadcast_messages` | id (ULID), event_id, sender_user_id, body, urgency | |
### 9. Forms, Check-In & Operational
| Table | Key Columns | Notes |
|-------|-------------|-------|
| `public_forms` | id (ULID), event_id, crowd_type_id, fields (JSON), conditional_logic (JSON), iframe_token | |
| `form_submissions` | id (ULID), public_form_id, person_id, data (JSON) | |
| `check_ins` | id (ULID), event_id, person_id, scanned_by_user_id, scanned_at | NO soft delete. Immutable audit record. |
| `scanners` | id (ULID), event_id, name, type, pairing_code | |
| `inventory_items` | id (ULID), event_id, name, item_code, assigned_to_person_id | |
| `production_requests` | id (ULID), event_id, company_id, title, status, token (ULID unique) | |
| `material_requests` | id (ULID), production_request_id, category, name, quantity, status | |
---
## Model Relationships
**User**
- belongsToMany Organisations (via `organisation_user`)
- belongsToMany Events (via `event_user_roles`)
**Organisation**
- hasMany Events
- hasMany CrowdTypes
- hasMany AccreditationCategories
- hasMany Companies
- belongsToMany Users (via `organisation_user`)
**Event**
- belongsTo Organisation
- hasMany FestivalSections
- hasMany TimeSlots
- hasMany Persons
- hasMany Artists
- hasMany Briefings
- hasMany Locations
- hasMany AccessZones
- hasMany PublicForms
**FestivalSection**
- belongsTo Event
- hasMany Shifts
**TimeSlot**
- belongsTo Event
- hasMany Shifts
- hasMany ShiftAssignments (denormalized)
**Shift**
- belongsTo FestivalSection
- belongsTo TimeSlot
- belongsTo Location (nullable)
- hasMany ShiftAssignments
**Person**
- belongsTo Event
- belongsTo CrowdType
- belongsTo User (nullable)
- belongsTo Company (nullable)
- hasMany ShiftAssignments
- hasMany AccreditationAssignments
- hasMany CheckIns
**Artist**
- belongsTo Event
- hasMany Performances
- hasMany AdvanceSections
- hasMany ArtistContacts
---
## Security & CORS
Three frontend origins in `config/cors.php` (via env):
| App | Dev URL | Env Variable |
|-----|---------|--------------|
| Admin | `http://localhost:5173` | `FRONTEND_ADMIN_URL` |
| App | `http://localhost:5174` | `FRONTEND_APP_URL` |
| Portal | `http://localhost:5175` | `FRONTEND_PORTAL_URL` |
---
## Real-time Events (WebSocket)
Via Laravel Echo + Pusher/Soketi:
- `PersonCheckedIn`
- `ShiftFillRateChanged`
- `ArtistCheckInStatusChanged`
- `AdvanceSectionSubmitted`
- `AccreditationItemHandedOut`
- `BriefingSendQueued`
---
*Source: EventCrew Design Document v1.3, March 2026*