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:
@@ -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*
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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
|
||||||
---
|
---
|
||||||
|
|||||||
32
.cursorrules
32
.cursorrules
@@ -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
|
||||||
|
|||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -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
171
CLAUDE.md
@@ -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
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -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
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -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 codebase’s `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 app’s 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 |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 user’s 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(
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'name' => env('APP_NAME', 'Laravel'),
|
'name' => env('APP_NAME', 'Crewli'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
|||||||
@@ -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`).
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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> {}
|
||||||
|
|||||||
@@ -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`).
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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`).
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
68
docs/API.md
68
docs/API.md
@@ -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.)_
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
195
docs/SETUP.md
195
docs/SETUP.md
@@ -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 app’s `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
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user