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.
> Source of truth: `/resources/design/EventCrew_Design_Document_v1.3.docx`
> Source of truth: `/resources/design/Crewli_Design_Document_v1.3.docx`
## System Overview
@@ -432,6 +432,8 @@ Three frontend origins in `config/cors.php` (via env):
| 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://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)
@@ -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.
> Design Document: `/resources/design/EventCrew_Design_Document_v1.3.docx`
> Dev Guide: `/resources/design/EventCrew_Dev_Guide_v1.0.docx`
> Start Guide: `/resources/design/EventCrew_Start_Guide_v1.0.docx`
> Design Document: `/resources/design/Crewli_Design_Document_v1.3.docx`
> Dev Guide: `/resources/design/Crewli_Dev_Guide_v1.0.docx`
> Start Guide: `/resources/design/Crewli_Start_Guide_v1.0.docx`
## Project Overview
**Name**: EventCrew
**Name**: Crewli
**Type**: Multi-tenant SaaS platform (API-first architecture)
**Status**: Development
### 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
@@ -130,7 +130,7 @@ 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 EventCrew structure
4. Replace Vuexy demo navigation with Crewli structure
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: ["**/*"]
alwaysApply: true
---
# 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
@@ -36,7 +36,7 @@ You are an expert full-stack developer working on EventCrew, a multi-tenant SaaS
## Project Structure
```
event-crew/
crewli/
├── api/ # Laravel 12 REST API (JSON only)
│ ├── app/
│ │ ├── Http/
@@ -130,8 +130,19 @@ event-crew/
| Redis | `localhost:6379` | - |
| 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
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

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"]
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}"]
alwaysApply: true
---
@@ -23,7 +23,7 @@ alwaysApply: true
- Minimal modifications needed
### `apps/app/` (Organizer - Main App)
- Sidebar nav customized for EventCrew structure
- Sidebar nav customized for Crewli structure
- Remove Vuexy demo/customizer components
- Full Vuetify component usage
- 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}"]
alwaysApply: true
---
@@ -101,7 +101,7 @@ Route::middleware(['auth:sanctum', 'event.role:event_manager'])->group(...);
### 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
3. POST /api/v1/portal/token-auth { token: '01HQ3K...' }
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
```
1. Volunteer navigates to https://portal.eventcrew.app/login
1. Volunteer navigates to https://portal.crewli.app/login
2. Enters email + password
3. POST /api/v1/auth/login (same endpoint as apps/app/)
4. Returns user + organisations + event roles
@@ -196,6 +196,8 @@ class PortalTokenMiddleware
'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
### 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"]
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"]
alwaysApply: true
---

View File

