chore: align migrations, docs, and frontends with crewli.app setup

- Replace dated migrations with ordered 2026_04_07_* chain; fold users update into base migration
- Update OrganisationScope, AppServiceProvider, seeders, api routes, and .env.example
- Refresh Cursor rules, CLAUDE.md, Makefile, README, and docs (API, SCHEMA, SETUP)
- Adjust admin/app/portal HTML, packages, api-client, events types, and theme config
- Update docker-compose and VS Code settings; remove stray Office lock files from resources

Made-with: Cursor
This commit is contained in:
2026-04-07 10:45:34 +02:00
parent 5e2ede14b4
commit fda161ee09
53 changed files with 355 additions and 446 deletions

View File

@@ -1,7 +1,7 @@
# EventCrew - Architecture # Crewli - Architecture
> Multi-tenant SaaS platform for event- and festival management. > Multi-tenant SaaS platform for event- and festival management.
> Source of truth: `/resources/design/EventCrew_Design_Document_v1.3.docx` > Source of truth: `/resources/design/Crewli_Design_Document_v1.3.docx`
## System Overview ## System Overview
@@ -432,6 +432,8 @@ Three frontend origins in `config/cors.php` (via env):
| App | `http://localhost:5174` | `FRONTEND_APP_URL` | | App | `http://localhost:5174` | `FRONTEND_APP_URL` |
| Portal | `http://localhost:5175` | `FRONTEND_PORTAL_URL` | | Portal | `http://localhost:5175` | `FRONTEND_PORTAL_URL` |
Production (registered domain **crewli.app**): API `https://api.crewli.app` (`APP_URL`); SPAs `https://admin.crewli.app`, `https://app.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. `admin.crewli.app,app.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) ## Real-time Events (WebSocket)
@@ -446,4 +448,4 @@ Via Laravel Echo + Pusher/Soketi:
--- ---
*Source: EventCrew Design Document v1.3, March 2026* *Source: Crewli Design Document v1.3, March 2026*

View File

@@ -1,19 +1,19 @@
# EventCrew - Cursor AI Instructions # Crewli - Cursor AI Instructions
> Multi-tenant SaaS platform for event- and festival management. > Multi-tenant SaaS platform for event- and festival management.
> Design Document: `/resources/design/EventCrew_Design_Document_v1.3.docx` > Design Document: `/resources/design/Crewli_Design_Document_v1.3.docx`
> Dev Guide: `/resources/design/EventCrew_Dev_Guide_v1.0.docx` > Dev Guide: `/resources/design/Crewli_Dev_Guide_v1.0.docx`
> Start Guide: `/resources/design/EventCrew_Start_Guide_v1.0.docx` > Start Guide: `/resources/design/Crewli_Start_Guide_v1.0.docx`
## Project Overview ## Project Overview
**Name**: EventCrew **Name**: Crewli
**Type**: Multi-tenant SaaS platform (API-first architecture) **Type**: Multi-tenant SaaS platform (API-first architecture)
**Status**: Development **Status**: Development
### Description ### Description
EventCrew 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. 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 ## Quick Reference
@@ -130,7 +130,7 @@ Build auth flow in apps/app/:
1. stores/useAuthStore.ts - token storage, isAuthenticated, me() loading 1. stores/useAuthStore.ts - token storage, isAuthenticated, me() loading
2. pages/login.vue - use Vuexy login layout 2. pages/login.vue - use Vuexy login layout
3. Router guard - redirect to login if not authenticated 3. Router guard - redirect to login if not authenticated
4. Replace Vuexy demo navigation with EventCrew structure 4. Replace Vuexy demo navigation with Crewli structure
5. CASL permissions: connect to Spatie roles from auth/me response 5. CASL permissions: connect to Spatie roles from auth/me response
``` ```

View File

@@ -1,12 +1,12 @@
--- ---
description: Core workspace rules for EventCrew multi-tenant SaaS platform description: Core workspace rules for Crewli multi-tenant SaaS platform
globs: ["**/*"] globs: ["**/*"]
alwaysApply: true alwaysApply: true
--- ---
# Workspace Rules # Workspace Rules
You are an expert full-stack developer working on EventCrew, a multi-tenant SaaS platform for event and festival management. The backend is a Laravel 12 REST API (JSON only, no Blade), and three Vue 3 SPA frontends communicate via CORS + Sanctum tokens. You are an expert full-stack developer working on Crewli, a multi-tenant SaaS platform for event and festival management. The backend is a Laravel 12 REST API (JSON only, no Blade), and three Vue 3 SPA frontends communicate via CORS + Sanctum tokens.
## Tech Stack ## Tech Stack
@@ -36,7 +36,7 @@ You are an expert full-stack developer working on EventCrew, a multi-tenant SaaS
## Project Structure ## Project Structure
``` ```
event-crew/ crewli/
├── api/ # Laravel 12 REST API (JSON only) ├── api/ # Laravel 12 REST API (JSON only)
│ ├── app/ │ ├── app/
│ │ ├── Http/ │ │ ├── Http/
@@ -130,8 +130,19 @@ event-crew/
| Redis | `localhost:6379` | - | | Redis | `localhost:6379` | - |
| Mailpit | `http://localhost:8025` | - | | Mailpit | `http://localhost:8025` | - |
### Production URLs (crewli.app)
**Note:** `crewli.app` = this SaaS product. `crewli.nl` is for a future public marketing site only — never use `.nl` for API, SPAs, or app-originated mail in this project.
| Service | URL | Env variable |
|---------|-----|--------------|
| API | `https://api.crewli.app` | `APP_URL` |
| Admin SPA | `https://admin.crewli.app` | `FRONTEND_ADMIN_URL` |
| Organizer SPA | `https://app.crewli.app` | `FRONTEND_APP_URL` |
| Portal SPA | `https://portal.crewli.app` | `FRONTEND_PORTAL_URL` |
### CORS ### CORS
Three frontend origins configured in `config/cors.php` via env variables. Each Vite dev server gets its own port for CORS isolation. Three frontend origins configured in `config/cors.php` via env variables. Each Vite dev server gets its own port for CORS isolation. In production, set the same env vars to the `https://…` origins above (see `api/.env.example`).
## Git Conventions ## Git Conventions

View File

@@ -1,5 +1,5 @@
--- ---
description: Laravel API development guidelines for EventCrew multi-tenant platform description: Laravel API development guidelines for Crewli multi-tenant platform
globs: ["api/**/*.php"] globs: ["api/**/*.php"]
alwaysApply: true alwaysApply: true
--- ---

View File

@@ -1,5 +1,5 @@
--- ---
description: Vue 3, TypeScript, and Vuexy patterns for EventCrew platform description: Vue 3, TypeScript, and Vuexy patterns for Crewli platform
globs: ["apps/**/*.{vue,ts,tsx}"] globs: ["apps/**/*.{vue,ts,tsx}"]
alwaysApply: true alwaysApply: true
--- ---
@@ -23,7 +23,7 @@ alwaysApply: true
- Minimal modifications needed - Minimal modifications needed
### `apps/app/` (Organizer - Main App) ### `apps/app/` (Organizer - Main App)
- Sidebar nav customized for EventCrew structure - Sidebar nav customized for Crewli structure
- Remove Vuexy demo/customizer components - Remove Vuexy demo/customizer components
- Full Vuetify component usage - Full Vuetify component usage
- 90% of development work happens here - 90% of development work happens here

View File

