diff --git a/.cursor/ARCHITECTURE.md b/.cursor/ARCHITECTURE.md deleted file mode 100644 index 82880018..00000000 --- a/.cursor/ARCHITECTURE.md +++ /dev/null @@ -1,436 +0,0 @@ -# Crewli - Architecture - -> Multi-tenant SaaS platform for event- and festival management. -> Source of truth: `/resources/design/design-document.md` - -## System Overview - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ INTERNET │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ┌───────────────┼───────────────┐ - │ │ - ▼ ▼ - ┌───────────────┐ ┌───────────────┐ - │ Organizer + │ │ Portal SPA │ - │ Admin SPA │ │ (External) │ - │ :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 two SPAs. - ---- - -## Applications - -### Organizer App (`apps/app/`) - -**Purpose**: Main application for event management per organisation. Also serves as the platform admin interface for `super_admin` users via `/platform/*` routes. - -**Users**: Organisation Admins, Event Managers, Staff Coordinators, Artist Managers, Volunteer Coordinators, Super Admins (platform management via `/platform/*`). - -**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 -- Platform admin: organisation management, billing, platform users (`/platform/*` routes, `super_admin` only) - -**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 - -Two frontend origins in `config/cors.php` (via env): - -| App | Dev URL | Env Variable | -|-----|---------|--------------| -| App | `http://localhost:5174` | `FRONTEND_APP_URL` | -| Portal | `http://localhost:5175` | `FRONTEND_PORTAL_URL` | - -Production (registered domain **crewli.app**): API `https://api.crewli.app` (`APP_URL`); SPAs `https://crewli.app`, `https://portal.crewli.app` via the same env keys. Frontends use `VITE_API_URL=https://api.crewli.app/api/v1`. `SANCTUM_STATEFUL_DOMAINS` = comma-separated SPA hostnames only (e.g. `crewli.app,portal.crewli.app`). **`crewli.nl`** is reserved for a future marketing site only — not used for this application stack. - ---- - -## Real-time Events (WebSocket) - -Via Laravel Echo + Pusher/Soketi: -- `PersonCheckedIn` -- `ShiftFillRateChanged` -- `ArtistCheckInStatusChanged` -- `AdvanceSectionSubmitted` -- `AccreditationItemHandedOut` -- `BriefingSendQueued` - ---- - -*Source: Crewli Design Document v1.3, March 2026* diff --git a/.cursor/instructions.md b/.cursor/instructions.md deleted file mode 100644 index b0a2ae71..00000000 --- a/.cursor/instructions.md +++ /dev/null @@ -1,222 +0,0 @@ -# Crewli - Cursor AI Instructions - -> Multi-tenant SaaS platform for event- and festival management. -> Design Document: `/resources/design/design-document.md` -> Dev Guide: `/resources/design/dev-guide.md` -> Start Guide: `/resources/design/start-guide.md` - -## Project Overview - -**Name**: Crewli -**Type**: Multi-tenant SaaS platform (API-first architecture) -**Status**: Development - -### Description - -Crewli is a multi-tenant SaaS platform for professional event and festival management. It supports the full operational cycle: artist booking and advancing, staff planning and volunteer management, accreditation, briefings, and real-time show-day operations (Mission Control). Built for a professional volunteer organisation, with SaaS expansion potential. - -## Quick Reference - -| Component | Technology | Location | Port | -|-----------|------------|----------|------| -| API | Laravel 12 + Sanctum + Spatie Permission | `api/` | 8000 | -| Organizer + Admin App (Main) | Vue 3 + Vuexy (full) | `apps/app/` | 5174 | -| Portal (External) | Vue 3 + Vuexy (stripped) | `apps/portal/` | 5175 | -| Database | MySQL 8 | Docker | 3306 | -| Cache / Queues | Redis | Docker | 6379 | -| Mail | Mailpit | Docker | 8025 | - -## Documentation Structure - -``` -.cursor/ -├── instructions.md # This file - overview and quick start -├── ARCHITECTURE.md # System architecture, schema, API routes -└── rules/ - ├── 001_workspace.mdc # Project structure, conventions, multi-tenancy - ├── 100_laravel.mdc # Laravel API patterns and templates - ├── 101_vue.mdc # Vue + Vuexy patterns and templates - └── 200_testing.mdc # Testing strategies and templates -``` - ---- - -## Core Modules - -### Phase 1 - Foundation -- [ ] Multi-tenant architecture + Auth (Sanctum + Spatie) -- [ ] Users, Roles & Permissions (three-level model) -- [ ] Organisations CRUD + User Invitations -- [ ] Events CRUD with lifecycle status -- [ ] Crowd Types (org-level configuration) -- [ ] Festival Sections + Time Slots + Shifts -- [ ] Persons & Crowd Lists -- [ ] Accreditation Engine (categories, items, access zones) - -### Phase 2 - Core Operations -- [ ] Briefings & Communication (template builder, queue-based sending) -- [ ] Staff & Crew Management (crowd pool, accreditation matrix) -- [ ] Volunteer Management + Portal (registration, shift claiming, approval flow) -- [ ] Form Builder (drag-drop, conditional logic, iframe embed) -- [ ] Artist Advancing + Portal (token-based access) -- [ ] Timetable & Stage management -- [ ] Show Day Mode -- [ ] Shift Swap & Waitlist -- [ ] Volunteer Profile + Festival Passport -- [ ] Communication Hub (email/SMS/WhatsApp via Zender, urgency levels) - -### Phase 3 - Advancing & Show Day -- [ ] Guests & Hospitality -- [ ] Suppliers & Production (production requests, supplier portal) -- [ ] Mission Control (real-time check-in, artist handling, scanner management) -- [ ] Communication Campaigns (email + SMS batch) -- [ ] Allocation Sheet PDF (Browsershot) -- [ ] Scan infrastructure (hardware pairing) -- [ ] Reporting & Insights -- [ ] No-show automation -- [ ] Post-festival evaluation + retrospective - -### Phase 4 - Differentiators -- [ ] Real-time WebSocket notifications (Echo + Pusher/Soketi) -- [ ] Cross-event crew pool with reliability score -- [ ] Global search (cmd+K) -- [ ] Crew PWA -- [ ] Public REST API + webhook system -- [ ] CO2/sustainability reporting - ---- - -## Module Development Order (per module) - -Always follow this sequence: - -1. Migration(s) - ULID PKs, composite indexes, constrained FKs -2. Eloquent Model - HasUlids, relations, scopes, OrganisationScope -3. Factory - realistic Dutch test data -4. Policy - authorization via Spatie roles -5. Form Request(s) - Store + Update validation -6. API Resource - computed fields, `whenLoaded()`, permission-dependent fields -7. Resource Controller - index/show/store/update/destroy -8. Routes in `api.php` -9. PHPUnit Feature Test - happy path (200/201) + unauthenticated (401) + wrong organisation (403) + validation (422) -10. Vue Composable (`useModuleName.ts`) - TanStack Query -11. Pinia Store (if cross-component state needed) -12. Vue Page Component -13. Vue Router entry - ---- - -## Getting Started Prompts - -### 1. Phase 1 Foundation (Backend) - -``` -Read CLAUDE.md. Then generate Phase 1 Foundation: - -1. Migrations: Update users (add timezone, locale, deleted_at). Create organisations (ULID, name, slug, billing_status, settings JSON, deleted_at), organisation_user pivot, user_invitations, events (ULID, organisation_id, name, slug, start_date, end_date, timezone, status enum, deleted_at), event_user_roles pivot. -2. Models: User (update), Organisation, UserInvitation, Event. All with HasUlids, SoftDeletes where applicable, OrganisationScope on Event. -3. Spatie Permission: RoleSeeder with roles: super_admin, org_admin, org_member, event_manager, staff_coordinator, volunteer_coordinator. -4. Auth: LoginController, LogoutController, MeController (returns user + organisations + active event roles). -5. Organisations: Controller, Policy, Request, Resource. -6. Events: Controller nested under organisations, Policy, Request, Resource. -7. Feature tests per step. Run php artisan test after each step. -``` - -### 2. Phase 1 Foundation (Frontend) - -``` -Build auth flow in apps/app/: -1. stores/useAuthStore.ts - token storage, isAuthenticated, me() loading -2. pages/login.vue - use Vuexy login layout -3. Router guard - redirect to login if not authenticated -4. Replace Vuexy demo navigation with Crewli structure -5. CASL permissions: connect to Spatie roles from auth/me response -``` - -### 3. Module Generation (example: Shifts) - -``` -Build the Shifts module following CLAUDE.md module order: -- Migration with ULID PK, festival_section_id, time_slot_id, location_id, slots_total, slots_open_for_claiming, status. Composite indexes. -- Model with HasUlids, SoftDeletes, relations, computed accessors (slots_filled, fill_rate). -- shift_assignments with denormalized time_slot_id, status machine (pending_approval > approved/rejected/cancelled/completed), UNIQUE(person_id, time_slot_id). -- Configurable auto-approve per shift. -- Queued notification jobs using ZenderService for WhatsApp. -- Feature tests covering 200/401/403/422. -``` - ---- - -## Common Tasks - -### Add a New API Endpoint - -1. Create/update Controller in `app/Http/Controllers/Api/V1/` -2. Create Form Request in `app/Http/Requests/Api/V1/` -3. Create/update API Resource in `app/Http/Resources/Api/V1/` -4. Add route in `routes/api.php` -5. Create Service class if complex business logic needed -6. Write PHPUnit Feature Test (200/401/403/422) - -### Add a New Vue Page - -1. Create page component in `src/pages/` -2. Route added automatically by file-based routing (or add to router) -3. Add navigation item in `src/navigation/` -4. Create composable for API calls in `src/composables/` -5. Use Vuexy/Vuetify components for UI - -### Add a New Database Table - -1. Create migration with ULID PK, composite indexes -2. Create model with HasUlids, relations, OrganisationScope (if applicable) -3. Create factory with realistic Dutch test data -4. Create Policy, Form Request, Resource, Controller -5. Register routes in `api.php` -6. Write PHPUnit Feature Test - ---- - -## Code Generation Preferences - -When generating code, always: - -- Use PHP 8.2+ features (typed properties, enums, match, readonly) -- Use `declare(strict_types=1);` -- Use ULID primary keys via HasUlids trait -- Use Spatie laravel-permission for roles (never hardcode role strings) -- Scope all queries on `organisation_id` via Global Scope -- Use `