@@ -1,21 +1,21 @@
# EventCrew Cursor Rules
# 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
- Resource controllers, form requests, API resources — always
- `HasUlids` on business models, `HasFactory`, `SoftDeletes` where documented
- Global scope `OrganisationScope` on event-related models
- Policies for authorization — never 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
- `<script setup lang="ts">` always
- TanStack Query for API state, Pinia for UI state
- Vuetify components first; custom CSS only as a last resort
## Naming
- snake_case DB | camelCase JS | PascalCase Vue | `use*` composables | `use*Store` Pinia
## 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": [
{ "directory": "apps/admin", "changeProcessCWD": true },
{ "directory": "apps/band", "changeProcessCWD": true },
{ "directory": "apps/customers", "changeProcessCWD": true }
{ "directory": "apps/app", "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.
Gebouwd voor een professionele vrijwilligersorganisatie, met SaaS-uitbreidingspotentieel.
Design Document: /docs/EventCrew_Design_Document_v1.3.docx
Crewli is a multi-tenant SaaS platform for event and festival management.
Built for a professional volunteer organisation, with potential to expand as SaaS.
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
- Frontend: TypeScript, Vue 3 (Composition API), Vuexy/Vuetify, Pinia, TanStack Query
- Testing: PHPUnit (backend), Vitest (frontend)
## Repository Structuur
## Repository layout
- api/ Laravel backend
- apps/admin/ Super Admin SPA
- apps/app/ Organizer SPA (hoofdapp)
- apps/portal/ Externe portals (vrijwilliger, artiest, leverancier)
- `api/` Laravel backend
- `apps/admin/` Super Admin SPA
- `apps/app/` Organizer SPA (main product app)
- `apps/portal/` External portal (volunteers, artists, suppliers, etc.)
## Apps & Portal Architectuur
## Apps and portal architecture
- apps/admin/ = Super Admin platformbeheer, organisaties aanmaken
- apps/app/ = Organizer event management per organisatie
- apps/portal/ = Externe gebruikers — één app, twee toegangsmodi:
- Login-based (auth:sanctum): vrijwilligers, crew — persons met user_id
- Token-based (portal.token middleware): artiesten, leveranciers, pers — persons zonder user_id
- `apps/admin/` Super Admin: platform management, creating organisations
- `apps/app/` Organizer: event management per organisation
- `apps/portal/` External users: one app, two access modes:
- Login-based (`auth:sanctum`): volunteers, crew — persons with `user_id`
- Token-based (`portal.token` middleware): artists, suppliers, press — persons without `user_id`
### CORS
Drie frontend origins configureren in zowel Laravel (config/cors.php via env) als Vite dev server proxy:
- admin: localhost:5173
- app: localhost:5174
- portal: localhost:5175
Configure three frontend origins in both Laravel (`config/cors.php` via env) and the Vite dev server proxy:
## 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
- 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
- Every query on event data **must** scope on `organisation_id`
- Use `OrganisationScope` as an Eloquent global scope on all event-related models
- Never use raw ID checks in controllers — always use 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())
- Use resource controllers (`index` / `show` / `store` / `update` / `destroy`)
- Namespace: `App\Http\Controllers\Api\V1\`
- Return all responses through API resources (never return raw model attributes)
- Validate with form requests (never inline `validate()`)
### Modellen
### Models
- 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
- Use the `HasUlids` trait on all business models (not UUID v4)
- Soft deletes on: Organisation, Event, FestivalSection, Shift, ShiftAssignment, Person, Artist
- No soft deletes on: CheckIn, BriefingSend, MessageReply, ShiftWaitlist (audit records)
- JSON columns **only** for opaque configuration — never for queryable/filterable 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
- Primary keys: ULID via `HasUlids` (not UUID v4, not auto-increment on business tables)
- Create migrations in dependency order: foundation first, then dependent tables
- Always add composite indexes as documented in the design document (section 3.5)
### Rollen & Permissies
### Roles and permissions
- 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.)
- Use Spatie `laravel-permission`
- Check roles via `$user->hasRole()` and policies — never hard-code role strings in controllers
- Three levels: app (`super_admin`), organisation (`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
- Write PHPUnit feature tests per controller
- Minimum per endpoint: happy path + unauthenticated (401) + wrong organisation (403)
- Use factories for all test data
- 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
- Props altijd getypeerd met defineProps<{...}>()
- Emits altijd gedeclareerd met defineEmits<{...}>()
- Always `<script setup lang="ts">` — never the Options API
- Type props with `defineProps<{...}>()`
- Declare emits with `defineEmits<{...}>()`
### API Calls
### 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
- Use TanStack Query (`useQuery` / `useMutation`) for **all** API calls
- Never call axios directly from a component — always via a composable under `composables/`
- Use Pinia stores for cross-component state — no prop drilling
### Naamgeving
### Naming
- 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)
- DB columns: `snake_case`
- TypeScript / JS variables: `camelCase`
- Vue components: PascalCase (e.g. `ShiftAssignPanel.vue`)
- Composables: `use` prefix (e.g. `useShifts.ts`)
- Pinia stores: `use` prefix + `Store` suffix (e.g. `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
- Always use Vuexy/Vuetify for layout, forms, tables, dialogs
- Do not write custom CSS when a Vuetify utility class exists
- Responsive: mobile-first, usable from 375px width
## Verboden Patronen
## Forbidden patterns
- 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)
- Never: `$user->role === 'admin'` (use policies)
- Never: `Model::all()` without a `where` clause (always scope)
- Never: leave `dd()` or `var_dump()` in code
- Never: hard-code `.env` values in code
- Never: use JSON columns for data you need to filter on
- 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
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
1. Create and run migration(s)
2. Eloquent model with relationships, scopes, and `HasUlids`
3. Factory for test data
4. Policy for authorization
5. Form request(s) for validation
6. API resource for response shaping
7. Resource controller
8. Register routes in `api.php`
9. Write and run PHPUnit feature tests
10. Vue composable for API calls (e.g. `useShifts.ts`)
11. Pinia store if cross-component state is needed
12. Vue page component
13. Add route in Vue Router

View File

@@ -9,7 +9,7 @@ NC := \033[0m
help:
@echo ""
@echo "$(GREEN)╔══════════════════════════════════════════════════════════════╗$(NC)"
@echo "$(GREEN)EVENT CREW - Development Commands ║$(NC)"
@echo "$(GREEN) CREWLI - Development Commands $(NC)"
@echo "$(GREEN)╚══════════════════════════════════════════════════════════════╝$(NC)"
@echo ""
@echo " $(YELLOW)Services (Docker):$(NC)"
@@ -33,7 +33,7 @@ services:
@docker compose up -d
@echo ""
@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)Mailpit:$(NC) http://localhost:8025"
@echo ""
@@ -68,4 +68,4 @@ fresh:
@cd api && php artisan migrate:fresh --seed
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.
---
## 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).
- **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
```
event-crew/
crewli/
├── api/ # Laravel 12 REST API (JSON only)
├── apps/
│ ├── admin/ # Super Admin SPA
@@ -94,7 +94,20 @@ Detailed setup: **[docs/SETUP.md](docs/SETUP.md)**.
| Portal | http://localhost:5175 | `FRONTEND_PORTAL_URL` |
| 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 |
|----------|----------|
| [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/instructions.md](.cursor/instructions.md) | Quick reference, phased roadmap, module build order |
| [.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_KEY=
APP_DEBUG=true
# Local API origin (no path suffix). Production: https://api.crewli.app
APP_URL=http://localhost:8000
APP_LOCALE=en
@@ -20,8 +21,8 @@ LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=event_crew
DB_USERNAME=event_crew
DB_DATABASE=crewli
DB_USERNAME=crewli
DB_PASSWORD=secret
SESSION_DRIVER=database
@@ -47,11 +48,19 @@ MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=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}"
# CORS - Frontend SPAs
FRONTEND_URL=http://localhost:5173
# CORS + Sanctum — SPA origins (no trailing slash; must match the browser URL)
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
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\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
{
public function __construct(

View File

@@ -1,22 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
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->string('name');
$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->string('password');
$table->string('timezone')->default('Europe/Amsterdam');
$table->string('locale')->default('nl');
$table->string('avatar')->nullable();
$table->rememberToken();
$table->timestamps();
$table->index(['type', 'status']);
$table->softDeletes();
});
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
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ pnpm install
```env
VITE_API_URL=http://localhost:8000/api/v1
VITE_APP_NAME="Event Crew Admin"
VITE_APP_NAME="Crewli Admin"
```
4. Start development:
@@ -31,4 +31,6 @@ pnpm dev
## 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" />
<meta name="robots" content="noindex, nofollow" />
<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" />
</head>

View File

@@ -1,5 +1,5 @@
{
"name": "eventcrew-admin",
"name": "crewli-admin",
"version": "9.5.0",
"private": true,
"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).
* 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({
baseURL: import.meta.env.VITE_API_URL,

View File

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

View File

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

View File

@@ -17,8 +17,8 @@ export interface Pagination {
to: number | null
}
/** EventCrew festival / multi-day event (API resource). */
export type EventCrewEventStatus =
/** Crewli festival / multi-day event (API resource). */
export type CrewliEventStatus =
| 'draft'
| 'published'
| 'registration_open'
@@ -41,7 +41,7 @@ export interface Event {
start_date: string
end_date: string
timezone: string
status: EventCrewEventStatus
status: CrewliEventStatus
created_at: string
updated_at: string
organisation?: OrganisationSummary
@@ -53,7 +53,7 @@ export interface CreateEventData {
start_date: string
end_date: string
timezone?: string
status?: EventCrewEventStatus
status?: CrewliEventStatus
}
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
1. Copy Vuexy Vue **starter-kit** (TypeScript) here:
```bash
cp -r /path/to/vuexy/typescript-version/starter-kit/* .
```
2. Install dependencies:
1. Install dependencies:
```bash
pnpm install
```
3. Create `.env.local`:
2. Create `.env.local`:
```env
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:
```typescript
export default defineConfig({
// ... existing config
server: {
port: 5174,
},
})
```
5. Start development:
3. Dev server uses port **5174** (see `vite.config.ts` or run from repo root: `make app`).
```bash
pnpm dev --port 5174
@@ -43,3 +26,5 @@ pnpm dev --port 5174
## Port
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" />
<meta name="robots" content="noindex, nofollow" />
<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" />
</head>

View File

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

View File

@@ -2,6 +2,11 @@ import axios from 'axios'
import { parse } from 'cookie-es'
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({
baseURL: import.meta.env.VITE_API_URL,
headers: {
@@ -14,9 +19,7 @@ const apiClient: AxiosInstance = axios.create({
function getAccessToken(): string | null {
if (typeof document === 'undefined') return null
const cookies = parse(document.cookie)
const token = cookies.accessToken
return token ?? null
return cookies.accessToken ?? null
}
apiClient.interceptors.request.use(
@@ -25,11 +28,9 @@ apiClient.interceptors.request.use(
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
if (import.meta.env.DEV) {
console.log(`🚀 ${config.method?.toUpperCase()} ${config.url}`, config.data)
}
return config
},
error => Promise.reject(error),
@@ -40,26 +41,20 @@ apiClient.interceptors.response.use(
if (import.meta.env.DEV) {
console.log(`${response.status} ${response.config.url}`, response.data)
}
return response
},
error => {
if (import.meta.env.DEV) {
console.error(
`${error.response?.status} ${error.config?.url}`,
error.response?.data,
)
console.error(`${error.response?.status} ${error.config?.url}`, error.response?.data)
}
if (error.response?.status === 401) {
document.cookie = 'accessToken=; path=/; max-age=0'
document.cookie = 'userData=; 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'
}
}
return Promise.reject(error)
},
)

View File

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

View File

@@ -10,7 +10,7 @@ import { AppContentLayoutNav, ContentWidth, FooterType, NavbarType } from '@layo
export const { themeConfig, layoutConfig } = defineThemeConfig({
app: {
title: 'Band',
title: 'Organizer',
logo: h('div', { innerHTML: logo, style: 'line-height:0; color: rgb(var(--v-global-theme-primary))' }),
contentWidth: ContentWidth.Boxed,
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
1. Copy Vuexy Vue **starter-kit** (TypeScript) here:
```bash
cp -r /path/to/vuexy/typescript-version/starter-kit/* .
```
2. Install dependencies:
1. Install dependencies:
```bash
pnpm install
```
3. Create `.env.local`:
2. Create `.env.local`:
```env
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:
```typescript
export default defineConfig({
// ... existing config
server: {
port: 5175,
},
})
```
5. Start development:
3. Dev server uses port **5175** (see `vite.config.ts` or run from repo root: `make portal`).
```bash
pnpm dev --port 5175
@@ -43,3 +26,5 @@ pnpm dev --port 5175
## Port
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" />
<meta name="robots" content="noindex, nofollow" />
<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" />
</head>

View File

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

View File

@@ -10,7 +10,7 @@ import { AppContentLayoutNav, ContentWidth, FooterType, NavbarType } from '@layo
export const { themeConfig, layoutConfig } = defineThemeConfig({
app: {
title: 'Customer',
title: 'Portal',
logo: h('div', { innerHTML: logo, style: 'line-height:0; color: rgb(var(--v-global-theme-primary))' }),
contentWidth: ContentWidth.Boxed,
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
services:
@@ -9,8 +9,8 @@ services:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: event_crew
MYSQL_USER: event_crew
MYSQL_DATABASE: crewli
MYSQL_USER: crewli
MYSQL_PASSWORD: secret
volumes:
- 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
POST /auth/login
POST /auth/logout
GET /auth/me
- `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
- `GET /organisations` list (super admin)
- `POST /organisations` — create
- `GET /organisations/{org}` — show
- `PUT /organisations/{org}` — update
- `GET /organisations/{org}/members` — members
- `POST /organisations/{org}/invite` — invite user
## Events
GET /organisations/{org}/events
POST /organisations/{org}/events
GET /organisations/{org}/events/{event}
PUT /organisations/{org}/events/{event}
- `GET /organisations/{org}/events`
- `POST /organisations/{org}/events`
- `GET /organisations/{org}/events/{event}`
- `PUT /organisations/{org}/events/{event}`
## Festival Sections
## Festival sections
GET /events/{event}/sections
POST /events/{event}/sections
GET /events/{event}/sections/{section}
- `GET /events/{event}/sections`
- `POST /events/{event}/sections`
- `GET /events/{event}/sections/{section}`
## Time Slots
## Time slots
GET /events/{event}/time-slots
POST /events/{event}/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
- `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
- `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)
_(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
> 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
@@ -60,7 +60,7 @@ docker -v # Should show Docker version
## Step 1: Start Docker Services
```bash
cd event-crew
cd crewli
make services
```
@@ -85,8 +85,8 @@ Requirements:
- Set up CORS for localhost:5173, localhost:5174, localhost:5175
- Use MySQL with these credentials:
- Host: 127.0.0.1
- Database: event_crew
- Username: event_crew
- Database: crewli
- Username: crewli
- Password: secret
Follow the conventions in .cursor/rules for code style.
@@ -95,7 +95,7 @@ Follow the conventions in .cursor/rules for code style.
### Manual Alternative
```bash
cd event-crew
cd crewli
composer create-project laravel/laravel api
cd api
composer require laravel/sanctum
@@ -107,46 +107,38 @@ Then configure `api/.env`:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=event_crew
DB_USERNAME=event_crew
DB_DATABASE=crewli
DB_USERNAME=crewli
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
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
2. Navigate to: `vuexy-vuejs-admin-template/typescript-version/full-version/`
3. Copy ALL contents to: `apps/admin/`
| Directory | Role | Typical Vuexy source |
|-----------|------|----------------------|
| `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
# 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/
```
### 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
@@ -155,8 +147,8 @@ cp -r ~/Downloads/vuexy/typescript-version/starter-kit/* apps/customers/
```bash
cd apps/admin && pnpm install
cd ../band && pnpm install
cd ../customers && pnpm install
cd ../app && pnpm install
cd ../portal && pnpm install
```
### Create Environment Files
@@ -164,139 +156,96 @@ cd ../customers && pnpm install
**apps/admin/.env.local**
```env
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
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
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:
```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,
},
})
```
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`.
---
## Step 5: Set Up API Client in SPAs
## Step 5: API client in SPAs
Use Cursor to add the API client. Prompt:
```
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
```
`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.
---
## 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:
```
Create Laravel migrations for all tables defined in .cursor/rules:
- users (with roles and types)
- customers
- locations
- events
- event_invitations
- music_numbers
- music_attachments
- setlists
- setlist_items
- booking_requests
- notifications
- activity_logs
- **`docs/SCHEMA.md`** — table list, columns, indexes
- **`.cursor/ARCHITECTURE.md`** — overview and relationships
- **`.cursor/rules/103_database.mdc`** — ULIDs, soft deletes, index rules
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:
```bash
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
# Tab 1: Services (already running)
# Tab 1: Services (Docker)
make services
# Tab 2: Laravel API
make api
# Tab 3: Admin SPA
# Tab 3: Admin SPA (optional)
make admin
# Tab 4: Band Portal (optional)
make band
# Tab 4: Organizer SPA (optional)
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
```
Create the authentication system:
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
Wire Sanctum API auth: login, logout, me; form requests; API resources; Vue apps use axios + token storage (see .cursor/rules).
```
### Events Module
### Events module (Crewli)
```
Create the Events module:
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
Events nested under organisations: ULID PK, OrganisationScope, policies, EventResource, feature tests (200/401/403/422).
```
### RSVP System
### Portal token flow
```
Create the RSVP system:
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
Portal token middleware and routes for artist/supplier contexts; document links on https://portal.crewli.app/... (see .cursor/rules/102_multi_tenancy.mdc).
```
---
@@ -325,15 +274,13 @@ pnpm type-check
---
## Next Steps
## Next steps
1. Services running
2. Laravel API created
3. ✅ Vuexy copied to SPAs
4. Environment configured
5. 🔲 Build authentication
6. 🔲 Build events module
7. 🔲 Build members module
8. 🔲 Build music catalog
9. 🔲 Build setlist manager
10. 🔲 Build customer portal
1. Services running (Docker)
2. Laravel API configured and migrated
3. SPAs installed (`apps/admin`, `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`