WS-3 PR-C: doc-state reckoning + apps/portal sweep #7
@@ -12,18 +12,14 @@ if [[ "$path" = /* ]]; then
|
||||
rel="${path#$CLAUDE_PROJECT_DIR/}"
|
||||
fi
|
||||
|
||||
# Match apps/app/** or apps/portal/** for .vue/.ts/.tsx/.js files.
|
||||
if ! echo "$rel" | grep -Eq '^apps/(app|portal)/.+\.(vue|ts|tsx|js)$'; then
|
||||
# Match apps/app/** for .vue/.ts/.tsx/.js files.
|
||||
if ! echo "$rel" | grep -Eq '^apps/app/.+\.(vue|ts|tsx|js)$'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract SPA dir and the path relative to it.
|
||||
spa="$(echo "$rel" | grep -oE '^apps/(app|portal)')"
|
||||
inside="${rel#$spa/}"
|
||||
# Path inside apps/app/.
|
||||
inside="${rel#apps/app/}"
|
||||
|
||||
# SPA may not exist yet (apps/portal/ is planned but not present in tree).
|
||||
[ -d "$CLAUDE_PROJECT_DIR/$spa" ] || exit 0
|
||||
|
||||
cd "$CLAUDE_PROJECT_DIR/$spa" 2>/dev/null || exit 0
|
||||
cd "$CLAUDE_PROJECT_DIR/apps/app" 2>/dev/null || exit 0
|
||||
pnpm eslint --fix "$inside" >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
exit 0
|
||||
@@ -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*
|
||||
@@ -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 `<script setup lang="ts">` for Vue components
|
||||
- Use TanStack Query for all API calls
|
||||
- Use VeeValidate + Zod for form validation
|
||||
- Use Vuetify/Vuexy components for UI (never custom CSS if Vuetify class exists)
|
||||
|
||||
---
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### Docker Services
|
||||
```bash
|
||||
make services # Start MySQL, Redis, Mailpit
|
||||
make services-stop # Stop services
|
||||
```
|
||||
|
||||
### Development Servers
|
||||
```bash
|
||||
make api # Laravel on :8000
|
||||
make app # Organizer + Admin SPA on :5174
|
||||
make portal # Portal SPA on :5175
|
||||
```
|
||||
|
||||
### Database
|
||||
```bash
|
||||
make migrate # Run migrations
|
||||
make fresh # Fresh migrate + seed
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
cd api && php artisan test # All tests
|
||||
cd api && php artisan test --filter=ShiftTest # Specific test
|
||||
cd api && php artisan test --coverage # With coverage
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: Vue 3, TypeScript, and Vuexy patterns for Crewli platform
|
||||
globs: ["apps/**/*.{vue,ts,tsx}"]
|
||||
description: Vue 3, TypeScript, and Vuexy patterns for Crewli
|
||||
globs: ["apps/app/**/*.{vue,ts,tsx}"]
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
@@ -8,675 +8,78 @@ alwaysApply: true
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Composition API only** - Always `<script setup lang="ts">`
|
||||
2. **TypeScript strict mode** - No `any` types
|
||||
3. **TanStack Query for API** - Never raw axios in components
|
||||
4. **Pinia for client state** - Server data stays in TanStack Query
|
||||
5. **Vuexy/Vuetify components** - Never custom CSS if a Vuetify class exists
|
||||
6. **VeeValidate + Zod** - For all form validation
|
||||
7. **Mobile-first** - Minimum 375px width
|
||||
1. **Composition API only** — always `<script setup lang="ts">`, never Options API
|
||||
2. **No `any` types** — use proper typing or `unknown` + narrowing
|
||||
3. **TanStack Query for API** — never raw axios in components
|
||||
4. **Pinia for cross-component client state** — server data lives in TanStack Query, never duplicated in stores
|
||||
5. **Vuetify components first** — custom CSS only when no Vuetify class fits the use case
|
||||
6. **VeeValidate + Zod** for all form validation
|
||||
7. **Mobile-first** — minimum 375px width, responsive at every breakpoint
|
||||
|
||||
## App-Specific Rules
|
||||
## File structure
|
||||
|
||||
### `apps/app/` (Organizer + Platform Admin - Main App)
|
||||
- Sidebar nav customized for Crewli structure
|
||||
- Remove Vuexy demo/customizer components
|
||||
- Full Vuetify component usage
|
||||
- 90% of development work happens here
|
||||
- Super admin functionality under `/platform/*` routes for `super_admin` users
|
||||
|
||||
### `apps/portal/` (External Portal)
|
||||
- Stripped Vuexy: no sidebar, no customizer, no dark mode toggle
|
||||
- Custom layout: top-bar with event logo + name
|
||||
- Uses Vuetify components + Vuexy SCSS variables
|
||||
- Two access modes: login (volunteers) and token (artists/suppliers)
|
||||
- Mobile-first design
|
||||
|
||||
## Vuexy Folder Rules
|
||||
|
||||
### Never Modify
|
||||
```
|
||||
src/@core/ # Vuexy core
|
||||
src/@layouts/ # Vuexy layouts
|
||||
apps/app/src/
|
||||
├── lib/axios.ts # Single axios instance (do not duplicate)
|
||||
├── composables/api/use*.ts # TanStack Query composables (one per resource)
|
||||
├── stores/use*Store.ts # Pinia stores — client state only
|
||||
├── types/*.ts # TypeScript interfaces (mirror backend Resources)
|
||||
├── pages/ # File-based routing via unplugin-vue-router
|
||||
├── layouts/ # Layout components
|
||||
├── components/ # Reusable components
|
||||
└── @core/ # Vuexy core — DO NOT MODIFY
|
||||
```
|
||||
|
||||
### Customize
|
||||
```
|
||||
src/
|
||||
├── components/ # Custom components
|
||||
├── composables/ # useModule.ts composables (TanStack Query)
|
||||
├── layouts/ # App layout customizations
|
||||
├── lib/ # axios.ts (SINGLE axios instance per app)
|
||||
├── navigation/ # Sidebar menu items
|
||||
├── pages/ # Page components
|
||||
├── plugins/ # vue-query, casl, vuetify
|
||||
├── stores/ # Pinia stores (client state only)
|
||||
└── types/ # TypeScript interfaces
|
||||
```
|
||||
## Reference patterns (read these for templates)
|
||||
|
||||
## File Templates
|
||||
For working examples in the actual codebase:
|
||||
|
||||
### Axios Instance (ONE per app)
|
||||
- **Composable pattern:** `apps/app/src/composables/api/useEvents.ts`
|
||||
- **Pinia store pattern:** `apps/app/src/stores/useAuthStore.ts`
|
||||
- **Page pattern:** `apps/app/src/pages/events/index.vue`
|
||||
- **Form pattern:** `apps/app/src/components/events/CreateEventDialog.vue`
|
||||
- **Layout pattern:** `apps/app/src/layouts/OrganizerLayout.vue`
|
||||
|
||||
```typescript
|
||||
// src/lib/axios.ts
|
||||
import axios from 'axios'
|
||||
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
For Vuexy component selection, consult `dev-docs/VUEXY_COMPONENTS.md` — the registry of @core wrappers and patterns. Always check that registry before writing a custom component.
|
||||
|
||||
const api: AxiosInstance = axios.create({
|
||||
baseURL: `${import.meta.env.VITE_API_URL}/api/v1`,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
timeout: 30000,
|
||||
})
|
||||
For auth and routing, see `dev-docs/AUTH_ARCHITECTURE.md` (httpOnly cookies, dual-axios for portal-token routes, route guard logic).
|
||||
|
||||
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.token) {
|
||||
config.headers.Authorization = `Bearer ${authStore.token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
## Strict rules
|
||||
|
||||
api.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error.response?.status === 401) {
|
||||
const authStore = useAuthStore()
|
||||
authStore.logout()
|
||||
}
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
### TypeScript
|
||||
- Use `import type { ... }` for type-only imports
|
||||
- Mirror backend PHP Enums as const objects with `as const` in `apps/app/src/types/`
|
||||
- Generic API response shape: `{ data: T, meta?: PaginationMeta }`
|
||||
|
||||
export { api }
|
||||
```
|
||||
### Architecture
|
||||
- Components never import axios directly — always via composables
|
||||
- Composables call axios via the singleton in `apps/app/src/lib/axios.ts`
|
||||
- Mutations invalidate query keys after success
|
||||
- No prop drilling — use Pinia stores when state crosses two component boundaries
|
||||
|
||||
### TypeScript Types
|
||||
### UI
|
||||
- Three states for every list view: **loading** (VSkeletonLoader), **error** (VAlert with retry button), **empty** (helpful message explaining what action to take)
|
||||
- Custom CSS forbidden when a Vuetify utility class exists
|
||||
- Tables on mobile (<768px) collapse to VList or card view — never horizontal scroll without a visual indicator
|
||||
|
||||
```typescript
|
||||
// src/types/events.ts
|
||||
### Forms
|
||||
- Zod schema mirrors backend FormRequest validation
|
||||
- Errors shown inline via VeeValidate's `errors` object
|
||||
- Submit button disabled while `isPending`
|
||||
|
||||
export type EventStatus = 'draft' | 'published' | 'registration_open' | 'buildup' | 'showday' | 'teardown' | 'closed'
|
||||
export type PersonStatus = 'invited' | 'applied' | 'pending' | 'approved' | 'rejected' | 'no_show'
|
||||
export type BookingStatus = 'concept' | 'requested' | 'option' | 'confirmed' | 'contracted' | 'cancelled'
|
||||
export type ShiftAssignmentStatus = 'pending_approval' | 'approved' | 'rejected' | 'cancelled' | 'completed'
|
||||
export type CrowdSystemType = 'CREW' | 'GUEST' | 'ARTIST' | 'VOLUNTEER' | 'PRESS' | 'PARTNER' | 'SUPPLIER'
|
||||
### Routing
|
||||
- File-based routing via unplugin-vue-router
|
||||
- Guards in `apps/app/src/plugins/1.router/guards.ts`
|
||||
- Portal routes are at `/portal/*` (within apps/app), NOT a separate SPA
|
||||
- Platform admin routes are at `/platform/*`, gated by `super_admin` role
|
||||
|
||||
export interface Organisation {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
billing_status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
## Avoid
|
||||
|
||||
export interface Event {
|
||||
id: string
|
||||
organisation_id: string
|
||||
name: string
|
||||
slug: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
timezone: string
|
||||
status: EventStatus
|
||||
status_label: string
|
||||
status_color: string
|
||||
festival_sections?: FestivalSection[]
|
||||
persons_count?: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface FestivalSection {
|
||||
id: string
|
||||
event_id: string
|
||||
name: string
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
export interface TimeSlot {
|
||||
id: string
|
||||
event_id: string
|
||||
name: string
|
||||
person_type: CrowdSystemType
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
}
|
||||
|
||||
export interface Shift {
|
||||
id: string
|
||||
festival_section_id: string
|
||||
time_slot_id: string
|
||||
location_id: string | null
|
||||
slots_total: number
|
||||
slots_open_for_claiming: number
|
||||
slots_filled: number
|
||||
fill_rate: number
|
||||
status: string
|
||||
festival_section?: FestivalSection
|
||||
time_slot?: TimeSlot
|
||||
assignments?: ShiftAssignment[]
|
||||
}
|
||||
|
||||
export interface ShiftAssignment {
|
||||
id: string
|
||||
shift_id: string
|
||||
person_id: string
|
||||
time_slot_id: string
|
||||
status: ShiftAssignmentStatus
|
||||
auto_approved: boolean
|
||||
person?: Person
|
||||
}
|
||||
|
||||
export interface Person {
|
||||
id: string
|
||||
event_id: string
|
||||
crowd_type_id: string
|
||||
user_id: string | null
|
||||
name: string
|
||||
email: string
|
||||
phone: string | null
|
||||
status: PersonStatus
|
||||
is_blacklisted: boolean
|
||||
crowd_type?: CrowdType
|
||||
}
|
||||
|
||||
export interface CrowdType {
|
||||
id: string
|
||||
organisation_id: string
|
||||
name: string
|
||||
system_type: CrowdSystemType
|
||||
color: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export interface Artist {
|
||||
id: string
|
||||
event_id: string
|
||||
name: string
|
||||
booking_status: BookingStatus
|
||||
star_rating: number
|
||||
}
|
||||
|
||||
// API response types
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[]
|
||||
meta: {
|
||||
current_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
last_page: number
|
||||
}
|
||||
}
|
||||
|
||||
// Form types
|
||||
export interface CreateEventData {
|
||||
organisation_id: string
|
||||
name: string
|
||||
slug: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
timezone?: string
|
||||
status?: EventStatus
|
||||
}
|
||||
|
||||
export interface UpdateEventData extends Partial<CreateEventData> {}
|
||||
```
|
||||
|
||||
### TanStack Query Setup
|
||||
|
||||
```typescript
|
||||
// src/plugins/vue-query.ts
|
||||
import type { VueQueryPluginOptions } from '@tanstack/vue-query'
|
||||
import { QueryClient } from '@tanstack/vue-query'
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const vueQueryPluginOptions: VueQueryPluginOptions = {
|
||||
queryClient,
|
||||
}
|
||||
```
|
||||
|
||||
### Composable (useEvents)
|
||||
|
||||
```typescript
|
||||
// src/composables/useEvents.ts
|
||||
import { computed } from 'vue'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { api } from '@/lib/axios'
|
||||
import type { Event, CreateEventData, UpdateEventData, PaginatedResponse } from '@/types/events'
|
||||
|
||||
export function useEventList(organisationId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['organisations', organisationId, 'events'],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<PaginatedResponse<Event>>(
|
||||
`/organisations/${organisationId}/events`
|
||||
)
|
||||
return data
|
||||
},
|
||||
enabled: !!organisationId,
|
||||
})
|
||||
}
|
||||
|
||||
export function useEventDetail(eventId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['events', eventId],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<{ data: Event }>(`/events/${eventId}`)
|
||||
return data.data
|
||||
},
|
||||
enabled: !!eventId,
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateEvent() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payload: { organisationId: string; data: CreateEventData }) => {
|
||||
const { data } = await api.post<{ data: Event }>(
|
||||
`/organisations/${payload.organisationId}/events`,
|
||||
payload.data,
|
||||
)
|
||||
return data.data
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['organisations', variables.organisationId, 'events'],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateEvent() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payload: { eventId: string; data: UpdateEventData }) => {
|
||||
const { data } = await api.put<{ data: Event }>(
|
||||
`/events/${payload.eventId}`,
|
||||
payload.data,
|
||||
)
|
||||
return data.data
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['events', data.id] })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['organisations', data.organisation_id, 'events'],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteEvent() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (eventId: string) => {
|
||||
await api.delete(`/events/${eventId}`)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['events'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Pinia Store (Auth)
|
||||
|
||||
```typescript
|
||||
// src/stores/useAuthStore.ts
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { api } from '@/lib/axios'
|
||||
import type { Organisation } from '@/types/events'
|
||||
|
||||
interface AuthUser {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
timezone: string
|
||||
locale: string
|
||||
organisations: Organisation[]
|
||||
event_roles: Array<{ event_id: string; role: string }>
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<AuthUser | null>(null)
|
||||
const token = ref<string | null>(localStorage.getItem('auth_token'))
|
||||
const currentOrganisationId = ref<string | null>(localStorage.getItem('current_organisation_id'))
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
const currentOrganisation = computed(() =>
|
||||
user.value?.organisations.find(o => o.id === currentOrganisationId.value) ?? null
|
||||
)
|
||||
|
||||
async function login(email: string, password: string): Promise<boolean> {
|
||||
try {
|
||||
const { data } = await api.post('/auth/login', { email, password })
|
||||
user.value = data.data.user
|
||||
token.value = data.data.token
|
||||
localStorage.setItem('auth_token', data.data.token)
|
||||
|
||||
if (data.data.user.organisations.length > 0) {
|
||||
setCurrentOrganisation(data.data.user.organisations[0].id)
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchMe(): Promise<boolean> {
|
||||
if (!token.value) return false
|
||||
try {
|
||||
const { data } = await api.get('/auth/me')
|
||||
user.value = data.data
|
||||
return true
|
||||
} catch {
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentOrganisation(orgId: string) {
|
||||
currentOrganisationId.value = orgId
|
||||
localStorage.setItem('current_organisation_id', orgId)
|
||||
}
|
||||
|
||||
function logout() {
|
||||
user.value = null
|
||||
token.value = null
|
||||
currentOrganisationId.value = null
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('current_organisation_id')
|
||||
}
|
||||
|
||||
return {
|
||||
user, token, currentOrganisationId,
|
||||
isAuthenticated, currentOrganisation,
|
||||
login, fetchMe, setCurrentOrganisation, logout,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Page Component (Event List)
|
||||
|
||||
```vue
|
||||
<!-- src/pages/events/index.vue -->
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { useEventList } from '@/composables/useEvents'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const organisationId = computed(() => authStore.currentOrganisationId ?? '')
|
||||
const { data, isLoading, isError, error } = useEventList(organisationId.value)
|
||||
|
||||
const events = computed(() => data.value?.data ?? [])
|
||||
|
||||
function getStatusColor(status: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
draft: 'secondary',
|
||||
published: 'info',
|
||||
registration_open: 'primary',
|
||||
buildup: 'warning',
|
||||
showday: 'success',
|
||||
teardown: 'warning',
|
||||
closed: 'secondary',
|
||||
}
|
||||
return colors[status] ?? 'secondary'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-space-between align-center mb-6">
|
||||
<div>
|
||||
<h4 class="text-h4 mb-1">Events</h4>
|
||||
<p class="text-body-1 text-medium-emphasis">
|
||||
Manage events for your organisation
|
||||
</p>
|
||||
</div>
|
||||
<VBtn
|
||||
color="primary"
|
||||
prepend-icon="tabler-plus"
|
||||
:to="{ name: 'events-create' }"
|
||||
>
|
||||
Create Event
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<VCard v-if="isLoading">
|
||||
<VCardText class="text-center py-8">
|
||||
<VProgressCircular indeterminate color="primary" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Error State -->
|
||||
<VAlert v-else-if="isError" type="error" class="mb-4">
|
||||
{{ error?.message ?? 'Failed to load events' }}
|
||||
</VAlert>
|
||||
|
||||
<!-- Events Table -->
|
||||
<VCard v-else>
|
||||
<VDataTable
|
||||
:items="events"
|
||||
:headers="[
|
||||
{ title: 'Name', key: 'name' },
|
||||
{ title: 'Dates', key: 'start_date' },
|
||||
{ title: 'Status', key: 'status' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false },
|
||||
]"
|
||||
>
|
||||
<template #item.name="{ item }">
|
||||
<RouterLink :to="{ name: 'events-show', params: { id: item.id } }">
|
||||
{{ item.name }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<template #item.start_date="{ item }">
|
||||
{{ item.start_date }} - {{ item.end_date }}
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<VChip :color="getStatusColor(item.status)" size="small">
|
||||
{{ item.status_label }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<VBtn
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
:to="{ name: 'events-edit', params: { id: item.id } }"
|
||||
>
|
||||
<VIcon icon="tabler-edit" />
|
||||
</VBtn>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Navigation Menu (Organizer App)
|
||||
|
||||
```typescript
|
||||
// src/navigation/vertical/index.ts
|
||||
import type { VerticalNavItems } from '@/@layouts/types'
|
||||
|
||||
export default [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
to: { name: 'dashboard' },
|
||||
icon: { icon: 'tabler-smart-home' },
|
||||
},
|
||||
{ heading: 'Event Management' },
|
||||
{
|
||||
title: 'Events',
|
||||
to: { name: 'events' },
|
||||
icon: { icon: 'tabler-calendar-event' },
|
||||
},
|
||||
{
|
||||
title: 'Festival Sections',
|
||||
to: { name: 'festival-sections' },
|
||||
icon: { icon: 'tabler-layout-grid' },
|
||||
},
|
||||
{
|
||||
title: 'Time Slots & Shifts',
|
||||
to: { name: 'shifts' },
|
||||
icon: { icon: 'tabler-clock' },
|
||||
},
|
||||
{ heading: 'People' },
|
||||
{
|
||||
title: 'Persons',
|
||||
to: { name: 'persons' },
|
||||
icon: { icon: 'tabler-users' },
|
||||
},
|
||||
{
|
||||
title: 'Artists',
|
||||
to: { name: 'artists' },
|
||||
icon: { icon: 'tabler-music' },
|
||||
},
|
||||
{
|
||||
title: 'Volunteers',
|
||||
to: { name: 'volunteers' },
|
||||
icon: { icon: 'tabler-heart-handshake' },
|
||||
},
|
||||
{ heading: 'Operations' },
|
||||
{
|
||||
title: 'Accreditation',
|
||||
to: { name: 'accreditation' },
|
||||
icon: { icon: 'tabler-id-badge-2' },
|
||||
},
|
||||
{
|
||||
title: 'Briefings',
|
||||
to: { name: 'briefings' },
|
||||
icon: { icon: 'tabler-mail' },
|
||||
},
|
||||
{
|
||||
title: 'Mission Control',
|
||||
to: { name: 'mission-control' },
|
||||
icon: { icon: 'tabler-broadcast' },
|
||||
},
|
||||
{ heading: 'Insights' },
|
||||
{
|
||||
title: 'Reports',
|
||||
to: { name: 'reports' },
|
||||
icon: { icon: 'tabler-chart-bar' },
|
||||
},
|
||||
] as VerticalNavItems
|
||||
```
|
||||
|
||||
## Forms with VeeValidate + Zod
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { z } from 'zod'
|
||||
import { useCreateEvent } from '@/composables/useEvents'
|
||||
|
||||
const schema = toTypedSchema(
|
||||
z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
slug: z.string().min(1, 'Slug is required'),
|
||||
start_date: z.string().min(1, 'Start date is required'),
|
||||
end_date: z.string().min(1, 'End date is required'),
|
||||
timezone: z.string().default('Europe/Amsterdam'),
|
||||
})
|
||||
)
|
||||
|
||||
const { handleSubmit, errors, defineField } = useForm({ validationSchema: schema })
|
||||
const [name, nameAttrs] = defineField('name')
|
||||
const [startDate, startDateAttrs] = defineField('start_date')
|
||||
|
||||
const { mutate: createEvent, isPending } = useCreateEvent()
|
||||
|
||||
const onSubmit = handleSubmit(values => {
|
||||
createEvent({
|
||||
organisationId: authStore.currentOrganisationId!,
|
||||
data: values as CreateEventData,
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit="onSubmit">
|
||||
<VTextField
|
||||
v-model="name"
|
||||
v-bind="nameAttrs"
|
||||
label="Event Name"
|
||||
:error-messages="errors.name"
|
||||
/>
|
||||
<VTextField
|
||||
v-model="startDate"
|
||||
v-bind="startDateAttrs"
|
||||
label="Start Date"
|
||||
type="date"
|
||||
:error-messages="errors.start_date"
|
||||
/>
|
||||
<VBtn type="submit" color="primary" :loading="isPending">
|
||||
Create Event
|
||||
</VBtn>
|
||||
</form>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Always Use
|
||||
- `<script setup lang="ts">` for components
|
||||
- Props: `defineProps<{...}>()`
|
||||
- Emits: `defineEmits<{...}>()`
|
||||
- TanStack Query for all API calls via composables
|
||||
- Computed properties for derived state
|
||||
- Vuexy/Vuetify components (VBtn, VCard, VDataTable, VDialog, etc.)
|
||||
- `import type { ... }` for type-only imports
|
||||
- Status KPI tiles as clickable VCards on list pages
|
||||
- VSkeleton loader during loading
|
||||
- VAlert with retry on errors
|
||||
- Mobile: table collapses to VList below 768px
|
||||
|
||||
### Avoid
|
||||
- Options API
|
||||
- Options API (`export default { ... }`)
|
||||
- `any` types
|
||||
- Raw axios calls in components (use composables)
|
||||
- Inline styles (use Vuetify utility classes)
|
||||
- Raw axios calls in components
|
||||
- Inline styles
|
||||
- Direct DOM manipulation
|
||||
- Mutating props
|
||||
- Prop drilling (use Pinia stores)
|
||||
- Custom CSS when Vuetify class exists
|
||||
|
||||
## Portal Router Guards
|
||||
|
||||
```typescript
|
||||
// apps/portal/src/router/guards.ts
|
||||
export function determineAccessMode(route: RouteLocationNormalized): 'token' | 'login' | 'unauthenticated' {
|
||||
if (route.query.token) return 'token'
|
||||
if (authStore.isAuthenticated) return 'login'
|
||||
return 'unauthenticated'
|
||||
}
|
||||
|
||||
// Token-based: POST /api/v1/portal/token-auth { token: '...' } -> returns person context
|
||||
// Login-based: Same /api/v1/auth/login as app/
|
||||
```
|
||||
- Custom CSS when a Vuetify class exists
|
||||
- Hardcoded URLs or string-literal status values
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: Multi-tenancy and portal architecture rules for Crewli
|
||||
globs: ["api/**/*.php", "apps/portal/**/*.{vue,ts}"]
|
||||
globs: ["api/**/*.php"]
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
@@ -92,16 +92,20 @@ Route::middleware(['auth:sanctum', 'event.role:event_manager'])->group(...);
|
||||
|
||||
## Portal Architecture
|
||||
|
||||
### Two Access Modes in One App (`apps/portal/`)
|
||||
### Two Access Modes Under `/portal/*` Routes (within `apps/app/`)
|
||||
|
||||
Post-WS-3, the portal lives in the main SPA at `/portal/*` routes.
|
||||
Two access modes coexist:
|
||||
|
||||
| Mode | Middleware | Users | Token Source |
|
||||
|------|-----------|-------|-------------|
|
||||
| Login | `auth:sanctum` | Volunteers, Crew | Bearer token from login |
|
||||
|------|------------|-------|--------------|
|
||||
| Login | `auth:sanctum` | Volunteers, Crew | Bearer token from login (httpOnly cookie) |
|
||||
| Token | `portal.token` | Artists, Suppliers, Press | URL token param: `?token=ULID` |
|
||||
|
||||
### Token-Based Authentication Flow
|
||||
```
|
||||
1. Artist/supplier receives email with link: https://portal.crewli.app/advance?token=01HQ3K...
|
||||
1. Artist/supplier receives email with link: https://crewli.app/portal/advance?token=01HQ3K...
|
||||
(Legacy portal.crewli.app links 301-redirect, preserving the token query param)
|
||||
2. Portal detects token in URL query parameter
|
||||
3. POST /api/v1/portal/token-auth { token: '01HQ3K...' }
|
||||
4. Backend validates token against artists.portal_token or production_requests.token
|
||||
@@ -111,9 +115,9 @@ Route::middleware(['auth:sanctum', 'event.role:event_manager'])->group(...);
|
||||
|
||||
### Login-Based Authentication Flow
|
||||
```
|
||||
1. Volunteer navigates to https://portal.crewli.app/login
|
||||
1. Volunteer navigates to https://crewli.app/login
|
||||
2. Enters email + password
|
||||
3. POST /api/v1/auth/login (same endpoint as apps/app/)
|
||||
3. POST /api/v1/auth/login
|
||||
4. Returns user + organisations + event roles
|
||||
5. Portal shows volunteer-specific views (My Shifts, Claim Shifts, Messages, Profile)
|
||||
```
|
||||
@@ -190,12 +194,11 @@ class PortalTokenMiddleware
|
||||
// config/cors.php
|
||||
'allowed_origins' => [
|
||||
env('FRONTEND_APP_URL', 'http://localhost:5174'),
|
||||
env('FRONTEND_PORTAL_URL', 'http://localhost:5175'),
|
||||
],
|
||||
'supports_credentials' => true,
|
||||
```
|
||||
|
||||
Production example (subdomains on **crewli.app**): `FRONTEND_APP_URL=https://crewli.app`, `FRONTEND_PORTAL_URL=https://portal.crewli.app`, and `SANCTUM_STATEFUL_DOMAINS=crewli.app,portal.crewli.app`.
|
||||
Production example (registered domain **crewli.app**): `FRONTEND_APP_URL=https://crewli.app` and `SANCTUM_STATEFUL_DOMAINS=crewli.app`. The legacy `FRONTEND_PORTAL_URL` env key is retained for outbound-email controllers (per AUTH_ARCHITECTURE.md §11), but resolves to the same host post-WS-3.
|
||||
|
||||
## Shift Claiming & Approval Flow
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -124,13 +124,12 @@ make db-shell
|
||||
|
||||
| Resource | Contents |
|
||||
|----------|----------|
|
||||
| [resources/design/](resources/design/) | **Canonical product specs** in Markdown. Referenced by `.cursor` and `CLAUDE.md` as source of truth for features and data model: `design-document.md`, `dev-guide.md`, `start-guide.md`. |
|
||||
| [.cursor/ARCHITECTURE.md](.cursor/ARCHITECTURE.md) | System diagram, apps, multi-tenancy, roles, event lifecycle, API route map, core schema overview (summarises `resources/design` when present) |
|
||||
| [.cursor/instructions.md](.cursor/instructions.md) | Quick reference, phased roadmap, module build order |
|
||||
| [.cursor/rules/](.cursor/rules/) | Workspace, Laravel, Vue, testing conventions |
|
||||
| [docs/SETUP.md](docs/SETUP.md) | Environment and local setup |
|
||||
| [docs/API.md](docs/API.md) | API notes (if maintained) |
|
||||
| [docs/SCHEMA.md](docs/SCHEMA.md) | Schema notes (if maintained) |
|
||||
| [CLAUDE.md](CLAUDE.md) | Project conventions, vibe-coding principles, Vuexy-first decision tree (auto-loaded by Claude Code). |
|
||||
| [.cursor/rules/](.cursor/rules/) | Workspace, Laravel, Vue, testing conventions. |
|
||||
| [dev-docs/SETUP.md](dev-docs/SETUP.md) | Environment and local setup. |
|
||||
| [dev-docs/SCHEMA.md](dev-docs/SCHEMA.md) | Database schema (kept in sync with migrations). |
|
||||
| [dev-docs/API.md](dev-docs/API.md) | API contract. |
|
||||
| [dev-docs/design-document.md](dev-docs/design-document.md) | Product specification. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
> **Doelgroep:** Bert (product owner + solo dev), Claude (architect/PM rol in chat), Claude Code (executie).
|
||||
> **Positie t.o.v. andere docs:** dit document is tijdelijk. Na afronding van de sprint wordt
|
||||
> de inhoud gedistribueerd over `/CLAUDE.md`, `/dev-docs/SCHEMA.md`, `/dev-docs/ARCH-*.md` en
|
||||
> `/dev-docs/dev-guide.md`. Tijdens de sprint is het de enkele bron van waarheid voor wát
|
||||
> `/dev-docs/SETUP.md`. Tijdens de sprint is het de enkele bron van waarheid voor wát
|
||||
> we aan het doen zijn en waaróm.
|
||||
|
||||
---
|
||||
|
||||
@@ -850,35 +850,6 @@ post-WS-3 PR-B2b.
|
||||
|
||||
---
|
||||
|
||||
### TECH-DOCS-APPS-PORTAL-PURGE — Sweep remaining apps/portal references from briefing/tooling docs
|
||||
|
||||
**Aanleiding:** WS-3 PR-B2b purgeerde `apps/portal` uit de
|
||||
load-bearing files (`README.md`, `Makefile`, `CLAUDE.md`) en de
|
||||
deploy-config. De briefing/tooling docs verwijzen nog steeds naar
|
||||
de pre-consolidatie tweede SPA.
|
||||
|
||||
**Files:**
|
||||
|
||||
- `.cursor/instructions.md`
|
||||
- `.cursor/ARCHITECTURE.md`
|
||||
- `.cursor/rules/101_vue.mdc`
|
||||
- `.cursor/rules/102_multi_tenancy.mdc`
|
||||
- `dev-docs/MASTER_PROMPT_CC.md`
|
||||
- `dev-docs/MASTER_PROMPT_CURSOR.md`
|
||||
- `dev-docs/SETUP.md`
|
||||
- `dev-docs/dev-guide.md`
|
||||
- `dev-docs/CLAUDE_CODE_TOOLING.md`
|
||||
|
||||
**Skip:** `dev-docs/WS-3-SESSION-1C-AUDIT.md` — historical sprint
|
||||
audit, frozen in time, references are factually correct for the
|
||||
session it documents.
|
||||
|
||||
**Prioriteit:** Laag — single `chore(docs)` PR. Niet blokkerend voor
|
||||
runtime; LLM/IDE briefings produceren licht stale context tot dit
|
||||
landt. Effective post-WS-3 PR-B2b.
|
||||
|
||||
---
|
||||
|
||||
### OPS — Retire `portal.crewli.app` DNS record
|
||||
|
||||
**Aanleiding:** Post-WS-3 PR-B2b serves crewli.app als single SPA;
|
||||
@@ -994,6 +965,7 @@ ARCH-discussie en RFC.
|
||||
## Opgeloste items (mei 2026)
|
||||
|
||||
- ~~**WS-TOOLING-001**: Claude Code deterministic guard-rail layer (5 hooks, `crewli-reviewer` subagent op Opus 4.7, 3 slash commands `/sprint-status` `/review-multitenancy` `/sync-docs`, `dev-docs/CLAUDE_CODE_TOOLING.md`). 8/8 smoke tests groen, live integratie geverifieerd. Merge `ad36c06` op 2026-05-05. Follow-ups: TECH-HOOK-001, TECH-CMD-001, TECH-STYLE-001.~~ ✅
|
||||
- ~~**TECH-DOCS-APPS-PORTAL-PURGE**: per-file DELETE/REWRITE/KEEP_AND_PURGE matrix uitgevoerd op alle 9 docs uit de oorspronkelijke entry, plus de `post-edit-eslint.sh` hook (out-of-scope vondst uit Phase A). Vijf obsolete docs verwijderd (`.cursor/instructions.md`, `.cursor/ARCHITECTURE.md`, `dev-docs/MASTER_PROMPT_CC.md`, `dev-docs/MASTER_PROMPT_CURSOR.md`, `dev-docs/dev-guide.md` — totaal ~80 KB). Drie herschreven (`SETUP.md`, `101_vue.mdc`, hook-script). Twee chirurgisch gepurgeerd (`102_multi_tenancy.mdc`, `CLAUDE_CODE_TOOLING.md`). Externe verwijzingen in README.md, CLAUDE_DESKTOP_SETUP.md, ARCH-CONSOLIDATION-2026-04.md en VIBE_CODING_CHECKLIST.md mee bijgewerkt. WS-3 PR-C op 2026-05-06. Single SPA, single cookie, single deploy host. WS-3 compleet.~~ ✅
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Everything except `settings.local.json` is checked in.
|
||||
| PreToolUse | `Edit\|Write\|MultiEdit` | `protect-files.sh` | Blocks edits to secrets, lock files, default migrations, the deleted `apps/admin/`, `.claude/` itself, and `dev-docs/SCHEMA.md`. | Exit 2 with reason on stderr. |
|
||||
| PreToolUse | `Bash` | `block-dangerous-bash.sh` | Blocks `git reset --hard`, force pushes, blanket dependency updates, database wipes that aren't scoped to `--env=testing`, and `rm -rf` on absolute paths outside `/tmp`, `/var/folders`, and `$HOME`. | Exit 2 with reason on stderr. |
|
||||
| PostToolUse | `Edit\|Write\|MultiEdit` | `post-edit-pint.sh` | Runs `vendor/bin/pint --dirty` from `api/` after any `.php` edit. | Exit 0 silently — formatting failures never block. |
|
||||
| PostToolUse | `Edit\|Write\|MultiEdit` | `post-edit-eslint.sh` | Runs `pnpm eslint --fix` inside the matching SPA dir for `.vue/.ts/.tsx/.js` files under `apps/app/` or `apps/portal/`. | Exit 0 silently. |
|
||||
| PostToolUse | `Edit\|Write\|MultiEdit` | `post-edit-eslint.sh` | Runs `pnpm eslint --fix` inside `apps/app/` for `.vue/.ts/.tsx/.js` files. | Exit 0 silently. |
|
||||
| SessionStart | `compact` | `inject-sprint-context.sh` | Prints branch, last 10 commits, and the top of `BACKLOG.md` so Claude resumes with sprint context after auto-compaction. | Exit 0; output is appended to context. |
|
||||
|
||||
Every script:
|
||||
|
||||
@@ -111,11 +111,8 @@ start-guide.md
|
||||
SETUP.md
|
||||
SECURITY_AUDIT.md
|
||||
SCHEMA.md
|
||||
MASTER_PROMPT_CURSOR.md
|
||||
MASTER_PROMPT_CC.md
|
||||
form-builder-migration-playbook.md
|
||||
form-builder-getting-started.md
|
||||
dev-guide.md
|
||||
design-document.md
|
||||
COPY_CATALOGUE.md
|
||||
BACKLOG.md
|
||||
@@ -169,4 +166,4 @@ subprocess-spawn (bij volgende tool call).
|
||||
## Gerelateerd
|
||||
|
||||
- `CLAUDE.md` (repo root) — instructies voor Claude Code, niet Claude Desktop
|
||||
- `dev-docs/dev-guide.md` — ontwikkelworkflow
|
||||
- `dev-docs/SETUP.md` — ontwikkelworkflow
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
# Crewli — Claude Code Master Prompt
|
||||
# Plak dit BOVEN elke task. Vervang [TASK] onderaan.
|
||||
|
||||
## MANDATORY PREAMBLE — READ BEFORE DOING ANYTHING
|
||||
|
||||
Read `/CLAUDE.md` and `/dev-docs/SCHEMA.md` in full before starting. These are
|
||||
your single source of truth. Do not deviate from them. Do not make assumptions
|
||||
about the database schema — always verify against SCHEMA.md. Also read
|
||||
`/dev-docs/API.md` for the existing API contract.
|
||||
|
||||
If the task involves a module that already has existing code, read that code
|
||||
first. Understand the current state before making changes.
|
||||
|
||||
## PROJECT CONTEXT
|
||||
|
||||
Crewli is a multi-tenant SaaS platform for event and festival management.
|
||||
|
||||
- **Backend:** Laravel 12 REST API (no Blade views, no Inertia), Sanctum auth,
|
||||
Spatie Permission, MySQL 8, Redis
|
||||
- **Frontend:** Two standalone Vue 3 + TypeScript SPAs on Vuexy 9.5 /
|
||||
Vuetify 3.10 — `apps/app/` (port 5174), `apps/portal/` (port 5175)
|
||||
- **State:** Pinia + TanStack Vue Query
|
||||
- **Forms:** VeeValidate + Zod
|
||||
- **API base path:** `/api/v1/`
|
||||
- **Testing:** PHPUnit (backend), Vitest (frontend)
|
||||
|
||||
### Repository structure
|
||||
|
||||
```
|
||||
crewli/
|
||||
├── api/ # Laravel 12 backend
|
||||
│ ├── app/
|
||||
│ │ ├── Http/
|
||||
│ │ │ ├── Controllers/Api/V1/
|
||||
│ │ │ ├── Middleware/
|
||||
│ │ │ └── Requests/ # Form Requests per endpoint
|
||||
│ │ ├── Models/
|
||||
│ │ ├── Policies/
|
||||
│ │ ├── Services/ # Business logic (NOT in controllers)
|
||||
│ │ ├── Enums/ # PHP Enums for all status/type fields
|
||||
│ │ ├── Events/ + Listeners/
|
||||
│ │ └── Jobs/ # Queued jobs (briefings, notifications)
|
||||
│ ├── database/
|
||||
│ │ ├── migrations/
|
||||
│ │ ├── factories/
|
||||
│ │ └── seeders/
|
||||
│ └── tests/Feature/Api/V1/
|
||||
├── apps/
|
||||
│ ├── app/ # Organizer + Platform Admin SPA (main app)
|
||||
│ │ └── src/
|
||||
│ │ ├── lib/axios.ts # THE ONLY axios instance
|
||||
│ │ ├── composables/api/ # TanStack Query composables
|
||||
│ │ ├── stores/ # Pinia stores
|
||||
│ │ ├── types/ # TypeScript interfaces
|
||||
│ │ └── pages/
|
||||
│ └── portal/ # Dual-mode portal
|
||||
├── dev-docs/ # Developer documentation (source of truth)
|
||||
│ ├── SCHEMA.md
|
||||
│ ├── API.md
|
||||
│ ├── BACKLOG.md
|
||||
│ ├── design-document.md
|
||||
│ ├── dev-guide.md
|
||||
│ └── start-guide.md
|
||||
├── docs/ # VitePress end-user documentation (Dutch)
|
||||
├── CLAUDE.md # This file's companion — workspace rules
|
||||
└── .cursorrules
|
||||
```
|
||||
|
||||
### Key architectural decisions (final, non-negotiable)
|
||||
|
||||
- **ULID** primary keys on all business tables via `HasUlids` — NEVER UUID v4
|
||||
- **Integer auto-increment** on pure pivot tables only
|
||||
- **JSON columns** exclusively for opaque config (settings, blocks) — never
|
||||
for queryable data
|
||||
- **Multi-tenancy** via `OrganisationScope` Eloquent Global Scope on all
|
||||
event-related models
|
||||
- **Soft delete** per table type as documented in SCHEMA.md — immutable audit
|
||||
records (check_ins, form_submissions, briefing_sends) do NOT get soft delete
|
||||
- **Portal dual-mode:** Sanctum session for volunteers/crew (persistent
|
||||
identity), `portal.token` middleware for artists/suppliers/press
|
||||
(event-specific, no account)
|
||||
|
||||
---
|
||||
|
||||
## ZERO-COMPROMISE RULES — VIOLATIONS ARE BLOCKERS
|
||||
|
||||
These rules are absolute. No workarounds. No "we'll fix it later". No partial
|
||||
implementations. If something cannot be done properly, STOP and report it.
|
||||
|
||||
### Architecture & design
|
||||
|
||||
1. **ARCHITECTURE FIRST.** If the task requires a new pattern, data structure,
|
||||
or integration that isn't documented in CLAUDE.md or SCHEMA.md — stop and
|
||||
ask. Do not invent architecture. Write an Architecture Decision Record (ADR)
|
||||
in `/dev-docs/decisions/` if a significant design choice is needed.
|
||||
|
||||
2. **DELETE OVER ADAPT.** If you find duplicate logic, conflicting patterns, or
|
||||
legacy code that does the same thing differently: delete the worse version.
|
||||
Do not build alongside it. Do not "wrap" it. If two implementations exist,
|
||||
one must die. Consolidate to one source of truth.
|
||||
|
||||
3. **STRICT LAYERING.** Business logic belongs in `app/Services/`, NEVER in
|
||||
controllers. Controllers handle HTTP concerns only: receive request, call
|
||||
service, return resource. Data access patterns go through Eloquent models
|
||||
with proper scopes. No "quick fixes" in the wrong layer.
|
||||
- Controller → receives FormRequest, calls Service, returns API Resource
|
||||
- Service → contains business logic, validation beyond FormRequest,
|
||||
orchestration
|
||||
- Model → relationships, scopes, accessors, mutators
|
||||
- Job → async work dispatched by Service
|
||||
|
||||
4. **CONTRACT-FIRST.** Before implementing any module:
|
||||
- Define the PHP Enum(s) in `app/Enums/` for all status/type fields
|
||||
- Define the API Resource (response shape) first
|
||||
- Define the Form Request (input validation) first
|
||||
- Then implement the Service and Controller
|
||||
Types and contracts define behaviour. Implementation follows.
|
||||
|
||||
5. **CONSISTENCY OVER CLEVERNESS.** Use the same pattern for every similar
|
||||
problem. If existing modules use a specific approach for pagination, error
|
||||
handling, or resource loading — use that exact same approach. Never
|
||||
introduce a "better" alternative pattern without refactoring ALL existing
|
||||
code to match. One pattern per problem type, across the entire codebase.
|
||||
|
||||
6. **SINGLE SOURCE OF TRUTH.** Every piece of information exists in exactly
|
||||
one place:
|
||||
- Enum values → PHP Enum class (not string literals)
|
||||
- Validation rules → Form Request (not duplicated in frontend)
|
||||
- Response shape → API Resource (not ad-hoc arrays)
|
||||
- Config values → .env / config files (not hardcoded)
|
||||
- Schema definition → SCHEMA.md (not guessed)
|
||||
|
||||
### Code quality
|
||||
|
||||
7. **NO TODO / FIXME / HACK.** Zero tolerance. If you cannot complete
|
||||
something, stop and report it. Do not leave stubs, placeholders, or
|
||||
"implement later" comments. Every file you touch must be production-ready.
|
||||
|
||||
8. **NO UNTYPED CODE.** PHP: `declare(strict_types=1)` on every file. Return
|
||||
types on all methods. Typed properties. Use PHP Enums (not string literals)
|
||||
for all status, type, and role fields. No `mixed` where a concrete type
|
||||
or union type would work.
|
||||
|
||||
9. **NO GENERIC NAMES.** Names must be specific and self-documenting:
|
||||
- ❌ `DataService`, `Helper`, `Manager`, `handleData()`, `processItem()`
|
||||
- ✅ `ShiftAssignmentService`, `VolunteerAvailabilityChecker`,
|
||||
`resolveShiftConflict()`, `calculateFillRate()`
|
||||
|
||||
10. **NO SILENT ERROR HANDLING.** No empty catch blocks. No `catch { return
|
||||
null; }`. Every error must be: logged (via `Log::error()` with context),
|
||||
or rethrown, or handled with a proper API error response. Use
|
||||
`report($e)` for unexpected errors.
|
||||
|
||||
### Testing
|
||||
|
||||
11. **TESTS ARE DESIGN, NOT AFTERTHOUGHT.** Tests define expected behaviour.
|
||||
Every controller needs feature tests covering:
|
||||
- 200/201 (happy path for each action)
|
||||
- 401 (unauthenticated access)
|
||||
- 403 (wrong organisation — cross-tenant access attempt)
|
||||
- 403 (insufficient role/permission)
|
||||
- 422 (validation errors with specific field assertions)
|
||||
- Edge cases specific to the module (e.g., shift conflict, capacity full)
|
||||
Run `php artisan test` after EVERY module. Fix failures before proceeding.
|
||||
Never skip, comment out, or mark tests as incomplete.
|
||||
|
||||
12. **FACTORIES ARE REALISTIC.** Use realistic Dutch test data (names,
|
||||
addresses, company names) that reflects actual usage. Factories must create
|
||||
valid, complete records — no missing required fields or placeholder values.
|
||||
|
||||
### Data & persistence
|
||||
|
||||
13. **DATA MODEL IS SACRED.** Never deviate from SCHEMA.md. Every column,
|
||||
every constraint, every index documented there must be in the migration.
|
||||
If SCHEMA.md is unclear, ask — do not guess.
|
||||
|
||||
14. **EVERY MIGRATION HAS down().** The `down()` method must cleanly reverse
|
||||
the `up()`. Drop tables, remove columns, restore previous state. No
|
||||
`down()` methods that throw or do nothing.
|
||||
|
||||
15. **INDEXES ARE MANDATORY.** Every foreign key column, every column used in
|
||||
WHERE/ORDER BY clauses, every unique constraint from SCHEMA.md must have
|
||||
an explicit index. Verify composite indexes match the documented patterns.
|
||||
|
||||
### Security & multi-tenancy
|
||||
|
||||
16. **NO UNSCOPED QUERIES.** Every query on event-related models must be
|
||||
scoped to `organisation_id` via `OrganisationScope`. Mental test: can
|
||||
User A from Org 1 ever see, modify, or infer the existence of data from
|
||||
Org 2? If yes → security bug → fix immediately.
|
||||
|
||||
17. **POLICIES ARE COMPLETE.** Every policy method checks:
|
||||
- Does the user belong to the correct organisation?
|
||||
- Does the user have the required Spatie role/permission for this action?
|
||||
- No `return true` placeholders. No missing methods.
|
||||
|
||||
18. **FORM REQUESTS ARE COMPLETE.** Every store/update has a Form Request
|
||||
with full validation matching SCHEMA.md constraints: required, nullable,
|
||||
max length, enum values (referencing the PHP Enum), exists rules with
|
||||
proper scoping (e.g., `exists:events,id` scoped to organisation).
|
||||
|
||||
### API responses
|
||||
|
||||
19. **NO BARE MODEL RETURNS.** Every API response goes through an API
|
||||
Resource. Never `return $model`, `$model->toArray()`, or raw arrays.
|
||||
Resources define the public contract. Computed fields (fill_rate,
|
||||
status_label, slot counts) belong in the Resource.
|
||||
|
||||
20. **CONSISTENT RESPONSE STRUCTURE.** Follow the existing pattern:
|
||||
`{ data: {...}, meta: {...} }` for paginated lists. Consistent error
|
||||
format with `{ message: "...", errors: {...} }` for validation failures.
|
||||
|
||||
### Resilience & operations
|
||||
|
||||
21. **IDEMPOTENT OPERATIONS.** Every queued job must be safe to retry.
|
||||
Check-before-act: verify state hasn't changed. Use database transactions
|
||||
for multi-step mutations. Queued notifications and external API calls
|
||||
(Zender SMS/WhatsApp) must handle duplicates gracefully.
|
||||
|
||||
22. **OBSERVABILITY.** Log significant business events using
|
||||
`spatie/laravel-activitylog` (already installed). Every create, update,
|
||||
delete, and status change on business entities must be logged with:
|
||||
- `causedBy($user)` — who did it
|
||||
- `performedOn($model)` — what was affected
|
||||
- `withProperties([...])` — relevant context (old values, new values)
|
||||
|
||||
23. **API VERSIONING.** All routes under `/api/v1/`. Controllers in
|
||||
`App\Http\Controllers\Api\V1\`. Never introduce breaking changes to
|
||||
existing endpoints — add new fields, don't rename or remove.
|
||||
|
||||
### Process
|
||||
|
||||
24. **MODULE GENERATION ORDER.** Always follow this sequence. No skipping.
|
||||
1. PHP Enum(s) for status/type fields (`app/Enums/`)
|
||||
2. Migration(s) — verify against SCHEMA.md
|
||||
3. Eloquent Model with: HasUlids, HasFactory, SoftDeletes (if documented),
|
||||
OrganisationScope (if event-related), relationships, scopes, accessors
|
||||
4. Factory with realistic Dutch test data
|
||||
5. Service class for business logic (`app/Services/`)
|
||||
6. Policy for authorisation
|
||||
7. Form Request(s) for validation
|
||||
8. API Resource for response transformation
|
||||
9. Resource Controller (thin — delegates to Service)
|
||||
10. Routes in `api/routes/api.php`
|
||||
11. Feature tests — run them, fix failures
|
||||
12. Activity log integration in Service methods
|
||||
13. Update `/dev-docs/API.md` with new routes
|
||||
|
||||
25. **GIT.** Auto-commit after each completed module:
|
||||
`feat(module-name): add backend scaffold with tests`
|
||||
|
||||
---
|
||||
|
||||
## VERIFICATION CHECKLIST (run before reporting "done")
|
||||
|
||||
```bash
|
||||
# All tests pass
|
||||
php artisan test
|
||||
|
||||
# Database rebuilds cleanly
|
||||
php artisan migrate:fresh --seed
|
||||
|
||||
# New routes are visible
|
||||
php artisan route:list --path=api/v1
|
||||
|
||||
# No forbidden patterns
|
||||
grep -rn "TODO\|FIXME\|HACK\|dd(\|dump(\|var_dump\|Model::all()" \
|
||||
api/app/ api/tests/ --include="*.php"
|
||||
|
||||
# No UUID v4 in migrations
|
||||
grep -rn "uuid(" api/database/migrations/ --include="*.php"
|
||||
|
||||
# Static analysis (if configured)
|
||||
./vendor/bin/phpstan analyse
|
||||
```
|
||||
|
||||
Manual verification:
|
||||
- [ ] Every new model has: HasUlids, HasFactory, correct SoftDeletes, complete
|
||||
$fillable, all relationships from SCHEMA.md
|
||||
- [ ] Every new model with event data has OrganisationScope
|
||||
- [ ] Every controller action is covered by a Policy method
|
||||
- [ ] Every Service method logs activity via spatie/laravel-activitylog
|
||||
- [ ] Every Form Request references PHP Enums (not string literals) for
|
||||
enum validation
|
||||
- [ ] Business logic is in Service classes, not in Controllers
|
||||
- [ ] API Resource includes computed fields where applicable
|
||||
- [ ] No N+1: index actions use `with()` for all accessed relationships
|
||||
- [ ] `/dev-docs/API.md` updated with new routes
|
||||
|
||||
---
|
||||
|
||||
## TASK
|
||||
|
||||
[INSERT SPECIFIC TASK HERE]
|
||||
@@ -1,176 +0,0 @@
|
||||
# Crewli — Cursor Master Prompt
|
||||
# Plak dit BOVEN elke task. Vervang [TASK] onderaan.
|
||||
|
||||
## MANDATORY PREAMBLE
|
||||
|
||||
Read `@CLAUDE.md` and `@dev-docs/SCHEMA.md` before starting. Use `@workspace`
|
||||
for full codebase context. These documents are your source of truth.
|
||||
|
||||
If modifying existing code, read the current implementation first. Understand
|
||||
the patterns already in use before writing anything new.
|
||||
|
||||
## PROJECT CONTEXT
|
||||
|
||||
Crewli — multi-tenant SaaS for event/festival management.
|
||||
|
||||
- **Frontend:** Vue 3 + TypeScript + Vuexy 9.5 (Vuetify 3.10) + Pinia +
|
||||
TanStack Vue Query + VeeValidate + Zod
|
||||
- **Two SPAs:**
|
||||
- `apps/app/` — Organizer + Platform Admin main app (port 5174)
|
||||
- `apps/portal/` — Dual-mode portal (port 5175)
|
||||
- **API base:** `VITE_API_URL` from `.env.local` (default `http://localhost:8000`)
|
||||
- **Axios instance:** `src/lib/axios.ts` — this is the ONLY axios instance.
|
||||
Never create another. Never import axios directly in components.
|
||||
- **Backend Enums:** PHP Enums in `api/app/Enums/` define all valid status/type
|
||||
values. TypeScript equivalents must mirror these exactly in `src/types/`.
|
||||
|
||||
### Frontend file structure (per app)
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib/axios.ts # Singleton axios instance (DO NOT DUPLICATE)
|
||||
├── types/ # TypeScript interfaces per module
|
||||
│ └── [module].ts
|
||||
├── composables/api/ # TanStack Query composables per module
|
||||
│ └── use[Module].ts
|
||||
├── stores/ # Pinia stores (cross-component state only)
|
||||
│ └── use[Module]Store.ts
|
||||
├── pages/ # Page components
|
||||
│ └── [module]/
|
||||
│ ├── index.vue # List view
|
||||
│ └── [id].vue # Detail view (or side panel)
|
||||
└── router/ # Vue Router config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ZERO-COMPROMISE RULES — VIOLATIONS ARE BLOCKERS
|
||||
|
||||
### TypeScript & typing
|
||||
|
||||
1. **NO `any` TYPES. EVER.** Every variable, prop, emit, return type, ref,
|
||||
reactive, computed, and API response is fully typed. If you don't know the
|
||||
type, check the backend API Resource in `api/app/Http/Resources/` — that
|
||||
defines the contract. Create matching interfaces in `src/types/[module].ts`.
|
||||
|
||||
2. **TYPES FIRST.** Before writing any composable or component for a new
|
||||
module, create the TypeScript interfaces in `src/types/[module].ts`. These
|
||||
mirror the backend API Resource shape. Include:
|
||||
- Entity interface (e.g., `Shift`, `Person`, `Event`)
|
||||
- Create/Update DTOs (matching backend Form Request fields)
|
||||
- Enum-like union types or const objects matching backend PHP Enums
|
||||
- Paginated response wrapper if applicable
|
||||
|
||||
3. **ENUMS AS CONST OBJECTS.** Mirror backend PHP Enums as TypeScript const
|
||||
objects with `as const`, not as loose string unions scattered across files:
|
||||
```typescript
|
||||
// src/types/shift.ts
|
||||
export const ShiftAssignmentStatus = {
|
||||
PENDING_APPROVAL: 'pending_approval',
|
||||
APPROVED: 'approved',
|
||||
REJECTED: 'rejected',
|
||||
CANCELLED: 'cancelled',
|
||||
COMPLETED: 'completed',
|
||||
} as const
|
||||
export type ShiftAssignmentStatus = typeof ShiftAssignmentStatus[keyof typeof ShiftAssignmentStatus]
|
||||
```
|
||||
|
||||
### Architecture & patterns
|
||||
|
||||
4. **NO DIRECT AXIOS IN COMPONENTS.** All API calls go through composables in
|
||||
`composables/api/use[Module].ts` using TanStack Query (`useQuery` /
|
||||
`useMutation`). Components never import axios or `src/lib/axios.ts`.
|
||||
The composable is the only layer that knows about HTTP.
|
||||
|
||||
5. **NO OPTIONS API.** Always `<script setup lang="ts">`. Props via
|
||||
`defineProps<{...}>()`, emits via `defineEmits<{...}>()`, expose via
|
||||
`defineExpose()`. No `export default { ... }`.
|
||||
|
||||
6. **NO PROP DRILLING.** If state needs to cross more than one component
|
||||
boundary, use a Pinia store (`src/stores/use[Module]Store.ts`). Access
|
||||
reactive state via `storeToRefs()`. Mutations only through store actions.
|
||||
|
||||
7. **DELETE OVER ADAPT.** If you find duplicate composables, overlapping
|
||||
stores, or conflicting patterns: delete the worse version. Do not build
|
||||
alongside existing code that does the same thing. One implementation per
|
||||
concern.
|
||||
|
||||
8. **CONSISTENCY OVER CLEVERNESS.** Look at how existing modules handle the
|
||||
same pattern (table views, form dialogs, detail panels, error handling).
|
||||
Use the exact same approach. Never introduce a "better" alternative without
|
||||
refactoring ALL existing modules to match.
|
||||
|
||||
9. **SINGLE SOURCE OF TRUTH.** Don't duplicate:
|
||||
- Validation logic → Zod schema mirrors backend Form Request. One schema
|
||||
per form, referenced by VeeValidate.
|
||||
- Enum values → const objects in `src/types/`, matching backend Enums.
|
||||
Never use raw string literals like `'approved'` in templates or logic.
|
||||
- API URLs → constructed in composable from module prefix, never
|
||||
hardcoded in components.
|
||||
|
||||
### UI & UX
|
||||
|
||||
10. **NO CUSTOM CSS WHERE VUETIFY HAS A SOLUTION.** Before writing any CSS:
|
||||
check if a Vuetify utility class, component prop, or slot achieves the
|
||||
result. Custom CSS is a last resort, must be `<style scoped>`, and must
|
||||
have a comment explaining why Vuetify couldn't handle it.
|
||||
|
||||
11. **EVERY PAGE HAS THREE STATES:**
|
||||
- **Loading:** Vuetify skeleton loader or progress indicator
|
||||
- **Error:** User-facing message with retry action (`v-alert` with retry
|
||||
button). Show what went wrong in user terms, not technical jargon.
|
||||
- **Empty:** Helpful message explaining why the list is empty and what
|
||||
action to take (not a blank white screen).
|
||||
Never show only the happy path.
|
||||
|
||||
12. **MOBILE-FIRST.** Every component must be usable at 375px width. Use
|
||||
Vuetify's responsive props (`cols`, `sm`, `md`, `lg` on `v-col`). Never
|
||||
use fixed pixel widths for layout. Tables on mobile → card views or
|
||||
horizontal scroll with visual indicator.
|
||||
|
||||
13. **FORMS USE VEEVALIDATE + ZOD.** Define the Zod schema matching the
|
||||
backend Form Request rules. No inline validation logic in templates. Use
|
||||
`useForm()` and `useField()` from VeeValidate with the Zod resolver.
|
||||
|
||||
### Code quality
|
||||
|
||||
14. **NO TODO / FIXME / HACK.** Complete the implementation or report the
|
||||
blocker. No stubs, no "implement later", no placeholder components.
|
||||
|
||||
15. **NO ORPHANED IMPORTS OR UNUSED VARIABLES.** Run `npx tsc --noEmit`
|
||||
after every change. Zero errors, zero warnings. Dead code is deleted
|
||||
immediately, not commented out.
|
||||
|
||||
16. **NO GENERIC NAMES.** Component and composable names must be specific:
|
||||
- ❌ `DataTable.vue`, `useApi.ts`, `helpers.ts`, `utils.ts`
|
||||
- ✅ `ShiftAssignmentTable.vue`, `useShifts.ts`,
|
||||
`formatShiftTimeRange.ts`
|
||||
|
||||
### Routing & auth
|
||||
|
||||
17. **ROUTER GUARDS.** Protected routes check auth state via navigation
|
||||
guard. Portal routes validate token. No unguarded routes to
|
||||
authenticated content. Redirect to login on 401.
|
||||
|
||||
### Process
|
||||
|
||||
18. **COMPONENT CREATION ORDER.** For every new page/feature:
|
||||
1. TypeScript types in `src/types/[module].ts`
|
||||
2. API composable in `src/composables/api/use[Module].ts`
|
||||
3. Pinia store in `src/stores/use[Module]Store.ts` (if cross-component)
|
||||
4. Vue page component in `src/pages/[module]/`
|
||||
5. Router entry in `src/router/`
|
||||
|
||||
19. **VERIFY BEFORE DONE:**
|
||||
- `npx tsc --noEmit` — zero errors
|
||||
- No `any` types anywhere (search: `grep -rn ": any\|as any" src/`)
|
||||
- No TODO/FIXME/HACK
|
||||
- All three states (loading, error, empty) implemented
|
||||
- Mobile responsive at 375px
|
||||
- Consistent with existing module patterns
|
||||
|
||||
---
|
||||
|
||||
## TASK
|
||||
|
||||
[INSERT SPECIFIC TASK HERE]
|
||||
@@ -1,269 +1,216 @@
|
||||
# Crewli - Setup Guide
|
||||
# Crewli — Working in this repo
|
||||
|
||||
This guide walks you through setting up the Crewli project from scratch.
|
||||
|
||||
## Cursor AI Configuration
|
||||
|
||||
The project includes comprehensive AI instructions:
|
||||
|
||||
```
|
||||
.cursor/
|
||||
├── instructions.md # Quick start and common prompts
|
||||
├── ARCHITECTURE.md # System design and database schema
|
||||
└── rules/
|
||||
├── 001_workspace.mdc # Project structure and conventions
|
||||
├── 100_laravel.mdc # Laravel API patterns
|
||||
├── 101_vue.mdc # Vue + Vuexy patterns
|
||||
└── 200_testing.mdc # Testing strategies
|
||||
```
|
||||
|
||||
**Read these files first!** They contain everything Cursor needs to generate code correctly.
|
||||
A guide for developers continuing work on the Crewli codebase. Assumes you've cloned the repo and have basic terminal familiarity.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install these before starting:
|
||||
Install these once before starting:
|
||||
|
||||
### macOS (Homebrew)
|
||||
| Tool | Version | Install (macOS via Homebrew) |
|
||||
|------|---------|------------------------------|
|
||||
| PHP | 8.4 | `brew install php@8.4 && brew link php@8.4` |
|
||||
| Composer | 2.x | `brew install composer` |
|
||||
| Node.js | 20 LTS | `brew install fnm && fnm install 20 && fnm use 20` |
|
||||
| pnpm | 9.x | `npm install -g pnpm` |
|
||||
| Docker Desktop | latest | https://www.docker.com/products/docker-desktop/ |
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
# PHP 8.3
|
||||
brew install php@8.3
|
||||
brew link php@8.3
|
||||
|
||||
# Composer
|
||||
brew install composer
|
||||
|
||||
# Node.js (via fnm)
|
||||
brew install fnm
|
||||
fnm install 20
|
||||
fnm use 20
|
||||
|
||||
# pnpm
|
||||
npm install -g pnpm
|
||||
|
||||
# Docker Desktop
|
||||
# Download from: https://www.docker.com/products/docker-desktop/
|
||||
php -v # 8.4.x
|
||||
composer -V # 2.x
|
||||
node -v # v20.x
|
||||
pnpm -v # 9.x or 10.x
|
||||
docker -v # any recent version
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
## First-time setup
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
php -v # Should show 8.3.x
|
||||
composer -V # Should show 2.x
|
||||
node -v # Should show v20.x
|
||||
pnpm -v # Should show 8.x or 9.x
|
||||
docker -v # Should show Docker version
|
||||
```
|
||||
# 1. Backend dependencies
|
||||
cd api && composer install
|
||||
|
||||
---
|
||||
# 2. Frontend dependencies
|
||||
cd ../apps/app && pnpm install
|
||||
|
||||
## Step 1: Start Docker Services
|
||||
# 3. Backend env file
|
||||
cd ../../api
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
|
||||
```bash
|
||||
cd crewli
|
||||
# 4. Frontend env file
|
||||
cd ../apps/app
|
||||
cp .env.example .env.local
|
||||
|
||||
# 5. Start Docker services (MySQL, Redis, Mailpit)
|
||||
cd ..
|
||||
make services
|
||||
```
|
||||
|
||||
This starts:
|
||||
- **MySQL 8.0** on port 3306
|
||||
- **Redis** on port 6379
|
||||
- **Mailpit** on port 8025 (email testing UI)
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create Laravel API
|
||||
|
||||
Open the project in Cursor and use this prompt:
|
||||
|
||||
```
|
||||
Create a new Laravel 12 project in the api/ folder.
|
||||
|
||||
Requirements:
|
||||
- Use the command: composer create-project laravel/laravel api
|
||||
- After creation, install Sanctum: composer require laravel/sanctum
|
||||
- Configure for API-only (we don't need web routes)
|
||||
- Set up CORS for localhost:5174, localhost:5175
|
||||
- Use MySQL with these credentials:
|
||||
- Host: 127.0.0.1
|
||||
- Database: crewli
|
||||
- Username: crewli
|
||||
- Password: secret
|
||||
|
||||
Follow the conventions in .cursor/rules for code style.
|
||||
```
|
||||
|
||||
### Manual Alternative
|
||||
|
||||
```bash
|
||||
cd crewli
|
||||
composer create-project laravel/laravel api
|
||||
# 6. Database setup
|
||||
cd api
|
||||
composer require laravel/sanctum
|
||||
php artisan install:api
|
||||
php artisan migrate --seed
|
||||
|
||||
# 7. Frontend post-install (icon build + MSW worker)
|
||||
cd ../apps/app
|
||||
pnpm run build:icons
|
||||
pnpm run msw:init
|
||||
|
||||
# 8. Verify everything works
|
||||
cd ../../api
|
||||
php artisan test
|
||||
```
|
||||
|
||||
Then configure `api/.env`:
|
||||
If `php artisan test` is green, you're ready.
|
||||
|
||||
## Daily workflow
|
||||
|
||||
Three terminal tabs, plus an optional fourth for the queue worker:
|
||||
|
||||
| Terminal | Command | Where it runs | Port |
|
||||
|----------|---------|---------------|------|
|
||||
| 1. Services | `make services` (from repo root) | Docker | 3306 (MySQL), 6379 (Redis), 8025 (Mailpit) |
|
||||
| 2. API | `make api` (from repo root) | Laravel dev server | 8000 |
|
||||
| 3. SPA | `make app` (from repo root) | Vite dev server | 5174 |
|
||||
| 4. Queue worker (optional) | `cd api && php artisan queue:listen redis --queue=emails` | Local PHP | n/a |
|
||||
|
||||
The queue worker is only needed when you're triggering email flows (registration, password reset, email change, invitations). Routine UI work doesn't require it.
|
||||
|
||||
Stop services when done: `make services-stop`.
|
||||
|
||||
## Environment variables
|
||||
|
||||
### `api/.env`
|
||||
|
||||
The defaults from `.env.example` cover local development. Key entries:
|
||||
|
||||
```env
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=crewli
|
||||
DB_USERNAME=crewli
|
||||
DB_PASSWORD=secret
|
||||
|
||||
FRONTEND_APP_URL=http://localhost:5174
|
||||
FRONTEND_PORTAL_URL=http://localhost:5175
|
||||
SANCTUM_STATEFUL_DOMAINS=localhost:5174,localhost:5175
|
||||
QUEUE_CONNECTION=redis
|
||||
SESSION_DOMAIN=localhost
|
||||
FRONTEND_APP_URL=http://localhost:5174
|
||||
SANCTUM_STATEFUL_DOMAINS=localhost:5174
|
||||
APP_URL=http://localhost:8000
|
||||
```
|
||||
|
||||
**Production (domain `crewli.app`):** set `APP_URL=https://api.crewli.app`, point `FRONTEND_APP_URL` / `FRONTEND_PORTAL_URL` to `https://crewli.app` and `https://portal.crewli.app`, and `SANCTUM_STATEFUL_DOMAINS=crewli.app,portal.crewli.app` (hostnames only). Each SPA build should use `VITE_API_URL=https://api.crewli.app/api/v1`. Full template: `api/.env.example`. The product uses **`crewli.app`** only; **`crewli.nl`** is for a future public marketing site, not this API or SPAs.
|
||||
For production deployment (registered domain `crewli.app`):
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Vuexy frontends (this repo)
|
||||
|
||||
This monorepo already contains two SPAs under `apps/`:
|
||||
|
||||
| Directory | Role | Typical Vuexy source |
|
||||
|-----------|------|----------------------|
|
||||
| `apps/app/` | Organizer + Platform Admin (main product) | full-version (TypeScript) |
|
||||
| `apps/portal/` | External portal (volunteers, token links) | stripped starter / custom layout |
|
||||
|
||||
Super admin functionality lives in `apps/app/` under `/platform/*` routes, accessible to `super_admin` users.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Configure SPAs
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```bash
|
||||
cd apps/app && pnpm install
|
||||
cd ../portal && pnpm install
|
||||
```
|
||||
|
||||
### Create Environment Files
|
||||
|
||||
**apps/app/.env.local**
|
||||
```env
|
||||
VITE_API_URL=http://localhost:8000/api/v1
|
||||
VITE_APP_NAME="Crewli Organizer"
|
||||
APP_URL=https://api.crewli.app
|
||||
FRONTEND_APP_URL=https://crewli.app
|
||||
SESSION_DOMAIN=.crewli.app
|
||||
SANCTUM_STATEFUL_DOMAINS=crewli.app
|
||||
```
|
||||
|
||||
**apps/portal/.env.local**
|
||||
`crewli.nl` is reserved for a future marketing site only — not the application.
|
||||
|
||||
### `apps/app/.env.local`
|
||||
|
||||
```env
|
||||
VITE_API_URL=http://localhost:8000/api/v1
|
||||
VITE_APP_NAME="Crewli Portal"
|
||||
VITE_API_URL=http://localhost:8000
|
||||
VITE_APP_NAME="Crewli"
|
||||
```
|
||||
|
||||
### Dev server ports
|
||||
For production: `VITE_API_URL=https://api.crewli.app`.
|
||||
|
||||
From the repo root, `make app` and `make portal` start Vite on **5174** and **5175** respectively. If you run `pnpm dev` manually, configure the same ports in each app’s `vite.config.ts` under `server.port`.
|
||||
## Common tasks
|
||||
|
||||
---
|
||||
|
||||
## Step 5: API client in SPAs
|
||||
|
||||
`apps/app/src/lib/api-client.ts` and `apps/portal/src/lib/api-client.ts` share the same pattern: `VITE_API_URL` base, Bearer token from the `accessToken` cookie, 401 → clear cookies and redirect to `/login`. Build new composables on `apiClient`; keep Vuexy `useApi` for template demos only.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Create database schema
|
||||
|
||||
Implement migrations from the canonical schema, not a legacy intranet model:
|
||||
|
||||
- **`docs/SCHEMA.md`** — table list, columns, indexes
|
||||
- **`.cursor/ARCHITECTURE.md`** — overview and relationships
|
||||
- **`.cursor/rules/103_database.mdc`** — ULIDs, soft deletes, index rules
|
||||
|
||||
**Checked-in foundation (this repo):** Laravel defaults (`users`, `cache`, `jobs`) then `2026_04_07_*` migrations: Sanctum tokens → Spatie permission → activity log → `organisations` → `organisation_user` → `events` → `user_invitations` → `event_user_roles`. New modules should append migrations with a later timestamp in dependency order.
|
||||
|
||||
Typical next expansion order from `103_database.mdc`: festival sections, time slots, persons, shifts, …
|
||||
|
||||
Then run:
|
||||
### Run tests
|
||||
|
||||
```bash
|
||||
cd api && php artisan migrate
|
||||
# Backend (PHPUnit, all tests)
|
||||
cd api && php artisan test
|
||||
|
||||
# Backend (specific filter)
|
||||
cd api && php artisan test --filter=ShiftTest
|
||||
|
||||
# Backend (with coverage)
|
||||
cd api && php artisan test --coverage
|
||||
|
||||
# Frontend (Vitest, all tests)
|
||||
cd apps/app && pnpm test
|
||||
|
||||
# Frontend (typecheck)
|
||||
cd apps/app && pnpm typecheck
|
||||
|
||||
# Frontend (lint)
|
||||
cd apps/app && pnpm lint
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Start development
|
||||
|
||||
Open separate terminals (or use the Makefile from the repo root):
|
||||
### Database reset
|
||||
|
||||
```bash
|
||||
# Tab 1: Services (Docker)
|
||||
make services
|
||||
|
||||
# Tab 2: Laravel API
|
||||
make api
|
||||
|
||||
# Tab 3: Organizer SPA (optional)
|
||||
make app
|
||||
|
||||
# Tab 4: Portal SPA (optional)
|
||||
make portal
|
||||
cd api && php artisan migrate:fresh --seed
|
||||
```
|
||||
|
||||
---
|
||||
This drops all tables, re-runs migrations, and re-seeds. Useful when schema changes or seeders are updated.
|
||||
|
||||
## Building features
|
||||
### Inspect routes
|
||||
|
||||
Use Cursor with **`CLAUDE.md`** and **`.cursor/instructions.md`**. Example directions:
|
||||
|
||||
### Authentication
|
||||
|
||||
```
|
||||
Wire Sanctum API auth: login, logout, me; form requests; API resources; Vue apps use axios + token storage (see .cursor/rules).
|
||||
```bash
|
||||
cd api && php artisan route:list --path=api/v1
|
||||
```
|
||||
|
||||
### Events module (Crewli)
|
||||
### Build for production
|
||||
|
||||
```
|
||||
Events nested under organisations: ULID PK, OrganisationScope, policies, EventResource, feature tests (200/401/403/422).
|
||||
```bash
|
||||
cd apps/app && pnpm build
|
||||
```
|
||||
|
||||
### Portal token flow
|
||||
Output lands in `apps/app/dist/`. The `deploy.sh` script handles the rest for VPS deploys.
|
||||
|
||||
```
|
||||
Portal token middleware and routes for artist/supplier contexts; document links on https://portal.crewli.app/... (see .cursor/rules/102_multi_tenancy.mdc).
|
||||
### Static analysis (optional)
|
||||
|
||||
```bash
|
||||
cd api && composer analyse # Larastan / PHPStan level 6
|
||||
cd api && composer rector --dry-run # Rector findings
|
||||
```
|
||||
|
||||
---
|
||||
## Documentation reference
|
||||
|
||||
The `dev-docs/` directory is the developer source of truth. The most-used files:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `/CLAUDE.md` | Project conventions, vibe-coding principles, Vuexy-first decision tree (auto-loaded by Claude Code) |
|
||||
| `dev-docs/SCHEMA.md` | Database schema (current version v2.x, kept in sync with migrations) |
|
||||
| `dev-docs/API.md` | API contract |
|
||||
| `dev-docs/AUTH_ARCHITECTURE.md` | Auth design (httpOnly cookies, MFA, impersonation, portal tokens) |
|
||||
| `dev-docs/CLAUDE_CODE_TOOLING.md` | The `.claude/` deterministic guard-rail layer (hooks, subagent, slash commands) |
|
||||
| `dev-docs/VUEXY_COMPONENTS.md` | Vuexy component registry — consult before writing any frontend |
|
||||
| `dev-docs/BACKLOG.md` | Tracked tech debt and follow-ups |
|
||||
| `dev-docs/ARCH-*.md` | Architecture decisions per workstream (consolidation, form builder, bindings, API validation) |
|
||||
|
||||
The `docs/` directory (separate from `dev-docs/`) is end-user VitePress documentation in Dutch.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MySQL Connection Refused
|
||||
```bash
|
||||
# Check if Docker is running
|
||||
docker ps
|
||||
### MySQL connection refused
|
||||
|
||||
# Restart services
|
||||
make services-stop
|
||||
make services
|
||||
```bash
|
||||
docker ps # check containers are up
|
||||
make services-stop && make services # restart
|
||||
```
|
||||
|
||||
### CORS Errors
|
||||
Check `api/config/cors.php` allows your frontend origins.
|
||||
### Port 8000 already in use
|
||||
|
||||
Something else is bound to port 8000. Find it: `lsof -i :8000`. Kill it or change `make api` to use another port.
|
||||
|
||||
### Frontend TypeScript errors after pulling main
|
||||
|
||||
### Vuexy TypeScript Errors
|
||||
```bash
|
||||
cd apps/app
|
||||
pnpm install
|
||||
pnpm type-check
|
||||
pnpm install # picks up new dependencies
|
||||
pnpm typecheck # confirm clean
|
||||
```
|
||||
|
||||
---
|
||||
### Queue worker not picking up jobs
|
||||
|
||||
## Next steps
|
||||
Confirm Redis is running: `docker ps | grep redis`. If yes, restart the queue worker — long-running listeners can drift after long idle periods.
|
||||
|
||||
1. Services running (Docker)
|
||||
2. Laravel API configured and migrated
|
||||
3. SPAs installed (`apps/app`, `apps/portal`)
|
||||
4. Environment files for API + each SPA
|
||||
5. Authentication and organisation switching
|
||||
6. Events, sections, time slots, shifts
|
||||
7. Persons, crowd types, portal flows
|
||||
8. Accreditation, briefings, operational modules per roadmap in `.cursor/instructions.md`
|
||||
### Test suite is slow
|
||||
|
||||
PHPUnit defaults to a single process. Parallel: `cd api && php artisan test --parallel`.
|
||||
|
||||
@@ -40,7 +40,6 @@ overzicht en review-checklist.
|
||||
| Schema definitie | `/dev-docs/SCHEMA.md` | `/docs/SCHEMA.md` |
|
||||
| API contract | `/dev-docs/API.md` | `/docs/API.md` |
|
||||
| Design document | `/dev-docs/design-document.md` | `/docs/design-document.md` |
|
||||
| Dev guide | `/dev-docs/dev-guide.md` | `/docs/dev-guide.md` |
|
||||
| User docs (VitePress) | `/docs/` | — |
|
||||
| Workspace rules | `/CLAUDE.md` (root) | — |
|
||||
| Axios instance (app) | `apps/app/src/lib/axios.ts` | `src/utils/api.ts` etc. |
|
||||
|
||||
@@ -1,798 +0,0 @@
|
||||
# Crewli Development Guide
|
||||
|
||||
Cursor & Claude Code — Van Leeg Project naar Productie
|
||||
|
||||
**Versie:** 1.0 | **Datum:** Maart 2026 | **Stack:** Laravel 12 + Vue 3 | **AI Tools:** Cursor + Claude Code
|
||||
|
||||
## 1. Strategie & Mindset
|
||||
|
||||
Cursor vs Claude Code — wanneer gebruik je wat?
|
||||
|
||||
Voordat je begint met ontwikkelen is het belangrijk te begrijpen hoe Cursor en Claude Code zich tot elkaar verhouden. Ze zijn complementair — niet concurrerend.
|
||||
|
||||
| **Tool** | **Wanneer inzetten** |
|
||||
|----|----|
|
||||
| Cursor (IDE) | Dagelijks coderen. Inline autocomplete, context-aware suggesties, kleine refactors, code reviews, directe file-edits. Beste voor: een specifiek component bouwen, een bug fixen, een test schrijven. |
|
||||
| Claude Code (Terminal) | Grote, multi-file taken. Scaffolding van een volledig module (migrations + model + controller + tests + Vue-pagina). Autonome agent die zelfstandig werkt, tests uitvoert en fouten corrigeert. Beste voor: 'Bouw het volledige shift-module end-to-end.' |
|
||||
| Samen | Aanbevolen workflow: Claude Code genereert het skelet en alle bestanden. Cursor verfijnt, debugt en voegt details toe. Claude Code draait de test-suite. Cursor doet code review en stijlcorrecties. |
|
||||
|
||||
> **KERNPRINCIPE**
|
||||
>
|
||||
> Claude Code is je senior developer die grote blokken werk autonoom uitvoert.
|
||||
>
|
||||
> Cursor is je pair programmer die naast je zit terwijl jij zelf ook werkt.
|
||||
>
|
||||
> Jij bent de architect en product owner: jij beslist, zij bouwen.
|
||||
|
||||
## 2. De Eerste Stappen
|
||||
|
||||
Wat je vandaag doet voordat je één regel code schrijft
|
||||
|
||||
### 2.1 Repository structuur definitief maken
|
||||
|
||||
Controleer en bevestig de folderstructuur
|
||||
|
||||
Jouw huidige setup heeft al een goede basis. Bevestig of maak de volgende structuur:
|
||||
|
||||
```
|
||||
crewli/ # Monorepo root
|
||||
├── api/ # Laravel 12 backend
|
||||
│ ├── app/
|
||||
│ │ ├── Http/
|
||||
│ │ │ ├── Controllers/Api/V1/
|
||||
│ │ │ ├── Middleware/
|
||||
│ │ │ └── Requests/ # Form Requests per endpoint
|
||||
│ │ ├── Models/
|
||||
│ │ ├── Policies/ # Laravel Policies per model
|
||||
│ │ ├── Services/ # Business logic buiten controllers
|
||||
│ │ ├── Events/ + Listeners/
|
||||
│ │ └── Jobs/ # Queue jobs (briefings, PDF, notifs)
|
||||
│ ├── database/
|
||||
│ │ ├── migrations/
|
||||
│ │ ├── factories/
|
||||
│ │ └── seeders/
|
||||
│ └── tests/Feature/Api/V1/ # PHPUnit feature tests per controller
|
||||
├── apps/
|
||||
│ ├── app/ # Organizer + Platform Admin SPA (Vuexy) -- HOOFDAPP
|
||||
│ └── portal/ # Externe portals (vrijwilliger, artiest, leverancier)
|
||||
├── docs/ # Design document, API docs, ERD
|
||||
│ ├── design-document.md
|
||||
│ └── dev-guide.md
|
||||
└── .cursorrules # Cursor workspace rules
|
||||
```
|
||||
|
||||
### 2.2 Dependencies installeren
|
||||
|
||||
Backend en frontend klaarstomen
|
||||
|
||||
**Backend (api/)**
|
||||
|
||||
```bash
|
||||
cd api
|
||||
|
||||
# Spatie permissions (rollen/permissies)
|
||||
composer require spatie/laravel-permission
|
||||
|
||||
# Audit log
|
||||
composer require spatie/laravel-activitylog
|
||||
|
||||
# Media library (bestandsbeheer)
|
||||
composer require spatie/laravel-medialibrary
|
||||
|
||||
# PDF generatie
|
||||
composer require barryvdh/laravel-dompdf
|
||||
|
||||
# QR codes
|
||||
composer require endroid/qr-code
|
||||
|
||||
# Publiceer Spatie configs
|
||||
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
|
||||
php artisan vendor:publish --provider="Spatie\LaravelActivitylog\ActivitylogServiceProvider"
|
||||
```
|
||||
|
||||
**Frontend — alle apps (apps/app/, apps/portal/)**
|
||||
|
||||
```bash
|
||||
# TanStack Query voor API state management
|
||||
npm install @tanstack/vue-query
|
||||
|
||||
# Formuliervalidatie
|
||||
npm install vee-validate zod @vee-validate/zod
|
||||
|
||||
# Drag-and-drop (form builder, timetable, prioriteitsranking)
|
||||
npm install vuedraggable@next
|
||||
|
||||
# In apps/app/ ook:
|
||||
npm install @fullcalendar/vue3 @fullcalendar/timeline @fullcalendar/resource-timeline
|
||||
```
|
||||
|
||||
### 2.3 CLAUDE.md aanmaken
|
||||
|
||||
Het belangrijkste bestand in je hele repo
|
||||
|
||||
CLAUDE.md is de instructieset voor Claude Code. Het wordt automatisch geladen bij elke sessie. Dit bestand is de meest impactvolle investering die je doet — een uur hieraan besteden bespaart honderden uren aan correcties.
|
||||
|
||||
Maak aan: /crewli/CLAUDE.md (root niveau, zodat het voor alle sub-projecten geldt)
|
||||
|
||||
## 3. Helper Files — Volledige Inhoud
|
||||
|
||||
De exacte bestanden die je aanmaakt voor de eerste prompt
|
||||
|
||||
### 3.1 CLAUDE.md — Root niveau
|
||||
|
||||
Dit is de volledige, aanbevolen inhoud voor je CLAUDE.md. Kopieer dit letterlijk en pas aan waar nodig.
|
||||
|
||||
```
|
||||
# Crewli — Claude Code Instructies
|
||||
|
||||
## Project Context
|
||||
Crewli is een multi-tenant SaaS platform voor event- en festivalbeheer.
|
||||
Gebouwd voor een professionele vrijwilligersorganisatie, met SaaS-uitbreidingspotentieel.
|
||||
Design Document: /resources/design/design-document.md
|
||||
|
||||
## Tech Stack
|
||||
- Backend: PHP 8.2+, Laravel 12, Sanctum, Spatie Permission, MySQL 8, Redis
|
||||
- Frontend: TypeScript, Vue 3 (Composition API), Vuexy/Vuetify, Pinia, TanStack Query
|
||||
- Testing: PHPUnit (backend), Vitest (frontend)
|
||||
|
||||
## Repository Structuur
|
||||
- api/ Laravel backend
|
||||
- apps/app/ Organizer + Platform Admin SPA (hoofdapp, super admin onder /platform/*)
|
||||
- apps/portal/ Externe portals (vrijwilliger, artiest, leverancier)
|
||||
|
||||
## Backend Regels (STRIKT VOLGEN)
|
||||
|
||||
### Multi-tenancy
|
||||
- ELKE query op event-data MOET scoperen op organisation_id
|
||||
- Gebruik OrganisationScope als Eloquent Global Scope op alle event-gerelateerde modellen
|
||||
- Nooit directe id-checks in controllers — gebruik altijd Policies
|
||||
|
||||
### Controllers
|
||||
- Gebruik Resource Controllers (index/show/store/update/destroy)
|
||||
- Namespace: App\Http\Controllers\Api\V1\
|
||||
- Alle responses via API Resources (nooit model-attributen direct teruggeven)
|
||||
- Validatie via Form Requests (nooit inline validate())
|
||||
|
||||
### Modellen
|
||||
- Gebruik HasUlids trait op alle business-modellen (GEEN UUID v4)
|
||||
- Soft deletes op: Organisation, Event, FestivalSection, Shift, ShiftAssignment, Person, Artist
|
||||
- GEEN soft deletes op: CheckIn, BriefingSend, MessageReply, ShiftWaitlist (audit-records)
|
||||
- JSON kolommen ALLEEN voor opaque configuratie — nooit voor queryable data
|
||||
|
||||
### Database
|
||||
- Primaire sleutels: ULID via HasUlids (niet UUID v4, niet auto-increment voor business tables)
|
||||
- Elke migratie in volgorde aanmaken: eerst foundation, dan afhankelijke tabellen
|
||||
- ALTIJD composite indexes toevoegen zoals gedocumenteerd in het design document sectie 3.5
|
||||
|
||||
### Rollen & Permissies
|
||||
- Gebruik Spatie laravel-permission
|
||||
- Check rollen via $user->hasRole() en Policies — nooit hardcoded role strings in controllers
|
||||
- Drie niveaus: app (super_admin), organisatie (org_admin/org_member), event (event_manager etc.)
|
||||
|
||||
### Testing
|
||||
- Schrijf PHPUnit Feature Tests per controller
|
||||
- Minimaal per endpoint: happy path + unauthenticated (401) + wrong organisation (403)
|
||||
- Gebruik factories voor alle test-data
|
||||
- Draai tests NA elke module: php artisan test --filter=ModuleNaam
|
||||
|
||||
## Frontend Regels (STRIKT VOLGEN)
|
||||
|
||||
### Vue Componenten
|
||||
- Altijd <script setup lang='ts'> — nooit Options API
|
||||
- Props altijd getypeerd met defineProps<{...}>()
|
||||
- Emits altijd gedeclareerd met defineEmits<{...}>()
|
||||
|
||||
### API Calls
|
||||
- Gebruik TanStack Query (useQuery / useMutation) voor ALLE API calls
|
||||
- Nooit direct axios in een component — altijd via een composable in composables/api/
|
||||
- Pinia stores voor cross-component state — nooit prop drilling
|
||||
|
||||
### Naamgeving
|
||||
- DB kolommen: snake_case
|
||||
- TypeScript/JS variabelen: camelCase
|
||||
- Vue componenten: PascalCase (bijv. ShiftAssignPanel.vue)
|
||||
- Composables: use-prefix (bijv. useShifts.ts)
|
||||
- Pinia stores: use-suffix store (bijv. useEventStore.ts)
|
||||
|
||||
### UI
|
||||
- Gebruik ALTIJD Vuexy/Vuetify componenten voor layout, forms, tabellen, dialogen
|
||||
- Nooit custom CSS schrijven als een Vuetify klasse bestaat
|
||||
- Responsief: mobile-first, minimaal werkend op 375px breedte
|
||||
|
||||
## Verboden Patronen
|
||||
- NOOIT: $user->role === 'admin' (gebruik policies)
|
||||
- NOOIT: Model::all() zonder where-clausule (altijd scopen)
|
||||
- NOOIT: dd() of var_dump() achterlaten in code
|
||||
- NOOIT: .env waarden hardcoden in code
|
||||
- NOOIT: JSON kolommen gebruiken voor data waarop gefilterd wordt
|
||||
- NOOIT: UUID v4 als primaire sleutel (gebruik HasUlids)
|
||||
|
||||
## Volgorde bij elke nieuwe module
|
||||
1. Migratie(s) aanmaken en draaien
|
||||
2. Eloquent Model met relaties, scopes en HasUlids
|
||||
3. Factory voor test-data
|
||||
4. Policy voor autorisatie
|
||||
5. Form Request(s) voor validatie
|
||||
6. API Resource voor response transformatie
|
||||
7. Resource Controller
|
||||
8. Routes registreren in api.php
|
||||
9. PHPUnit Feature Test schrijven en draaien
|
||||
10. Vue composable voor API calls (useModuleNaam.ts)
|
||||
11. Pinia store indien cross-component state nodig
|
||||
12. Vue pagina component
|
||||
13. Route toevoegen in Vue Router
|
||||
```
|
||||
|
||||
### 3.2 .cursorrules — Root niveau
|
||||
|
||||
Dit is het equivalent van CLAUDE.md maar voor Cursor's autocomplete en inline AI. Korter en meer gefocust op directe code-stijl.
|
||||
|
||||
```
|
||||
# Crewli Cursor Rules
|
||||
|
||||
## Stack
|
||||
PHP 8.2 + Laravel 12 | TypeScript + Vue 3 + Vuexy/Vuetify | Pinia + TanStack Query
|
||||
|
||||
## Laravel
|
||||
- Resource Controllers, Form Requests, API Resources — altijd
|
||||
- HasUlids op business modellen, HasFactory, SoftDeletes waar gedocumenteerd
|
||||
- Global Scope OrganisationScope op event-gerelateerde modellen
|
||||
- Policies voor autorisatie, nooit inline role checks
|
||||
|
||||
## Vue 3
|
||||
- <script setup lang='ts'> altijd
|
||||
- TanStack Query voor API state, Pinia voor UI state
|
||||
- Vuetify componenten eerst, custom CSS als laatste redmiddel
|
||||
|
||||
## Naamgeving
|
||||
- snake_case DB | camelCase JS | PascalCase Vue | use* composables | use*Store Pinia
|
||||
|
||||
## Tests
|
||||
- PHPUnit Feature Test per controller, minimaal: 200 + 401 + 403
|
||||
```
|
||||
|
||||
### 3.3 dev-docs/SCHEMA.md — Levend schema-document
|
||||
|
||||
Maak een Markdown bestand aan in /dev-docs/ dat de tabel-definitie bevat als platte tekst. Claude Code gebruikt dit als primaire referentie bij het genereren van migraties.
|
||||
|
||||
```
|
||||
# Crewli Database Schema
|
||||
# Versie: 1.3 | Gegenereerd vanuit Design Document
|
||||
|
||||
## Regels
|
||||
- Primaire sleutels: ULID via HasUlids (nooit UUID v4)
|
||||
- Soft delete: zie lijst per tabel hieronder
|
||||
- JSON kolommen: alleen voor opaque config
|
||||
|
||||
## Tabellen
|
||||
|
||||
### users
|
||||
- id (ulid, PK)
|
||||
- name (string)
|
||||
- email (string, unique)
|
||||
- password (string)
|
||||
- timezone (string, default: Europe/Amsterdam)
|
||||
- locale (string, default: nl)
|
||||
- avatar (string, nullable)
|
||||
- email_verified_at (timestamp, nullable)
|
||||
- deleted_at (timestamp, nullable) -- soft delete
|
||||
|
||||
### organisations
|
||||
- id (ulid, PK)
|
||||
- name (string)
|
||||
- slug (string, unique)
|
||||
- billing_status (enum: trial|active|suspended|cancelled, default: trial)
|
||||
- settings (json, nullable) -- UI display prefs only
|
||||
- deleted_at (timestamp, nullable)
|
||||
|
||||
# ... (volledig schema uit Design Document sectie 3.5)
|
||||
```
|
||||
|
||||
### 3.4 docs/API.md — API contract
|
||||
|
||||
Een simpele route-lijst die Claude Code gebruikt als referentie bij het genereren van controllers en Vue composables.
|
||||
|
||||
```
|
||||
# Crewli API Contract
|
||||
# Base: /api/v1/
|
||||
# Auth: Bearer token (Sanctum)
|
||||
|
||||
## Auth
|
||||
POST /auth/login
|
||||
POST /auth/logout
|
||||
GET /auth/me
|
||||
|
||||
## Organisations
|
||||
GET /organisations -- lijst (super admin)
|
||||
POST /organisations -- aanmaken
|
||||
GET /organisations/{org} -- detail
|
||||
PUT /organisations/{org} -- bijwerken
|
||||
GET /organisations/{org}/members -- leden
|
||||
POST /organisations/{org}/invite -- uitnodigen
|
||||
|
||||
## Events
|
||||
GET /organisations/{org}/events
|
||||
POST /organisations/{org}/events
|
||||
GET /organisations/{org}/events/{event}
|
||||
PUT /organisations/{org}/events/{event}
|
||||
|
||||
## Festival Sections
|
||||
GET /events/{event}/sections
|
||||
POST /events/{event}/sections
|
||||
GET /events/{event}/sections/{section}
|
||||
|
||||
## Time Slots
|
||||
GET /events/{event}/time-slots
|
||||
POST /events/{event}/time-slots
|
||||
|
||||
## Shifts
|
||||
GET /events/{event}/sections/{section}/shifts
|
||||
POST /events/{event}/sections/{section}/shifts
|
||||
PUT /events/{event}/sections/{section}/shifts/{shift}
|
||||
POST /events/{event}/sections/{section}/shifts/{shift}/assign
|
||||
POST /events/{event}/sections/{section}/shifts/{shift}/claim
|
||||
|
||||
## Persons
|
||||
GET /events/{event}/persons
|
||||
POST /events/{event}/persons
|
||||
GET /events/{event}/persons/{person}
|
||||
PUT /events/{event}/persons/{person}
|
||||
POST /events/{event}/persons/{person}/approve
|
||||
|
||||
# ... (volledig API contract uitbreiden per module)
|
||||
```
|
||||
|
||||
## 4. Development Workflow
|
||||
|
||||
Hoe je van leeg project naar werkende feature gaat
|
||||
|
||||
Elke feature volgt dezelfde drielaagse workflow. Commit altijd per voltooide laag — nooit halfafgebouwde code in main.
|
||||
|
||||
| **Laag** | **Wat je doet en met welk tool** |
|
||||
|----|----|
|
||||
| Laag 1 — Backend (API) | Claude Code genereert: migratie + model + factory + policy + form request + resource + controller + test. Jij reviewt en draait tests. |
|
||||
| Laag 2 — Frontend (Vue) | Claude Code genereert: composable + Pinia store + Vue pagina + router entry. Cursor verfijnt de UI met Vuexy componenten. |
|
||||
| Laag 3 — Integration | Cursor: verbind frontend met backend. Test end-to-end. Fix type-errors. Review mobile weergave. |
|
||||
|
||||
### 4.1 De Module-generatie volgorde
|
||||
|
||||
Altijd in deze volgorde. Nooit stappen overslaan — later toevoegen kost meer tijd dan nu correct doen.
|
||||
|
||||
| **Stap** | **Commando / Actie** |
|
||||
|----|----|
|
||||
| 1. Migratie | php artisan make:migration create_shifts_table |
|
||||
| 2. Model | php artisan make:model Shift -mfp (migration + factory + policy) |
|
||||
| 3. Form Request | php artisan make:request StoreShiftRequest + UpdateShiftRequest |
|
||||
| 4. API Resource | php artisan make:resource ShiftResource + ShiftCollection |
|
||||
| 5. Controller | php artisan make:controller Api/V1/ShiftController --api |
|
||||
| 6. Registreer routes | In api/routes/api.php toevoegen |
|
||||
| 7. Test | php artisan make:test ShiftControllerTest + draaien |
|
||||
| 8. Composable | apps/app/src/composables/api/useShifts.ts aanmaken |
|
||||
| 9. Store (indien nodig) | apps/app/src/stores/useShiftStore.ts |
|
||||
| 10. Vue pagina | apps/app/src/pages/sections/[id]/shifts.vue |
|
||||
| 11. Route | apps/app/src/router/index.ts |
|
||||
|
||||
### 4.2 Fase-planning: wat bouw je wanneer
|
||||
|
||||
| **Fase** | **Inhoud** |
|
||||
|----|----|
|
||||
| Fase 1 — Foundation (nu) | Auth (login/logout/me), Organisations CRUD, Events CRUD, User invitations, Multi-tenant scope, Roles & permissions setup, Basis dashboard shell |
|
||||
| Fase 2 — Core Operations | Persons & Crowd Types, Festival Sections + Time Slots + Shifts, Shift claiming + goedkeuring, Vrijwilligers registratie + portaal, Accreditatie engine, Basis briefings |
|
||||
| Fase 3 — Advancing & Show Day | Artist advancing + portaal, Timetable, Mission Control, Formulierbouwer, Post-festival evaluatie, PDF allocatiesheet, Campagnes (email + WhatsApp via Zender) |
|
||||
| Fase 4 — Differentiators | Real-time WebSockets, Show Day Mode, Vrijwilligersprofiel + festival-paspoort, Shift swap & wachtlijst, Retrospectief rapport, Leveranciersportaal uitgebreid |
|
||||
|
||||
## Gedeelde frontend packages
|
||||
|
||||
`apps/portal/` en `apps/app/` delen een klein aantal schema-gedreven modules. Deze sectie legt vast wat wel en niet in die gedeelde laag hoort, en hoe je de aliases opzet.
|
||||
|
||||
### Locatie
|
||||
|
||||
`packages/form-schema/` — pure TypeScript, **geen** `package.json`, alias-only. Geen npm-package en geen pnpm workspace member; puur geconsumeerd via per-app TypeScript- en Vite-path-aliases.
|
||||
|
||||
### Wat hoort erin
|
||||
|
||||
- Schema-types die backend PHP-enums en API-resources mirroren (bv. `PublicFormField`, `FormFieldType`, `PublicFormSchema`)
|
||||
- Schema-gedreven gedrag: validation rule builders, conditional-logic evaluator, step-navigation, stored-value formatters
|
||||
- Alles wat pure TypeScript is en in portal én app exact dezelfde output moet produceren
|
||||
|
||||
### Wat hoort er NIET in
|
||||
|
||||
- Vue SFCs (`.vue` bestanden) — elke app rendert zijn eigen UI
|
||||
- Vuetify/Vuexy-specifieke code, theming of styling
|
||||
- Vue Router-code, route-definities of navigation guards
|
||||
- Pinia stores of app-level state
|
||||
- Axios-instanties of API-client code
|
||||
- Composables die afhangen van app-runtime (auth, i18n, router)
|
||||
|
||||
### Rationale
|
||||
|
||||
Portal is submit-facing, app is builder- en review-facing. De twee apps zullen visueel uit elkaar groeien naarmate product-requirements verschillen. Gedeelde Vue-componenten zouden hun stijlen aan elkaar koppelen — daarom delen we het *contract en de logica* (schema-types, gedrag) maar laat elke app zijn eigen UI bezitten.
|
||||
|
||||
### Alias-setup (verplicht op drie plekken per app)
|
||||
|
||||
Beide apps moeten `@form-schema/*` registreren in álle drie:
|
||||
|
||||
- `apps/<app>/tsconfig.json` — `compilerOptions.paths`
|
||||
- `apps/<app>/vite.config.ts` — `resolve.alias`
|
||||
- `apps/<app>/vitest.config.ts` — `resolve.alias`
|
||||
|
||||
Alle drie zijn nodig omdat `vitest.config.ts` niet overerft van `vite.config.ts`. Eén van de drie vergeten breekt óf de productie-build, óf de dev-server, óf de test-suite. Wanneer een derde app consumer wordt, wire dan alle drie tegelijk.
|
||||
|
||||
### Vue-resolutie (dependency-note)
|
||||
|
||||
Het gedeelde package heeft geen `package.json` en kan dus geen eigen dependencies declareren. Om `vue` resolvable te maken tijdens TypeScript-compilatie vanuit `packages/form-schema/src/`, voegen beide apps een `"vue": ["./node_modules/vue"]`-entry toe aan `compilerOptions.paths`. Dit is het minimum; als het package ooit niet-triviale externe imports krijgt (bv. `@vueuse/core`, `lodash`), voeg dan per app een vergelijkbare paths-entry toe — níet een `package.json` in het package — of promoveer het package tot workspace-member.
|
||||
|
||||
## 5. Prompt Bibliotheek
|
||||
|
||||
Kant-en-klare prompts voor elke ontwikkelstap
|
||||
|
||||
Gebruik deze prompts letterlijk of als basis. De meest effectieve prompts zijn: specifiek, contextueel en taak-gebaseerd. Verwijs altijd naar de docs/ bestanden die je hebt aangemaakt.
|
||||
|
||||
### 5.1 Kickstart prompts
|
||||
|
||||
> **Fase 1 kickstart — Alles genereren in een sweep**
|
||||
>
|
||||
> Lees /resources/design/design-document.md sectie 3.5 (schema) en /CLAUDE.md.
|
||||
>
|
||||
> Genereer alle Fase 1 componenten in de juiste volgorde:
|
||||
>
|
||||
> 1. Migrations voor: users (update), organisations, organisation_user, user_invitations, events, event_user_roles
|
||||
> 2. Eloquent modellen met HasUlids, relaties, OrganisationScope global scope waar van toepassing
|
||||
> 3. Factories met realistic test data
|
||||
> 4. Spatie Permission seeder: maak rollen aan (super_admin, org_admin, org_member, event_manager, staff_coordinator, volunteer_coordinator)
|
||||
> 5. Auth controller (login/logout/me) met Sanctum
|
||||
> 6. Organisations controller (CRUD) met Policy en Feature Test
|
||||
> 7. Events controller (CRUD) met Policy en Feature Test
|
||||
>
|
||||
> Draai na elke stap: php artisan test. Los fouten op voor je verder gaat.
|
||||
|
||||
> **Module genereren — Shifts als voorbeeld**
|
||||
>
|
||||
> Lees /CLAUDE.md en /dev-docs/SCHEMA.md voor de shifts tabel definitie.
|
||||
>
|
||||
> Bouw het volledige Shifts module in de volgorde uit CLAUDE.md sectie 'Volgorde bij elke nieuwe module'.
|
||||
>
|
||||
> Specifieke eisen voor Shifts:
|
||||
>
|
||||
> - time_slot_id MOET gedenormaliseerd worden in shift_assignments voor de UNIQUE(person_id, time_slot_id) constraint
|
||||
> - ShiftAssignment heeft een status machine: pending_approval > approved/rejected/cancelled/completed
|
||||
> - Auto-approve is configureerbaar per shift (auto_approved bool op shift niveau)
|
||||
> - Bij approve: stuur notificatie naar vrijwilliger (queued job, gebruik ZenderService voor WhatsApp)
|
||||
> - ShiftResource moet slots_filled (count van approved assignments) en fill_rate (percentage) berekend teruggeven
|
||||
>
|
||||
> Eindig met: php artisan test --filter=Shift
|
||||
|
||||
### 5.2 Backend prompts
|
||||
|
||||
> **Migration genereren**
|
||||
>
|
||||
> Genereer een Laravel migratie voor de tabel [TABELNAAM] op basis van /dev-docs/SCHEMA.md.
|
||||
>
|
||||
> Gebruik $table->ulid('id')->primary() als PK.
|
||||
>
|
||||
> Voeg alle indexes toe zoals gedocumenteerd (composite indexes, unique constraints).
|
||||
>
|
||||
> Voeg timestamps() en softDeletes() toe indien van toepassing per CLAUDE.md.
|
||||
>
|
||||
> Gebruik constrained() op alle foreign keys voor cascade-gedrag.
|
||||
|
||||
> **Model met alle features**
|
||||
>
|
||||
> Genereer het Eloquent model voor [MODELNAAM].
|
||||
>
|
||||
> Gebruik: HasUlids, HasFactory, SoftDeletes (indien van toepassing).
|
||||
>
|
||||
> Voeg toe: OrganisationScope global scope, alle relaties (hasMany, belongsTo, belongsToMany),
|
||||
> computed accessors (fill_rate, available_slots), status-gerelateerde scopes (scopePending, scopeApproved),
|
||||
> en $fillable of $guarded array.
|
||||
>
|
||||
> Schrijf ook de factory met realistic Nederlandse testdata.
|
||||
|
||||
> **API Resource met computed velden**
|
||||
>
|
||||
> Genereer een Laravel API Resource voor [MODELNAAM].
|
||||
>
|
||||
> Voeg toe: alle relevante velden, computed velden (fill_rate, status_label),
|
||||
> conditioneel geladen relaties (whenLoaded), en wanneer van toepassing: when() voor permissie-afhankelijke velden.
|
||||
>
|
||||
> De Resource mag NOOIT model-attributen direct weggeven zonder transformatie.
|
||||
|
||||
> **Feature test schrijven**
|
||||
>
|
||||
> Schrijf een PHPUnit Feature Test voor [CONTROLLERNAAM].
|
||||
>
|
||||
> Dek minimaal af: index (200), show (200), store (201), update (200), destroy (204),
|
||||
> unauthenticated (401 op alle routes), wrong organisation (403), validatiefouten (422).
|
||||
>
|
||||
> Gebruik RefreshDatabase, ActingAs met correcte rol via Spatie Permission.
|
||||
>
|
||||
> Maak test data via factories — nooit hardcoded IDs.
|
||||
|
||||
> **ZenderService aanmaken (WhatsApp/SMS)**
|
||||
>
|
||||
> Maak app/Services/ZenderService.php aan.
|
||||
>
|
||||
> Zender is een self-hosted SMS/WhatsApp gateway (CodeCanyon product).
|
||||
>
|
||||
> Config: ZENDER_API_URL en ZENDER_API_KEY uit .env.
|
||||
>
|
||||
> Methoden: sendSms(string $to, string $message): bool
|
||||
> sendWhatsApp(string $to, string $message): bool
|
||||
> sendByUrgency(string $to, string $message, string $urgency): bool
|
||||
>
|
||||
> urgency: normal=email only, urgent=whatsapp, emergency=sms+whatsapp parallel
|
||||
>
|
||||
> Gebruik Laravel HTTP Client (Http::). Log alle sends via activitylog.
|
||||
>
|
||||
> Schrijf ook een ZenderServiceTest met HTTP fake.
|
||||
|
||||
### 5.3 Frontend prompts
|
||||
|
||||
> **Vue pagina voor een lijst-overzicht**
|
||||
>
|
||||
> Maak apps/app/src/pages/[module]/index.vue.
|
||||
>
|
||||
> Gebruik \<script setup lang='ts'\>.
|
||||
>
|
||||
> API calls via useQuery() uit TanStack Query — niet direct axios.
|
||||
>
|
||||
> Tabel via VDataTable van Vuetify — niet custom HTML.
|
||||
>
|
||||
> Bovenaan: status KPI-tiles (totaal, goedgekeurd, pending) als klikbare VCard componenten.
|
||||
>
|
||||
> Rij klik: opent een side panel (niet navigeert naar nieuwe pagina) met detail-informatie.
|
||||
>
|
||||
> Loading state: VSkeleton loader. Error state: VAlert met retry knop.
|
||||
>
|
||||
> Mobiel: tabel collapst naar een VList op viewport < 768px.
|
||||
|
||||
> **Composable voor API calls**
|
||||
>
|
||||
> Maak apps/app/src/composables/api/use[Module].ts.
|
||||
>
|
||||
> Exporteer: use[Module]List (useQuery), use[Module]Detail (useQuery met id param),
|
||||
> useCreate[Module] (useMutation), useUpdate[Module] (useMutation), useDelete[Module] (useMutation).
|
||||
>
|
||||
> Gebruik axios via de centrale api.ts instance (met Sanctum CSRF en auth header).
|
||||
>
|
||||
> Mutations invalideren automatisch de relevante query keys na succes.
|
||||
>
|
||||
> Alle response types volledig getypeerd via TypeScript interfaces in types/[module].ts.
|
||||
|
||||
> **Pinia store aanmaken**
|
||||
>
|
||||
> Maak apps/app/src/stores/use[Module]Store.ts.
|
||||
>
|
||||
> Gebruik defineStore met Setup syntax (niet Options syntax).
|
||||
>
|
||||
> Sla op: geselecteerde IDs, UI state (open sidepanel, actief tab), filters.
|
||||
>
|
||||
> NIET in Pinia: server data (dat zit in TanStack Query). Pinia is alleen voor UI state.
|
||||
>
|
||||
> Exporteer: alle state als readonly via storeToRefs.
|
||||
|
||||
> **Shift claim workflow — volledig end-to-end**
|
||||
>
|
||||
> Bouw de volledige shift claim workflow:
|
||||
>
|
||||
> Backend: POST /shifts/{shift}/claim endpoint in ShiftController.
|
||||
>
|
||||
> - Valideer: shift heeft slots_open_for_claiming beschikbaar
|
||||
> - Valideer: geen bestaande approved assignment voor zelfde time_slot_id voor deze person
|
||||
> - Maak ShiftAssignment aan met status=pending_approval
|
||||
> - Dispatch NotifyCoordinatorOfClaimJob (queued)
|
||||
> - Return ShiftAssignmentResource
|
||||
>
|
||||
> Frontend: 'Claim' knop in portal/shifts/index.vue.
|
||||
>
|
||||
> - Disable knop als conflict of geen slots beschikbaar
|
||||
> - Na claim: toon 'Wachten op goedkeuring' badge
|
||||
> - Optioneel: 'Op wachtlijst' knop als shift vol is
|
||||
|
||||
### 5.4 Agent-aanstuurprompts
|
||||
|
||||
> **Grote module — een prompt voor alles**
|
||||
>
|
||||
> Je bent een senior fullstack developer die werkt aan Crewli. Lees /CLAUDE.md volledig.
|
||||
>
|
||||
> Bouw het volledige [MODULE] module:
|
||||
>
|
||||
> Backend (in volgorde):
|
||||
>
|
||||
> 1. Migrations voor alle tabellen uit /dev-docs/SCHEMA.md sectie [X.X]
|
||||
> 2. Models met alle relaties, scopes en accessors
|
||||
> 3. Factories
|
||||
> 4. Policies
|
||||
> 5. Form Requests
|
||||
> 6. API Resources
|
||||
> 7. Controllers
|
||||
> 8. Routes
|
||||
> 9. Feature tests — draai ze, los fouten op
|
||||
>
|
||||
> Frontend:
|
||||
>
|
||||
> 10. TypeScript types in apps/app/src/types/[module].ts
|
||||
> 11. Composables in apps/app/src/composables/api/
|
||||
> 12. Vue pagina's (lijst + detail side panel)
|
||||
> 13. Router entries
|
||||
>
|
||||
> Stop na elke laag en vraag bevestiging voor je doorgaat.
|
||||
>
|
||||
> Als een test faalt: los het op voor je verdergaat — nooit overslaan.
|
||||
|
||||
> **Bug fix prompt**
|
||||
>
|
||||
> Er is een probleem met [BESCHRIJVING VAN HET PROBLEEM].
|
||||
>
|
||||
> Relevante bestanden: [BESTANDSPADEN].
|
||||
>
|
||||
> Foutmelding: [PLAK EXACTE ERROR].
|
||||
>
|
||||
> Verwacht gedrag: [WAT ZOU ER MOETEN GEBEUREN].
|
||||
>
|
||||
> Analyseer de oorzaak, schrijf een failing test die het probleem reproduceert,
|
||||
> fix het probleem, en bevestig dat de test slaagt.
|
||||
|
||||
> **Code review prompt**
|
||||
>
|
||||
> Review de code in [BESTANDSPAD] als senior Laravel/Vue developer.
|
||||
>
|
||||
> Check specifiek op:
|
||||
>
|
||||
> - Multi-tenancy: wordt organisation_id correct gescopeerd?
|
||||
> - Security: worden Policies gebruikt? Geen directe role-checks?
|
||||
> - Performance: ontbrekende eager loading (N+1), ontbrekende indexes?
|
||||
> - Conventies: volgt het CLAUDE.md regels?
|
||||
> - Types: zijn alle TypeScript types volledig (geen any)?
|
||||
>
|
||||
> Geef concrete verbeterpunten met codevoorbeelden.
|
||||
|
||||
## 6. Agents — Autonome Ontwikkeling
|
||||
|
||||
Hoe je Claude Code en Cursor agents effectief inzet
|
||||
|
||||
### 6.1 Claude Code als Agent
|
||||
|
||||
Claude Code kan volledig autonoom werken: bestanden lezen, aanmaken, aanpassen, tests draaien en fouten corrigeren — zonder dat jij elke stap bevestigt. Dit is het krachtigste en snelste werkmode.
|
||||
|
||||
| **Mode** | **Wanneer gebruiken** |
|
||||
|----|----|
|
||||
| Interactief (standaard) | Als je wil meekijken en goedkeuren. Claude Code stelt elke actie voor en wacht. Gebruik voor: eerste keer een module bouwen, complexe refactors. |
|
||||
| Autonoom (--dangerously-skip-permissions) | Als je een grote taak wil delegeren en wegloopt. Claude Code werkt door tot klaar. Gebruik voor: routine-modules die je eerder hebt gebouwd, test-driven fixes. |
|
||||
| Aanbevolen aanpak | Start autonoom voor scaffolding. Schakel naar interactief bij UI-componenten of business-logica die project-specifieke kennis vereist. |
|
||||
|
||||
**Claude Code opstarten**
|
||||
|
||||
```bash
|
||||
# Installeer Claude Code (eenmalig)
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Start in je project root
|
||||
cd /pad/naar/crewli
|
||||
claude
|
||||
|
||||
# Of: direct met een taak
|
||||
claude --print 'Genereer de Shift migration op basis van CLAUDE.md'
|
||||
|
||||
# Autonoom mode (voorzichtig gebruiken)
|
||||
claude --dangerously-skip-permissions
|
||||
```
|
||||
|
||||
### 6.2 Cursor Agent Mode
|
||||
|
||||
Cursor heeft een ingebouwde Agent mode die vergelijkbaar is met Claude Code maar geintegreerd in de IDE. Activeer via Cmd+Shift+P > 'Cursor: Open Agent'.
|
||||
|
||||
| **Feature** | **Gebruik** |
|
||||
|----|----|
|
||||
| @workspace | Geeft de agent toegang tot je hele codebase als context. Altijd meegeven bij module-niveau taken. |
|
||||
| @file | Verwijs naar een specifiek bestand. Bijv: @CLAUDE.md @api/app/Models/Shift.php |
|
||||
| @docs | Verwijs naar externe documentatie (Laravel docs, Vuetify docs). Cursor indexeert deze. |
|
||||
| Composer mode | Meerdere bestanden tegelijk bewerken. Ideaal voor: tegelijk model + controller + test aanpassen. |
|
||||
|
||||
### 6.3 De optimale agent-workflow per dag
|
||||
|
||||
> **DAGELIJKSE ROUTINE**
|
||||
>
|
||||
> Ochtend: Open Claude Code. Geef de taak voor die dag: 'Bouw het volledig Persons module op basis van CLAUDE.md en SCHEMA.md.' Laat autonoom draaien.
|
||||
>
|
||||
> Middag: Review de gegenereerde code in Cursor. Check: volgt het de conventies? Zijn de tests groen? Zijn de TypeScript types compleet?
|
||||
>
|
||||
> Namiddag: Cursor voor UI fijnafstelling, Vuexy componenten integratie, visuele correcties.
|
||||
>
|
||||
> Einde dag: php artisan test (alle tests groen). Commit alles met duidelijke commit messages.
|
||||
|
||||
### 6.4 Context window management
|
||||
|
||||
Claude Code heeft een beperkt context window. Bij grote taken verliest het de context van eerdere bestanden. Beheer dit proactief:
|
||||
|
||||
- Begin elke nieuwe sessie met: 'Lees /CLAUDE.md voor je begint.'
|
||||
|
||||
- Verwijs expliciet naar relevante bestanden: 'Zie /dev-docs/SCHEMA.md voor de tabel definitie.'
|
||||
|
||||
- Splits grote modules: 'Bouw eerst alleen de backend (stappen 1-9). Stop dan.'
|
||||
|
||||
- Na een context-verlies: geef een samenvatting: 'We bouwen Crewli. Tot nu toe klaar: auth, organisations, events. Nu: Shifts module backend.'
|
||||
|
||||
- Gebruik /dev-docs/API.md als levend document — houd bij wat er al gebouwd is.
|
||||
|
||||
## 7. Tips, Valkuilen & Best Practices
|
||||
|
||||
Geleerd van ervaring — lees dit voordat je begint
|
||||
|
||||
### 7.1 De grootste tijdverspillers
|
||||
|
||||
| **Valkuil** | **Oplossing** |
|
||||
|----|----|
|
||||
| Te brede prompts: 'Bouw de hele app' | Altijd per module. Per module maximaal 1 laag tegelijk. Breed = vaag = slechte output. |
|
||||
| CLAUDE.md niet up-to-date houden | Na elke architectuurbeslissing: update CLAUDE.md. Dit is je bron van waarheid. Verouderde regels leiden tot inconsistente code. |
|
||||
| Tests overslaan 'want het werkt toch' | Schrijf tests altijd. AI-gegenereerde code heeft subtiele bugs die pas later opduiken. Tests vangen dit vroeg. |
|
||||
| Alle gegenereerde code blindelings accepteren | Review altijd: check multi-tenancy scoping, check indexes, check error handling. AI mist soms subtiele business logica. |
|
||||
| Frontend en backend tegelijk bouwen | Backend eerst, compleet en getest. Dan frontend. Nooit parallel — je verliest overzicht. |
|
||||
| Geen versiecontrole per module | Commit na elke voltooide module (backend + frontend + tests). Kleine commits = makkelijk terugdraaien. |
|
||||
|
||||
### 7.2 Prompts die altijd goed werken
|
||||
|
||||
- **Geef altijd context: 'Crewli is een multi-tenant SaaS voor festival-organisatie...'**
|
||||
|
||||
- **Verwijs naar bestanden: 'Op basis van CLAUDE.md en SCHEMA.md...'**
|
||||
|
||||
- **Specificeer de output: 'Genereer X, Y en Z. Niets anders.'**
|
||||
|
||||
- **Vraag om uitleg: 'Leg uit waarom je deze aanpak kiest voor de shift conflict-check.'**
|
||||
|
||||
- **Gebruik iteratief: 'Dit klopt niet omdat... Pas aan en draai tests opnieuw.'**
|
||||
|
||||
### 7.3 Kwaliteitscontrole checklist
|
||||
|
||||
Gebruik dit als checklist voor elke voltooide module voordat je verder gaat:
|
||||
|
||||
| **Check** | **Wat je controleert** |
|
||||
|----|----|
|
||||
| Tests | php artisan test draait groen. Minimaal: 200, 401, 403 per endpoint. |
|
||||
| Multi-tenancy | Elke query heeft organisation_id scope. Controleer via tinker. |
|
||||
| N+1 queries | Gebruik Laravel Debugbar of query logging. Geen N+1 in lijst-endpoints. |
|
||||
| TypeScript | Geen 'any' types in Vue composables en components. npx tsc --noEmit groen. |
|
||||
| Mobile | Pagina is bruikbaar op 375px. Open Chrome DevTools en check. |
|
||||
| Error states | Wat ziet een gebruiker bij: lege lijst, API fout, netwerk timeout? |
|
||||
| CLAUDE.md | Geen verboden patronen (Model::all, hardcoded roles, UUID v4). |
|
||||
|
||||
### 7.4 Handige Laravel commando's
|
||||
|
||||
```bash
|
||||
# Alles in een keer voor een nieuwe model
|
||||
php artisan make:model Shift -a # model + migration + factory + seeder + policy + controller + resource
|
||||
|
||||
# Tests draaien
|
||||
php artisan test # alle tests
|
||||
php artisan test --filter=ShiftTest # specifieke test class
|
||||
php artisan test --coverage # met coverage rapport
|
||||
|
||||
# Database
|
||||
php artisan migrate:fresh --seed # reset + migreer + seed
|
||||
php artisan tinker # REPL voor quick checks
|
||||
|
||||
# Routes inspecteren
|
||||
php artisan route:list --path=api/v1 # alle API routes
|
||||
|
||||
# Queue (voor briefings, notificaties)
|
||||
php artisan queue:work --queue=notifications,briefings,default
|
||||
```
|
||||
|
||||
### 7.5 Eerste dag: exacte volgorde
|
||||
|
||||
| **#** | **Actie** |
|
||||
|----|----|
|
||||
| 1 | Repository structuur controleren (sectie 2, stap 01) |
|
||||
| 2 | Dependencies installeren (sectie 2, stap 02) |
|
||||
| 3 | CLAUDE.md aanmaken en invullen (sectie 3.1) |
|
||||
| 4 | .cursorrules aanmaken (sectie 3.2) |
|
||||
| 5 | docs/SCHEMA.md aanmaken met volledig schema uit design document |
|
||||
| 6 | docs/API.md aanmaken met initiiele routes |
|
||||
| 7 | Claude Code starten: 'Lees CLAUDE.md. Daarna: genereer Fase 1 — auth, organisations, events.' |
|
||||
| 8 | Tests draaien: php artisan test — los fouten op |
|
||||
| 9 | Commit: 'feat: fase 1 foundation — auth, organisations, events' |
|
||||
| 10 | Morgen: Fase 2 starten met Persons & Crowd Types |
|
||||
|
||||
---
|
||||
|
||||
Crewli Development Guide v1.0 — Maart 2026
|
||||
Reference in New Issue
Block a user