@@ -1,5 +1,5 @@
--- ---
description: Multi-tenancy and portal architecture rules for EventCrew description: Multi-tenancy and portal architecture rules for Crewli
globs: ["api/**/*.php", "apps/portal/**/*.{vue,ts}"] globs: ["api/**/*.php", "apps/portal/**/*.{vue,ts}"]
alwaysApply: true alwaysApply: true
--- ---
@@ -101,7 +101,7 @@ Route::middleware(['auth:sanctum', 'event.role:event_manager'])->group(...);
### Token-Based Authentication Flow ### Token-Based Authentication Flow
``` ```
1. Artist/supplier receives email with link: https://portal.eventcrew.app/advance?token=01HQ3K... 1. Artist/supplier receives email with link: https://portal.crewli.app/advance?token=01HQ3K...
2. Portal detects token in URL query parameter 2. Portal detects token in URL query parameter
3. POST /api/v1/portal/token-auth { token: '01HQ3K...' } 3. POST /api/v1/portal/token-auth { token: '01HQ3K...' }
4. Backend validates token against artists.portal_token or production_requests.token 4. Backend validates token against artists.portal_token or production_requests.token
@@ -111,7 +111,7 @@ Route::middleware(['auth:sanctum', 'event.role:event_manager'])->group(...);
### Login-Based Authentication Flow ### Login-Based Authentication Flow
``` ```
1. Volunteer navigates to https://portal.eventcrew.app/login 1. Volunteer navigates to https://portal.crewli.app/login
2. Enters email + password 2. Enters email + password
3. POST /api/v1/auth/login (same endpoint as apps/app/) 3. POST /api/v1/auth/login (same endpoint as apps/app/)
4. Returns user + organisations + event roles 4. Returns user + organisations + event roles
@@ -196,6 +196,8 @@ class PortalTokenMiddleware
'supports_credentials' => true, 'supports_credentials' => true,
``` ```
Production example (subdomains on **crewli.app**): `FRONTEND_ADMIN_URL=https://admin.crewli.app`, `FRONTEND_APP_URL=https://app.crewli.app`, `FRONTEND_PORTAL_URL=https://portal.crewli.app`, and `SANCTUM_STATEFUL_DOMAINS=admin.crewli.app,app.crewli.app,portal.crewli.app`.
## Shift Claiming & Approval Flow ## Shift Claiming & Approval Flow
### Three Assignment Strategies per Shift ### Three Assignment Strategies per Shift

View File

@@ -1,5 +1,5 @@
--- ---
description: Database schema conventions, ULID primary keys, JSON column rules, soft delete strategy, and index requirements for EventCrew description: Database schema conventions, ULID primary keys, JSON column rules, soft delete strategy, and index requirements for Crewli
globs: ["api/database/**/*.php", "api/app/Models/**/*.php"] globs: ["api/database/**/*.php", "api/app/Models/**/*.php"]
alwaysApply: true alwaysApply: true
--- ---

View File

@@ -1,5 +1,5 @@
--- ---
description: Testing standards for EventCrew multi-tenant platform description: Testing standards for Crewli multi-tenant platform
globs: ["**/tests/**", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/Test.php"] globs: ["**/tests/**", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/Test.php"]
alwaysApply: true alwaysApply: true
--- ---

View File

@@ -1,21 +1,21 @@
# EventCrew Cursor Rules # Crewli Cursor Rules
## Stack ## Stack
PHP 8.2 + Laravel 12 | TypeScript + Vue 3 + Vuexy/Vuetify | Pinia + TanStack Query PHP 8.2 + Laravel 12 | TypeScript + Vue 3 + Vuexy/Vuetify | Pinia + TanStack Query
## Laravel ## Laravel
- Resource Controllers, Form Requests, API Resources — altijd - Resource controllers, form requests, API resources — always
- HasUlids op business modellen, HasFactory, SoftDeletes waar gedocumenteerd - `HasUlids` on business models, `HasFactory`, `SoftDeletes` where documented
- Global Scope OrganisationScope op event-gerelateerde modellen - Global scope `OrganisationScope` on event-related models
- Policies voor autorisatie, nooit inline role checks - Policies for authorization — never inline role checks
## Vue 3 ## Vue 3
- <script setup lang='ts'> altijd - `<script setup lang="ts">` always
- TanStack Query voor API state, Pinia voor UI state - TanStack Query for API state, Pinia for UI state
- Vuetify componenten eerst, custom CSS als laatste redmiddel - Vuetify components first; custom CSS only as a last resort
## Naamgeving ## Naming
- snake_case DB | camelCase JS | PascalCase Vue | use* composables | use*Store Pinia - snake_case DB | camelCase JS | PascalCase Vue | `use*` composables | `use*Store` Pinia
## Tests ## Tests
- PHPUnit Feature Test per controller, minimaal: 200 + 401 + 403 - PHPUnit feature test per controller, minimum: 200 + 401 + 403

View File

@@ -31,7 +31,7 @@
"eslint.workingDirectories": [ "eslint.workingDirectories": [
{ "directory": "apps/admin", "changeProcessCWD": true }, { "directory": "apps/admin", "changeProcessCWD": true },
{ "directory": "apps/band", "changeProcessCWD": true }, { "directory": "apps/app", "changeProcessCWD": true },
{ "directory": "apps/customers", "changeProcessCWD": true } { "directory": "apps/portal", "changeProcessCWD": true }
] ]
} }

171
CLAUDE.md
View File

@@ -1,129 +1,132 @@
# EventCrew — Claude Code Instructies # Crewli — Claude Code Instructions
## Project Context ## Project context
EventCrew is een multi-tenant SaaS platform voor event- en festivalbeheer. Crewli is a multi-tenant SaaS platform for event and festival management.
Gebouwd voor een professionele vrijwilligersorganisatie, met SaaS-uitbreidingspotentieel. Built for a professional volunteer organisation, with potential to expand as SaaS.
Design Document: /docs/EventCrew_Design_Document_v1.3.docx Design document: `/resources/design/Crewli_Design_Document_v1.3.docx`
## Tech Stack ## Tech stack
- Backend: PHP 8.2+, Laravel 12, Sanctum, Spatie Permission, MySQL 8, Redis - Backend: PHP 8.2+, Laravel 12, Sanctum, Spatie Permission, MySQL 8, Redis
- Frontend: TypeScript, Vue 3 (Composition API), Vuexy/Vuetify, Pinia, TanStack Query - Frontend: TypeScript, Vue 3 (Composition API), Vuexy/Vuetify, Pinia, TanStack Query
- Testing: PHPUnit (backend), Vitest (frontend) - Testing: PHPUnit (backend), Vitest (frontend)
## Repository Structuur ## Repository layout
- api/ Laravel backend - `api/` Laravel backend
- apps/admin/ Super Admin SPA - `apps/admin/` Super Admin SPA
- apps/app/ Organizer SPA (hoofdapp) - `apps/app/` Organizer SPA (main product app)
- apps/portal/ Externe portals (vrijwilliger, artiest, leverancier) - `apps/portal/` External portal (volunteers, artists, suppliers, etc.)
## Apps & Portal Architectuur ## Apps and portal architecture
- apps/admin/ = Super Admin platformbeheer, organisaties aanmaken - `apps/admin/` Super Admin: platform management, creating organisations
- apps/app/ = Organizer event management per organisatie - `apps/app/` Organizer: event management per organisation
- apps/portal/ = Externe gebruikers — één app, twee toegangsmodi: - `apps/portal/` External users: one app, two access modes:
- Login-based (auth:sanctum): vrijwilligers, crew — persons met user_id - Login-based (`auth:sanctum`): volunteers, crew — persons with `user_id`
- Token-based (portal.token middleware): artiesten, leveranciers, pers — persons zonder user_id - Token-based (`portal.token` middleware): artists, suppliers, press — persons without `user_id`
### CORS ### CORS
Drie frontend origins configureren in zowel Laravel (config/cors.php via env) als Vite dev server proxy: Configure three frontend origins in both Laravel (`config/cors.php` via env) and the Vite dev server proxy:
- admin: localhost:5173
- app: localhost:5174
- portal: localhost:5175
## Backend Regels (STRIKT VOLGEN) - admin: `localhost:5173`
- app: `localhost:5174`
- portal: `localhost:5175`
**Production (`crewli.app`):** API `https://api.crewli.app`, SPAs `https://admin.crewli.app`, `https://app.crewli.app`, `https://portal.crewli.app` — see `api/.env.example` for `FRONTEND_*` and `SANCTUM_STATEFUL_DOMAINS`. **`crewli.nl`** is only for a future marketing site; this application stack uses **`crewli.app`** (not `.nl` for API, SPAs, or transactional mail).
## Backend rules (strict)
### Multi-tenancy ### Multi-tenancy
- ELKE query op event-data MOET scoperen op organisation_id - Every query on event data **must** scope on `organisation_id`
- Gebruik OrganisationScope als Eloquent Global Scope op alle event-gerelateerde modellen - Use `OrganisationScope` as an Eloquent global scope on all event-related models
- Nooit directe id-checks in controllers — gebruik altijd Policies - Never use raw ID checks in controllers — always use policies
### Controllers ### Controllers
- Gebruik Resource Controllers (index/show/store/update/destroy) - Use resource controllers (`index` / `show` / `store` / `update` / `destroy`)
- Namespace: App\Http\Controllers\Api\V1\ - Namespace: `App\Http\Controllers\Api\V1\`
- Alle responses via API Resources (nooit model-attributen direct teruggeven) - Return all responses through API resources (never return raw model attributes)
- Validatie via Form Requests (nooit inline validate()) - Validate with form requests (never inline `validate()`)
### Modellen ### Models
- Gebruik HasUlids trait op alle business-modellen (GEEN UUID v4) - Use the `HasUlids` trait on all business models (not UUID v4)
- Soft deletes op: Organisation, Event, FestivalSection, Shift, ShiftAssignment, Person, Artist - Soft deletes on: Organisation, Event, FestivalSection, Shift, ShiftAssignment, Person, Artist
- GEEN soft deletes op: CheckIn, BriefingSend, MessageReply, ShiftWaitlist (audit-records) - No soft deletes on: CheckIn, BriefingSend, MessageReply, ShiftWaitlist (audit records)
- JSON kolommen ALLEEN voor opaque configuratie — nooit voor queryable data - JSON columns **only** for opaque configuration — never for queryable/filterable data
### Database ### Database
- Primaire sleutels: ULID via HasUlids (niet UUID v4, niet auto-increment voor business tables) - Primary keys: ULID via `HasUlids` (not UUID v4, not auto-increment on business tables)
- Elke migratie in volgorde aanmaken: eerst foundation, dan afhankelijke tabellen - Create migrations in dependency order: foundation first, then dependent tables
- ALTIJD composite indexes toevoegen zoals gedocumenteerd in het design document sectie 3.5 - Always add composite indexes as documented in the design document (section 3.5)
### Rollen & Permissies ### Roles and permissions
- Gebruik Spatie laravel-permission - Use Spatie `laravel-permission`
- Check rollen via $user->hasRole() en Policies — nooit hardcoded role strings in controllers - Check roles via `$user->hasRole()` and policies — never hard-code role strings in controllers
- Drie niveaus: app (super_admin), organisatie (org_admin/org_member), event (event_manager etc.) - Three levels: app (`super_admin`), organisation (`org_admin` / `org_member`), event (`event_manager`, etc.)
### Testing ### Testing
- Schrijf PHPUnit Feature Tests per controller - Write PHPUnit feature tests per controller
- Minimaal per endpoint: happy path + unauthenticated (401) + wrong organisation (403) - Minimum per endpoint: happy path + unauthenticated (401) + wrong organisation (403)
- Gebruik factories voor alle test-data - Use factories for all test data
- Draai tests NA elke module: php artisan test --filter=ModuleNaam - After each module: `php artisan test --filter=ModuleName`
## Frontend Regels (STRIKT VOLGEN) ## Frontend rules (strict)
### Vue Componenten ### Vue components
- Altijd <script setup lang='ts'> — nooit Options API - Always `<script setup lang="ts">` — never the Options API
- Props altijd getypeerd met defineProps<{...}>() - Type props with `defineProps<{...}>()`
- Emits altijd gedeclareerd met defineEmits<{...}>() - Declare emits with `defineEmits<{...}>()`
### API Calls ### API calls
- Gebruik TanStack Query (useQuery / useMutation) voor ALLE API calls - Use TanStack Query (`useQuery` / `useMutation`) for **all** API calls
- Nooit direct axios in een component — altijd via een composable in composables/api/ - Never call axios directly from a component — always via a composable under `composables/`
- Pinia stores voor cross-component state — nooit prop drilling - Use Pinia stores for cross-component state — no prop drilling
### Naamgeving ### Naming
- DB kolommen: snake_case - DB columns: `snake_case`
- TypeScript/JS variabelen: camelCase - TypeScript / JS variables: `camelCase`
- Vue componenten: PascalCase (bijv. ShiftAssignPanel.vue) - Vue components: PascalCase (e.g. `ShiftAssignPanel.vue`)
- Composables: use-prefix (bijv. useShifts.ts) - Composables: `use` prefix (e.g. `useShifts.ts`)
- Pinia stores: use-suffix store (bijv. useEventStore.ts) - Pinia stores: `use` prefix + `Store` suffix (e.g. `useEventStore.ts`)
### UI ### UI
- Gebruik ALTIJD Vuexy/Vuetify componenten voor layout, forms, tabellen, dialogen - Always use Vuexy/Vuetify for layout, forms, tables, dialogs
- Nooit custom CSS schrijven als een Vuetify klasse bestaat - Do not write custom CSS when a Vuetify utility class exists
- Responsief: mobile-first, minimaal werkend op 375px breedte - Responsive: mobile-first, usable from 375px width
## Verboden Patronen ## Forbidden patterns
- NOOIT: $user->role === 'admin' (gebruik policies) - Never: `$user->role === 'admin'` (use policies)
- NOOIT: Model::all() zonder where-clausule (altijd scopen) - Never: `Model::all()` without a `where` clause (always scope)
- NOOIT: dd() of var_dump() achterlaten in code - Never: leave `dd()` or `var_dump()` in code
- NOOIT: .env waarden hardcoden in code - Never: hard-code `.env` values in code
- NOOIT: JSON kolommen gebruiken voor data waarop gefilterd wordt - Never: use JSON columns for data you need to filter on
- NOOIT: UUID v4 als primaire sleutel (gebruik HasUlids) - Never: UUID v4 as primary key (use `HasUlids`)
## Volgorde bij elke nieuwe module ## Order of work for each new module
1. Migratie(s) aanmaken en draaien 1. Create and run migration(s)
2. Eloquent Model met relaties, scopes en HasUlids 2. Eloquent model with relationships, scopes, and `HasUlids`
3. Factory voor test-data 3. Factory for test data
4. Policy voor autorisatie 4. Policy for authorization
5. Form Request(s) voor validatie 5. Form request(s) for validation
6. API Resource voor response transformatie 6. API resource for response shaping
7. Resource Controller 7. Resource controller
8. Routes registreren in api.php 8. Register routes in `api.php`
9. PHPUnit Feature Test schrijven en draaien 9. Write and run PHPUnit feature tests
10. Vue composable voor API calls (useModuleNaam.ts) 10. Vue composable for API calls (e.g. `useShifts.ts`)
11. Pinia store indien cross-component state nodig 11. Pinia store if cross-component state is needed
12. Vue pagina component 12. Vue page component
13. Route toevoegen in Vue Router 13. Add route in Vue Router

View File

@@ -9,7 +9,7 @@ NC := \033[0m
help: help:
@echo "" @echo ""
@echo "$(GREEN)╔══════════════════════════════════════════════════════════════╗$(NC)" @echo "$(GREEN)╔══════════════════════════════════════════════════════════════╗$(NC)"
@echo "$(GREEN)EVENT CREW - Development Commands ║$(NC)" @echo "$(GREEN) CREWLI - Development Commands $(NC)"
@echo "$(GREEN)╚══════════════════════════════════════════════════════════════╝$(NC)" @echo "$(GREEN)╚══════════════════════════════════════════════════════════════╝$(NC)"
@echo "" @echo ""
@echo " $(YELLOW)Services (Docker):$(NC)" @echo " $(YELLOW)Services (Docker):$(NC)"
@@ -33,7 +33,7 @@ services:
@docker compose up -d @docker compose up -d
@echo "" @echo ""
@echo "$(GREEN)Services:$(NC)" @echo "$(GREEN)Services:$(NC)"
@echo " $(CYAN)MySQL:$(NC) localhost:3306 (event_crew / secret)" @echo " $(CYAN)MySQL:$(NC) localhost:3306 (crewli / secret)"
@echo " $(CYAN)Redis:$(NC) localhost:6379" @echo " $(CYAN)Redis:$(NC) localhost:6379"
@echo " $(CYAN)Mailpit:$(NC) http://localhost:8025" @echo " $(CYAN)Mailpit:$(NC) http://localhost:8025"
@echo "" @echo ""
@@ -68,4 +68,4 @@ fresh:
@cd api && php artisan migrate:fresh --seed @cd api && php artisan migrate:fresh --seed
db-shell: db-shell:
@docker exec -it bm_mysql mysql -u event_crew -psecret event_crew @docker exec -it bm_mysql mysql -u crewli -psecret crewli

View File

@@ -1,10 +1,10 @@
# EventCrew # Crewli
Multi-tenant SaaS platform for **event and festival operations**: planning, people, accreditation, artist advancing, volunteer shifts, briefings, and show-day tooling (Mission Control). The backend is a **JSON-only** Laravel API; all user interfaces are **Vue 3** single-page apps. Multi-tenant SaaS platform for **event and festival operations**: planning, people, accreditation, artist advancing, volunteer shifts, briefings, and show-day tooling (Mission Control). The backend is a **JSON-only** Laravel API; all user interfaces are **Vue 3** single-page apps.
--- ---
## What EventCrew covers ## What Crewli covers
- **Organisations & events** — Multi-tenant data with organisation-scoped access; events move through a defined lifecycle (draft → published → registration → buildup → show day → teardown → closed). - **Organisations & events** — Multi-tenant data with organisation-scoped access; events move through a defined lifecycle (draft → published → registration → buildup → show day → teardown → closed).
- **Festival structure** — Sections, time slots, shifts, assignments, claiming/approval flows for volunteers and crew. - **Festival structure** — Sections, time slots, shifts, assignments, claiming/approval flows for volunteers and crew.
@@ -45,7 +45,7 @@ All apps talk to the API over **CORS** with **Laravel Sanctum** tokens.
## Project structure ## Project structure
``` ```
event-crew/ crewli/
├── api/ # Laravel 12 REST API (JSON only) ├── api/ # Laravel 12 REST API (JSON only)
├── apps/ ├── apps/
│ ├── admin/ # Super Admin SPA │ ├── admin/ # Super Admin SPA
@@ -94,7 +94,20 @@ Detailed setup: **[docs/SETUP.md](docs/SETUP.md)**.
| Portal | http://localhost:5175 | `FRONTEND_PORTAL_URL` | | Portal | http://localhost:5175 | `FRONTEND_PORTAL_URL` |
| Mailpit | http://localhost:8025 | Local mail capture | | Mailpit | http://localhost:8025 | Local mail capture |
Production hostnames are placeholders until you deploy; configure `config/cors.php` via `.env`. ### Production (crewli.app)
**Domains:** **`crewli.app`** is this product (API + organizer/admin/portal SPAs, transactional email from the app, seeds, etc.). **`crewli.nl`** is reserved for a future **public marketing site** only — do not point this codebases `APP_URL`, CORS, Sanctum, or app mail at `crewli.nl`.
Typical layout (configure the same values in `api/.env` — see `api/.env.example`):
| Service | URL | Env variable |
|---------|-----|----------------|
| API | `https://api.crewli.app` | `APP_URL` |
| Admin | `https://admin.crewli.app` | `FRONTEND_ADMIN_URL` |
| Organizer | `https://app.crewli.app` | `FRONTEND_APP_URL` |
| Portal | `https://portal.crewli.app` | `FRONTEND_PORTAL_URL` |
Frontends: set `VITE_API_URL=https://api.crewli.app/api/v1` in each apps env for production builds. `SANCTUM_STATEFUL_DOMAINS` must list the **hostnames only** of the three SPAs (e.g. `admin.crewli.app,app.crewli.app,portal.crewli.app`).
--- ---
@@ -118,7 +131,7 @@ make db-shell
| Resource | Contents | | Resource | Contents |
|----------|----------| |----------|----------|
| [resources/design/](resources/design/) | **Canonical product specs** — place design documents here (often Word). Referenced by `.cursor` as source of truth for features and data model. Expected names include `EventCrew_Design_Document_v1.3.docx`, `EventCrew_Dev_Guide_v1.0.docx`, `EventCrew_Start_Guide_v1.0.docx` (adjust versions if you update files). | | [resources/design/](resources/design/) | **Canonical product specs** — place design documents here (often Word). Referenced by `.cursor` as source of truth for features and data model. Expected names include `Crewli_Design_Document_v1.3.docx`, `Crewli_Dev_Guide_v1.0.docx`, `Crewli_Start_Guide_v1.0.docx` (adjust versions if you update files). |
| [.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/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/instructions.md](.cursor/instructions.md) | Quick reference, phased roadmap, module build order |
| [.cursor/rules/](.cursor/rules/) | Workspace, Laravel, Vue, testing conventions | | [.cursor/rules/](.cursor/rules/) | Workspace, Laravel, Vue, testing conventions |

View File

@@ -1,7 +1,8 @@
APP_NAME="Event Crew" APP_NAME="Crewli"
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
# Local API origin (no path suffix). Production: https://api.crewli.app
APP_URL=http://localhost:8000 APP_URL=http://localhost:8000
APP_LOCALE=en APP_LOCALE=en
@@ -20,8 +21,8 @@ LOG_LEVEL=debug
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_PORT=3306 DB_PORT=3306
DB_DATABASE=event_crew DB_DATABASE=crewli
DB_USERNAME=event_crew DB_USERNAME=crewli
DB_PASSWORD=secret DB_PASSWORD=secret
SESSION_DRIVER=database SESSION_DRIVER=database
@@ -47,11 +48,19 @@ MAIL_PORT=1025
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="noreply@eventcrew.nl" # App / transactional mail: use crewli.app. (crewli.nl = future marketing site only, not this stack.)
MAIL_FROM_ADDRESS="noreply@crewli.app"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
# CORS - Frontend SPAs # CORS + Sanctum — SPA origins (no trailing slash; must match the browser URL)
FRONTEND_URL=http://localhost:5173 FRONTEND_ADMIN_URL=http://localhost:5173
FRONTEND_APP_URL=http://localhost:5174
FRONTEND_PORTAL_URL=http://localhost:5175
SANCTUM_STATEFUL_DOMAINS=localhost:5173,localhost:5174,localhost:5175 SANCTUM_STATEFUL_DOMAINS=localhost:5173,localhost:5174,localhost:5175
VITE_APP_NAME="${APP_NAME}" # --- Production (crewli.app) — uncomment and adjust hostnames if you use this layout:
# APP_URL=https://api.crewli.app
# FRONTEND_ADMIN_URL=https://admin.crewli.app
# FRONTEND_APP_URL=https://app.crewli.app
# FRONTEND_PORTAL_URL=https://portal.crewli.app
# SANCTUM_STATEFUL_DOMAINS=admin.crewli.app,app.crewli.app,portal.crewli.app

View File

@@ -8,6 +8,12 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope; use Illuminate\Database\Eloquent\Scope;
/**
* Explicit organisation filter for queries ({@see Builder::withGlobalScope()}).
* Event data is currently scoped via the {@see Organisation} relationship and policies;
* when you add child models (persons, shifts, ), consider a global scope driven by
* the authenticated users active organisation (see `.cursor/rules/102_multi_tenancy.mdc`).
*/
final class OrganisationScope implements Scope final class OrganisationScope implements Scope
{ {
public function __construct( public function __construct(

View File

@@ -1,22 +1,18 @@
<?php <?php
declare(strict_types=1);
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
/**
* Register any application services.
*/
public function register(): void public function register(): void
{ {
// //
} }
/**
* Bootstrap any application services.
*/
public function boot(): void public function boot(): void
{ {
// //

View File

@@ -13,7 +13,7 @@ return [
| |
*/ */
'name' => env('APP_NAME', 'Laravel'), 'name' => env('APP_NAME', 'Crewli'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@@ -14,19 +14,14 @@ return new class extends Migration
$table->ulid('id')->primary(); $table->ulid('id')->primary();
$table->string('name'); $table->string('name');
$table->string('email')->unique(); $table->string('email')->unique();
$table->string('phone', 20)->nullable();
$table->text('bio')->nullable();
$table->json('instruments')->nullable();
$table->string('avatar_path')->nullable();
$table->enum('type', ['member', 'customer'])->default('member');
$table->enum('role', ['admin', 'booking_agent', 'music_manager', 'member'])->nullable();
$table->enum('status', ['active', 'inactive'])->default('active');
$table->timestamp('email_verified_at')->nullable(); $table->timestamp('email_verified_at')->nullable();
$table->string('password'); $table->string('password');
$table->string('timezone')->default('Europe/Amsterdam');
$table->string('locale')->default('nl');
$table->string('avatar')->nullable();
$table->rememberToken(); $table->rememberToken();
$table->timestamps(); $table->timestamps();
$table->softDeletes();
$table->index(['type', 'status']);
}); });
Schema::create('password_reset_tokens', function (Blueprint $table) { Schema::create('password_reset_tokens', function (Blueprint $table) {

View File

@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// Remove old band-management columns
$table->dropIndex(['type', 'status']);
$table->dropColumn(['phone', 'bio', 'instruments', 'avatar_path', 'type', 'role', 'status']);
});
Schema::table('users', function (Blueprint $table) {
// Add EventCrew columns per SCHEMA.md
$table->string('timezone')->default('Europe/Amsterdam')->after('password');
$table->string('locale')->default('nl')->after('timezone');
$table->string('avatar')->nullable()->after('locale');
$table->softDeletes();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropSoftDeletes();
$table->dropColumn(['timezone', 'locale', 'avatar']);
});
Schema::table('users', function (Blueprint $table) {
$table->string('phone', 20)->nullable();
$table->text('bio')->nullable();
$table->json('instruments')->nullable();
$table->string('avatar_path')->nullable();
$table->enum('type', ['member', 'customer'])->default('member');
$table->enum('role', ['admin', 'booking_agent', 'music_manager', 'member'])->nullable();
$table->enum('status', ['active', 'inactive'])->default('active');
$table->index(['type', 'status']);
});
}
};

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;

View File

@@ -17,7 +17,7 @@ class DatabaseSeeder extends Seeder
$admin = User::create([ $admin = User::create([
'name' => 'Super Admin', 'name' => 'Super Admin',
'email' => 'admin@eventcrew.nl', 'email' => 'admin@crewli.app',
'password' => Hash::make('password'), 'password' => Hash::make('password'),
]); ]);

View File

@@ -21,7 +21,7 @@ use Illuminate\Support\Facades\Route;
// Health check // Health check
Route::get('/', fn () => response()->json([ Route::get('/', fn () => response()->json([
'success' => true, 'success' => true,
'message' => 'EventCrew API v1', 'message' => 'Crewli API v1',
'timestamp' => now()->toIso8601String(), 'timestamp' => now()->toIso8601String(),
])); ]));

View File

@@ -20,7 +20,7 @@ pnpm install
```env ```env
VITE_API_URL=http://localhost:8000/api/v1 VITE_API_URL=http://localhost:8000/api/v1
VITE_APP_NAME="Event Crew Admin" VITE_APP_NAME="Crewli Admin"
``` ```
4. Start development: 4. Start development:
@@ -31,4 +31,6 @@ pnpm dev
## Port ## Port
Runs on http://localhost:5173 (Vite default) Runs on http://localhost:5173 (Vite default).
**Production:** point `VITE_API_URL` at your API, e.g. `https://api.crewli.app/api/v1`, with DNS/TLS for `admin.crewli.app` (and matching Laravel `FRONTEND_ADMIN_URL` / CORS / Sanctum settings — see repo `README.md` and `api/.env.example`).

View File

@@ -6,7 +6,7 @@
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Event Crew Admin</title> <title>Crewli Admin</title>
<link rel="stylesheet" type="text/css" href="/loader.css" /> <link rel="stylesheet" type="text/css" href="/loader.css" />
</head> </head>

View File

@@ -1,5 +1,5 @@
{ {
"name": "eventcrew-admin", "name": "crewli-admin",
"version": "9.5.0", "version": "9.5.0",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -5,7 +5,7 @@ import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
/** /**
* Single axios instance for the real Laravel API (VITE_API_URL). * Single axios instance for the real Laravel API (VITE_API_URL).
* Auth: Bearer token from cookie 'accessToken' (set by login). * Auth: Bearer token from cookie 'accessToken' (set by login).
* Use this for all event-crew API calls; useApi (composables/useApi) stays for Vuexy demo/mock endpoints. * Use this for all Crewli API calls; useApi (composables/useApi) stays for Vuexy demo/mock endpoints.
*/ */
const apiClient: AxiosInstance = axios.create({ const apiClient: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_URL, baseURL: import.meta.env.VITE_API_URL,

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useEvents } from '@/composables/useEvents' import { useEvents } from '@/composables/useEvents'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import type { EventCrewEventStatus, UpdateEventData } from '@/types/events' import type { CrewliEventStatus, UpdateEventData } from '@/types/events'
definePage({ definePage({
meta: { meta: {
@@ -17,7 +17,7 @@ const eventId = computed(() => route.params.id as string)
const formData = ref<UpdateEventData>({}) const formData = ref<UpdateEventData>({})
const statusOptions: { title: string; value: EventCrewEventStatus }[] = [ const statusOptions: { title: string; value: CrewliEventStatus }[] = [
{ title: 'Draft', value: 'draft' }, { title: 'Draft', value: 'draft' },
{ title: 'Published', value: 'published' }, { title: 'Published', value: 'published' },
{ title: 'Registration open', value: 'registration_open' }, { title: 'Registration open', value: 'registration_open' },

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useEvents } from '@/composables/useEvents' import { useEvents } from '@/composables/useEvents'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import type { CreateEventData, EventCrewEventStatus } from '@/types/events' import type { CreateEventData, CrewliEventStatus } from '@/types/events'
definePage({ definePage({
meta: { meta: {
@@ -21,7 +21,7 @@ const formData = ref<CreateEventData>({
status: 'draft', status: 'draft',
}) })
const statusOptions: { title: string; value: EventCrewEventStatus }[] = [ const statusOptions: { title: string; value: CrewliEventStatus }[] = [
{ title: 'Draft', value: 'draft' }, { title: 'Draft', value: 'draft' },
{ title: 'Published', value: 'published' }, { title: 'Published', value: 'published' },
{ title: 'Registration open', value: 'registration_open' }, { title: 'Registration open', value: 'registration_open' },

View File

@@ -17,8 +17,8 @@ export interface Pagination {
to: number | null to: number | null
} }
/** EventCrew festival / multi-day event (API resource). */ /** Crewli festival / multi-day event (API resource). */
export type EventCrewEventStatus = export type CrewliEventStatus =
| 'draft' | 'draft'
| 'published' | 'published'
| 'registration_open' | 'registration_open'
@@ -41,7 +41,7 @@ export interface Event {
start_date: string start_date: string
end_date: string end_date: string
timezone: string timezone: string
status: EventCrewEventStatus status: CrewliEventStatus
created_at: string created_at: string
updated_at: string updated_at: string
organisation?: OrganisationSummary organisation?: OrganisationSummary
@@ -53,7 +53,7 @@ export interface CreateEventData {
start_date: string start_date: string
end_date: string end_date: string
timezone?: string timezone?: string
status?: EventCrewEventStatus status?: CrewliEventStatus
} }
export interface UpdateEventData extends Partial<CreateEventData> {} export interface UpdateEventData extends Partial<CreateEventData> {}

View File

@@ -1,40 +1,23 @@
# Band Portal # Crewli — Organizer SPA
This folder will contain the Band Member Portal using Vuexy Vue. Main product UI for organisation and event staff (Vue 3 + Vuexy + Vuetify). Lives in this repo; only re-copy from Vuexy when upgrading the template.
## Setup ## Setup
1. Copy Vuexy Vue **starter-kit** (TypeScript) here: 1. Install dependencies:
```bash
cp -r /path/to/vuexy/typescript-version/starter-kit/* .
```
2. Install dependencies:
```bash ```bash
pnpm install pnpm install
``` ```
3. Create `.env.local`: 2. Create `.env.local`:
```env ```env
VITE_API_URL=http://localhost:8000/api/v1 VITE_API_URL=http://localhost:8000/api/v1
VITE_APP_NAME="Band Portal" VITE_APP_NAME="Crewli Organizer"
``` ```
4. Update `vite.config.ts` to use port 5174: 3. Dev server uses port **5174** (see `vite.config.ts` or run from repo root: `make app`).
```typescript
export default defineConfig({
// ... existing config
server: {
port: 5174,
},
})
```
5. Start development:
```bash ```bash
pnpm dev --port 5174 pnpm dev --port 5174
@@ -43,3 +26,5 @@ pnpm dev --port 5174
## Port ## Port
Runs on http://localhost:5174 Runs on http://localhost:5174
**Production:** e.g. `VITE_API_URL=https://api.crewli.app/api/v1` and host the SPA at `https://app.crewli.app` (see `api/.env.example` for `FRONTEND_APP_URL` and `SANCTUM_STATEFUL_DOMAINS`).

View File

@@ -6,7 +6,7 @@
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Event Crew - App</title> <title>Crewli — Organizer</title>
<link rel="stylesheet" type="text/css" href="/loader.css" /> <link rel="stylesheet" type="text/css" href="/loader.css" />
</head> </head>

View File

@@ -1,5 +1,5 @@
{ {
"name": "eventcrew-app", "name": "crewli-app",
"version": "9.5.0", "version": "9.5.0",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -2,6 +2,11 @@ import axios from 'axios'
import { parse } from 'cookie-es' import { parse } from 'cookie-es'
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios' import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
/**
* Single axios instance for the Laravel API (`VITE_API_URL`, e.g. …/api/v1).
* Auth: Bearer token from cookie `accessToken` (set by login).
* Use composables built on this client for real API calls; Vuexy `useApi` remains for demos/mocks.
*/
const apiClient: AxiosInstance = axios.create({ const apiClient: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_URL, baseURL: import.meta.env.VITE_API_URL,
headers: { headers: {
@@ -14,9 +19,7 @@ const apiClient: AxiosInstance = axios.create({
function getAccessToken(): string | null { function getAccessToken(): string | null {
if (typeof document === 'undefined') return null if (typeof document === 'undefined') return null
const cookies = parse(document.cookie) const cookies = parse(document.cookie)
const token = cookies.accessToken return cookies.accessToken ?? null
return token ?? null
} }
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
@@ -25,11 +28,9 @@ apiClient.interceptors.request.use(
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}` config.headers.Authorization = `Bearer ${token}`
} }
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.log(`🚀 ${config.method?.toUpperCase()} ${config.url}`, config.data) console.log(`🚀 ${config.method?.toUpperCase()} ${config.url}`, config.data)
} }
return config return config
}, },
error => Promise.reject(error), error => Promise.reject(error),
@@ -40,26 +41,20 @@ apiClient.interceptors.response.use(
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.log(`${response.status} ${response.config.url}`, response.data) console.log(`${response.status} ${response.config.url}`, response.data)
} }
return response return response
}, },
error => { error => {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.error( console.error(`${error.response?.status} ${error.config?.url}`, error.response?.data)
`${error.response?.status} ${error.config?.url}`,
error.response?.data,
)
} }
if (error.response?.status === 401) { if (error.response?.status === 401) {
document.cookie = 'accessToken=; path=/; max-age=0' document.cookie = 'accessToken=; path=/; max-age=0'
document.cookie = 'userData=; path=/; max-age=0' document.cookie = 'userData=; path=/; max-age=0'
document.cookie = 'userAbilityRules=; path=/; max-age=0' document.cookie = 'userAbilityRules=; path=/; max-age=0'
if (window.location.pathname !== '/login') { if (typeof window !== 'undefined' && window.location.pathname !== '/login') {
window.location.href = '/login' window.location.href = '/login'
} }
} }
return Promise.reject(error) return Promise.reject(error)
}, },
) )

View File

@@ -13,8 +13,8 @@ export interface Pagination {
to: number | null to: number | null
} }
/** EventCrew festival / multi-day event (aligned with API `EventResource`). */ /** Crewli festival / multi-day event (aligned with API `EventResource`). */
export type EventCrewEventStatus = export type CrewliEventStatus =
| 'draft' | 'draft'
| 'published' | 'published'
| 'registration_open' | 'registration_open'
@@ -37,7 +37,7 @@ export interface Event {
start_date: string start_date: string
end_date: string end_date: string
timezone: string timezone: string
status: EventCrewEventStatus status: CrewliEventStatus
created_at: string created_at: string
updated_at: string updated_at: string
organisation?: OrganisationSummary organisation?: OrganisationSummary

View File

@@ -10,7 +10,7 @@ import { AppContentLayoutNav, ContentWidth, FooterType, NavbarType } from '@layo
export const { themeConfig, layoutConfig } = defineThemeConfig({ export const { themeConfig, layoutConfig } = defineThemeConfig({
app: { app: {
title: 'Band', title: 'Organizer',
logo: h('div', { innerHTML: logo, style: 'line-height:0; color: rgb(var(--v-global-theme-primary))' }), logo: h('div', { innerHTML: logo, style: 'line-height:0; color: rgb(var(--v-global-theme-primary))' }),
contentWidth: ContentWidth.Boxed, contentWidth: ContentWidth.Boxed,
contentLayoutNav: AppContentLayoutNav.Vertical, contentLayoutNav: AppContentLayoutNav.Vertical,

View File

@@ -1,40 +1,23 @@
# Customer Portal # Crewli — Portal SPA
This folder will contain the Customer Portal using Vuexy Vue. External-facing app for volunteers (login) and artists/suppliers (token links). Stripped Vuexy layout; same stack as other apps.
## Setup ## Setup
1. Copy Vuexy Vue **starter-kit** (TypeScript) here: 1. Install dependencies:
```bash
cp -r /path/to/vuexy/typescript-version/starter-kit/* .
```
2. Install dependencies:
```bash ```bash
pnpm install pnpm install
``` ```
3. Create `.env.local`: 2. Create `.env.local`:
```env ```env
VITE_API_URL=http://localhost:8000/api/v1 VITE_API_URL=http://localhost:8000/api/v1
VITE_APP_NAME="Customer Portal" VITE_APP_NAME="Crewli Portal"
``` ```
4. Update `vite.config.ts` to use port 5175: 3. Dev server uses port **5175** (see `vite.config.ts` or run from repo root: `make portal`).
```typescript
export default defineConfig({
// ... existing config
server: {
port: 5175,
},
})
```
5. Start development:
```bash ```bash
pnpm dev --port 5175 pnpm dev --port 5175
@@ -43,3 +26,5 @@ pnpm dev --port 5175
## Port ## Port
Runs on http://localhost:5175 Runs on http://localhost:5175
**Production:** e.g. `VITE_API_URL=https://api.crewli.app/api/v1` and host the SPA at `https://portal.crewli.app` (portal links in emails use this host; see `api/.env.example`).

View File

@@ -6,7 +6,7 @@
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Event Crew - Customer Portal</title> <title>Crewli — Portal</title>
<link rel="stylesheet" type="text/css" href="/loader.css" /> <link rel="stylesheet" type="text/css" href="/loader.css" />
</head> </head>

View File

@@ -1,5 +1,5 @@
{ {
"name": "eventcrew-portal", "name": "crewli-portal",
"version": "9.5.0", "version": "9.5.0",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -10,7 +10,7 @@ import { AppContentLayoutNav, ContentWidth, FooterType, NavbarType } from '@layo
export const { themeConfig, layoutConfig } = defineThemeConfig({ export const { themeConfig, layoutConfig } = defineThemeConfig({
app: { app: {
title: 'Customer', title: 'Portal',
logo: h('div', { innerHTML: logo, style: 'line-height:0; color: rgb(var(--v-global-theme-primary))' }), logo: h('div', { innerHTML: logo, style: 'line-height:0; color: rgb(var(--v-global-theme-primary))' }),
contentWidth: ContentWidth.Boxed, contentWidth: ContentWidth.Boxed,
contentLayoutNav: AppContentLayoutNav.Vertical, contentLayoutNav: AppContentLayoutNav.Vertical,

View File

@@ -1,4 +1,4 @@
# Event Crew - Development Services # Crewli - Development Services
# PHP/Node run natively for best Cursor IDE performance # PHP/Node run natively for best Cursor IDE performance
services: services:
@@ -9,8 +9,8 @@ services:
- "3306:3306" - "3306:3306"
environment: environment:
MYSQL_ROOT_PASSWORD: root MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: event_crew MYSQL_DATABASE: crewli
MYSQL_USER: event_crew MYSQL_USER: crewli
MYSQL_PASSWORD: secret MYSQL_PASSWORD: secret
volumes: volumes:
- mysql_data:/var/lib/mysql - mysql_data:/var/lib/mysql

View File

@@ -1,56 +1,56 @@
# EventCrew API Contract # Crewli API Contract
# Base: /api/v1/ Base path: `/api/v1/`
# Auth: Bearer token (Sanctum) Auth: Bearer token (Sanctum)
## Auth ## Auth
POST /auth/login - `POST /auth/login`
POST /auth/logout - `POST /auth/logout`
GET /auth/me - `GET /auth/me`
## Organisations ## Organisations
GET /organisations -- lijst (super admin) - `GET /organisations` list (super admin)
POST /organisations -- aanmaken - `POST /organisations` — create
GET /organisations/{org} -- detail - `GET /organisations/{org}` — show
PUT /organisations/{org} -- bijwerken - `PUT /organisations/{org}` — update
GET /organisations/{org}/members -- leden - `GET /organisations/{org}/members` — members
POST /organisations/{org}/invite -- uitnodigen - `POST /organisations/{org}/invite` — invite user
## Events ## Events
GET /organisations/{org}/events - `GET /organisations/{org}/events`
POST /organisations/{org}/events - `POST /organisations/{org}/events`
GET /organisations/{org}/events/{event} - `GET /organisations/{org}/events/{event}`
PUT /organisations/{org}/events/{event} - `PUT /organisations/{org}/events/{event}`
## Festival Sections ## Festival sections
GET /events/{event}/sections - `GET /events/{event}/sections`
POST /events/{event}/sections - `POST /events/{event}/sections`
GET /events/{event}/sections/{section} - `GET /events/{event}/sections/{section}`
## Time Slots ## Time slots
GET /events/{event}/time-slots - `GET /events/{event}/time-slots`
POST /events/{event}/time-slots - `POST /events/{event}/time-slots`
## Shifts ## Shifts
GET /events/{event}/sections/{section}/shifts - `GET /events/{event}/sections/{section}/shifts`
POST /events/{event}/sections/{section}/shifts - `POST /events/{event}/sections/{section}/shifts`
PUT /events/{event}/sections/{section}/shifts/{shift} - `PUT /events/{event}/sections/{section}/shifts/{shift}`
POST /events/{event}/sections/{section}/shifts/{shift}/assign - `POST /events/{event}/sections/{section}/shifts/{shift}/assign`
POST /events/{event}/sections/{section}/shifts/{shift}/claim - `POST /events/{event}/sections/{section}/shifts/{shift}/claim`
## Persons ## Persons
GET /events/{event}/persons - `GET /events/{event}/persons`
POST /events/{event}/persons - `POST /events/{event}/persons`
GET /events/{event}/persons/{person} - `GET /events/{event}/persons/{person}`
PUT /events/{event}/persons/{person} - `PUT /events/{event}/persons/{person}`
POST /events/{event}/persons/{person}/approve - `POST /events/{event}/persons/{person}/approve`
# ... (volledig API contract uitbreiden per module) _(Extend this contract per module as endpoints are implemented.)_

View File

@@ -1,4 +1,4 @@
# EventCrew — Core Database Schema # Crewli — Core Database Schema
> Source: Design Document v1.3 — Section 3.5 > Source: Design Document v1.3 — Section 3.5
> All 12 findings from the database review (v1.3) are incorporated. > All 12 findings from the database review (v1.3) are incorporated.

View File

@@ -1,6 +1,6 @@
# Event Crew - Setup Guide # Crewli - Setup Guide
This guide walks you through setting up the Event Crew project from scratch. This guide walks you through setting up the Crewli project from scratch.
## Cursor AI Configuration ## Cursor AI Configuration
@@ -60,7 +60,7 @@ docker -v # Should show Docker version
## Step 1: Start Docker Services ## Step 1: Start Docker Services
```bash ```bash
cd event-crew cd crewli
make services make services
``` ```
@@ -85,8 +85,8 @@ Requirements:
- Set up CORS for localhost:5173, localhost:5174, localhost:5175 - Set up CORS for localhost:5173, localhost:5174, localhost:5175
- Use MySQL with these credentials: - Use MySQL with these credentials:
- Host: 127.0.0.1 - Host: 127.0.0.1
- Database: event_crew - Database: crewli
- Username: event_crew - Username: crewli
- Password: secret - Password: secret
Follow the conventions in .cursor/rules for code style. Follow the conventions in .cursor/rules for code style.
@@ -95,7 +95,7 @@ Follow the conventions in .cursor/rules for code style.
### Manual Alternative ### Manual Alternative
```bash ```bash
cd event-crew cd crewli
composer create-project laravel/laravel api composer create-project laravel/laravel api
cd api cd api
composer require laravel/sanctum composer require laravel/sanctum
@@ -107,46 +107,38 @@ Then configure `api/.env`:
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_PORT=3306 DB_PORT=3306
DB_DATABASE=event_crew DB_DATABASE=crewli
DB_USERNAME=event_crew DB_USERNAME=crewli
DB_PASSWORD=secret DB_PASSWORD=secret
FRONTEND_ADMIN_URL=http://localhost:5173
FRONTEND_APP_URL=http://localhost:5174
FRONTEND_PORTAL_URL=http://localhost:5175
SANCTUM_STATEFUL_DOMAINS=localhost:5173,localhost:5174,localhost:5175 SANCTUM_STATEFUL_DOMAINS=localhost:5173,localhost:5174,localhost:5175
SESSION_DOMAIN=localhost SESSION_DOMAIN=localhost
``` ```
**Production (domain `crewli.app`):** set `APP_URL=https://api.crewli.app`, point `FRONTEND_ADMIN_URL` / `FRONTEND_APP_URL` / `FRONTEND_PORTAL_URL` to `https://admin.crewli.app`, `https://app.crewli.app`, and `https://portal.crewli.app`, and `SANCTUM_STATEFUL_DOMAINS=admin.crewli.app,app.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.
--- ---
## Step 3: Copy Vuexy Files ## Step 3: Vuexy frontends (this repo)
### Admin SPA (Full Version) This monorepo already contains three SPAs under `apps/`:
1. Extract your Vuexy download | Directory | Role | Typical Vuexy source |
2. Navigate to: `vuexy-vuejs-admin-template/typescript-version/full-version/` |-----------|------|----------------------|
3. Copy ALL contents to: `apps/admin/` | `apps/admin/` | Super Admin | full-version (TypeScript) |
| `apps/app/` | Organizer (main product) | full-version or customized starter |
| `apps/portal/` | External portal (volunteers, token links) | stripped starter / custom layout |
If you ever need to re-copy from a Vuexy ZIP, use the paths above — not legacy `apps/band` or `apps/customers`.
```bash ```bash
# Example (adjust path to your Vuexy download) # Example only — adjust to your Vuexy download path
cp -r ~/Downloads/vuexy/typescript-version/full-version/* apps/admin/ cp -r ~/Downloads/vuexy/typescript-version/full-version/* apps/admin/
``` ```
### Band Portal (Starter Kit)
1. Navigate to: `vuexy-vuejs-admin-template/typescript-version/starter-kit/`
2. Copy ALL contents to: `apps/band/`
```bash
cp -r ~/Downloads/vuexy/typescript-version/starter-kit/* apps/band/
```
### Customer Portal (Starter Kit)
1. Copy starter-kit to: `apps/customers/`
```bash
cp -r ~/Downloads/vuexy/typescript-version/starter-kit/* apps/customers/
```
--- ---
## Step 4: Configure SPAs ## Step 4: Configure SPAs
@@ -155,8 +147,8 @@ cp -r ~/Downloads/vuexy/typescript-version/starter-kit/* apps/customers/
```bash ```bash
cd apps/admin && pnpm install cd apps/admin && pnpm install
cd ../band && pnpm install cd ../app && pnpm install
cd ../customers && pnpm install cd ../portal && pnpm install
``` ```
### Create Environment Files ### Create Environment Files
@@ -164,139 +156,96 @@ cd ../customers && pnpm install
**apps/admin/.env.local** **apps/admin/.env.local**
```env ```env
VITE_API_URL=http://localhost:8000/api/v1 VITE_API_URL=http://localhost:8000/api/v1
VITE_APP_NAME="Event Crew Admin" VITE_APP_NAME="Crewli Admin"
``` ```
**apps/band/.env.local** **apps/app/.env.local**
```env ```env
VITE_API_URL=http://localhost:8000/api/v1 VITE_API_URL=http://localhost:8000/api/v1
VITE_APP_NAME="Band Portal" VITE_APP_NAME="Crewli Organizer"
``` ```
**apps/customers/.env.local** **apps/portal/.env.local**
```env ```env
VITE_API_URL=http://localhost:8000/api/v1 VITE_API_URL=http://localhost:8000/api/v1
VITE_APP_NAME="Customer Portal" VITE_APP_NAME="Crewli Portal"
``` ```
### Update Vite Ports ### Dev server ports
**apps/band/vite.config.ts** - Add port 5174: From the repo root, `make admin`, `make app`, and `make portal` start Vite on **5173**, **5174**, and **5175** respectively. If you run `pnpm dev` manually, configure the same ports in each apps `vite.config.ts` under `server.port`.
```typescript
export default defineConfig({
// ... existing config
server: {
port: 5174,
},
})
```
**apps/customers/vite.config.ts** - Add port 5175:
```typescript
export default defineConfig({
// ... existing config
server: {
port: 5175,
},
})
```
--- ---
## Step 5: Set Up API Client in SPAs ## Step 5: API client in SPAs
Use Cursor to add the API client. Prompt: `apps/admin/src/lib/api-client.ts`, `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.
```
Add an API client to apps/admin/src/lib/api-client.ts that:
1. Uses axios with base URL from VITE_API_URL
2. Adds auth token from localStorage to requests
3. Handles 401 responses by redirecting to /login
4. Logs requests and responses in development
Follow the pattern in .cursor/rules
```
--- ---
## Step 6: Create Database Schema ## Step 6: Create database schema
Use Cursor to create migrations. Prompt: Implement migrations from the canonical schema, not a legacy intranet model:
``` - **`docs/SCHEMA.md`** — table list, columns, indexes
Create Laravel migrations for all tables defined in .cursor/rules: - **`.cursor/ARCHITECTURE.md`** — overview and relationships
- users (with roles and types) - **`.cursor/rules/103_database.mdc`** — ULIDs, soft deletes, index rules
- customers
- locations
- events
- event_invitations
- music_numbers
- music_attachments
- setlists
- setlist_items
- booking_requests
- notifications
- activity_logs
Use ULIDs for primary keys and follow Laravel conventions. **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: Then run:
```bash ```bash
cd api && php artisan migrate cd api && php artisan migrate
``` ```
--- ---
## Step 7: Start Development ## Step 7: Start development
Open 4 terminal tabs: Open separate terminals (or use the Makefile from the repo root):
```bash ```bash
# Tab 1: Services (already running) # Tab 1: Services (Docker)
make services make services
# Tab 2: Laravel API # Tab 2: Laravel API
make api make api
# Tab 3: Admin SPA # Tab 3: Admin SPA (optional)
make admin make admin
# Tab 4: Band Portal (optional) # Tab 4: Organizer SPA (optional)
make band make app
# Tab 5: Portal SPA (optional)
make portal
``` ```
--- ---
## Building Features ## Building features
Use these Cursor prompts to build features: Use Cursor with **`CLAUDE.md`** and **`.cursor/instructions.md`**. Example directions:
### Authentication ### Authentication
``` ```
Create the authentication system: Wire Sanctum API auth: login, logout, me; form requests; API resources; Vue apps use axios + token storage (see .cursor/rules).
1. AuthController with login, logout, register, user endpoints
2. Form requests for validation
3. API resources for user responses
4. Update Vuexy's auth to use our API instead of fake backend
``` ```
### Events Module ### Events module (Crewli)
``` ```
Create the Events module: Events nested under organisations: ULID PK, OrganisationScope, policies, EventResource, feature tests (200/401/403/422).
1. Event model with relationships (location, customer, setlist, invitations)
2. EventController with CRUD + invite and RSVP endpoints
3. Form requests and API resources
4. Event policy for authorization
``` ```
### RSVP System ### Portal token flow
``` ```
Create the RSVP system: Portal token middleware and routes for artist/supplier contexts; document links on https://portal.crewli.app/... (see .cursor/rules/102_multi_tenancy.mdc).
1. EventInvitation model
2. Endpoints for members to respond to invitations
3. Admin view of all RSVPs per event
4. Email notifications for new invitations
``` ```
--- ---
@@ -325,15 +274,13 @@ pnpm type-check
--- ---
## Next Steps ## Next steps
1. Services running 1. Services running (Docker)
2. Laravel API created 2. Laravel API configured and migrated
3. ✅ Vuexy copied to SPAs 3. SPAs installed (`apps/admin`, `apps/app`, `apps/portal`)
4. Environment configured 4. Environment files for API + each SPA
5. 🔲 Build authentication 5. Authentication and organisation switching
6. 🔲 Build events module 6. Events, sections, time slots, shifts
7. 🔲 Build members module 7. Persons, crowd types, portal flows
8. 🔲 Build music catalog 8. Accreditation, briefings, operational modules per roadmap in `.cursor/instructions.md`
9. 🔲 Build setlist manager
10. 🔲 Build customer portal