Files
crewli/CLAUDE.md
bert.hausmans b5765221bb docs(claude): point UI-framework conventions to PRIMEVUE_COMPONENTS.md; document migration-phase guidance
CLAUDE.md updated for the Vuetify→PrimeVue migration phase per
RFC-WS-FRONTEND-PRIMEVUE F2:

- Stack line: notes PrimeVue + Tailwind v4 as target, Vuetify still
  present on un-migrated surfaces
- Replaced "Vuexy reference source" + "Vuexy-first strategy" sections
  with a single "UI framework strategy (migration-aware)" section that
  splits guidance into migrated / un-migrated / new surfaces and
  forwards to PRIMEVUE_COMPONENTS.md
- Forms section now documents both target (@primevue/forms + Zod
  resolver via FormField) and legacy (ref + VForm + :rules) patterns,
  with the surface-level-consistency rule
- UI section reframed: PrimeVue + Tailwind on migrated surfaces,
  Vuetify utilities on legacy surfaces, three-state pattern preserved
  on both
- Order of work: framework note added for new pages during F4

Framework-agnostic sections (database, multi-tenancy, ULID,
controllers, models, security, testing) untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:10 +02:00

427 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Crewli — Claude Code Instructions
> See `dev-docs/CLAUDE_CODE_TOOLING.md` for the deterministic guard-rail layer (hooks, subagent, slash commands).
## Project context
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: `/dev-docs/design-document.md`
## Tech stack
- Backend: PHP 8.2+, Laravel 12, Sanctum, Spatie Permission, MySQL 8, Redis
- Frontend: TypeScript, Vue 3 (Composition API), PrimeVue + Tailwind v4 (target state, migration in progress per [RFC-WS-FRONTEND-PRIMEVUE](./dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md)) — Vuetify/Vuexy still present on un-migrated surfaces during F4; see [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md). Pinia, TanStack Query.
- Testing: PHPUnit (backend), Vitest (frontend)
## Quality gates
- `php artisan test` — PHPUnit feature suite. See Testing rules below.
- `./vendor/bin/pint` — code style. Runs pre-commit in scope; format before merge.
- `composer analyse` — Larastan static analysis at level 6 with
accept-all baseline. New errors beyond the baseline must be fixed
before merge. See `/dev-docs/LARASTAN.md`.
- `composer rector` — Rector dry-run for modernisation suggestions.
See `/dev-docs/RECTOR.md`. Apply only in scoped sprints, never
automatically.
- ts-reset patches TypeScript's loosest default types in the SPA.
See `/dev-docs/FRONTEND-TOOLING.md`. New TypeScript code adheres
to ts-reset's stricter types automatically.
- Vitest — `apps/app` has Vitest with 213 tests as of WS-3 PR-B2a.
Test count grows with each PR; check `pnpm test` for current value.
## Development tooling
- Laravel Telescope at `/telescope` — queries, jobs, mails, redis,
events. Local + testing only, never production. super_admin role
required to view. See `/dev-docs/TELESCOPE.md`.
## Repository layout
- `api/` — Laravel backend
- `apps/app/` — Single SPA covering organizers, volunteers, crew, super admins (context-routed in-app) plus the public form-fill / artist-advance flows
## App architecture
`apps/app/` — single workspace, two access modes:
- Login-based (`auth:sanctum`): organizers, volunteers, crew, super_admin. Includes **Platform Admin** section (`/platform/*`) for super_admin users (organisation management, user management, impersonation, activity log). Context-aware routing inside the SPA distinguishes organizer vs. volunteer experience based on `useAuthStore.availableContexts` (see `dev-docs/AUTH_ARCHITECTURE.md`).
- Token-based (`portal.token` middleware): artists, suppliers, press — persons without `user_id`. Stateless per-request token via `Authorization: Bearer` header or `?token=` query parameter.
### CORS
Single frontend origin in both Laravel (`config/cors.php` via env) and the Vite dev server proxy:
- dev: `localhost:5174`
- prod: `https://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, SPA, or transactional mail).
## Backend rules (strict)
### Multi-tenancy
- 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
- 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()`)
### Models
- 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
Crewli uses **MySQL 8.0 exclusively** across all environments:
- **Local development**: MySQL 8.0 via `docker-compose.yml` (`bm_mysql` container, port 3306). Run `make services` to start.
- **Testing**: MySQL 8.0 via the same Docker container, separate `crewli_test` database. Run `make test-db-create` once, then `make test`.
- **Staging/production**: MySQL 8.0 on the deployment VPS.
**SQLite is forbidden** in all environments. Reasons:
- SQLite has known quirks around foreign-key constraints (rebuild-on-FK-add cascades), JSON queries (key-order non-determinism on round-trip differs from MySQL), and concurrent writes that mask bugs which MySQL would surface in production
- Cross-database query syntax differences cause silent test/production drift (e.g. `sqlite_master` vs. `information_schema`, VARCHAR length enforcement)
- Crewli's enterprise-grade product philosophy doesn't tolerate "but it works on SQLite" as a passing test
If a contributor finds SQLite references in `phpunit.xml`, `.env.testing`, or any config file, treat it as a regression and fix immediately.
When writing migrations or queries:
- Use Laravel's query builder where possible — it handles cross-DB syntax consistently
- For raw SQL, write MySQL syntax (no need for SQLite fallbacks). Index introspection: query `information_schema.STATISTICS`, never `sqlite_master`.
- Foreign keys go on every relation column. The `nullOnDelete` / `cascadeOnDelete` choice is per the relationship's domain semantics; "no FK" is not an option.
- JSON values stored in byte-stable columns (`schema_snapshot`, webhook `payload_snapshot`, activity-log `properties` for diff payloads) MUST be canonicalized at write via `App\Support\Json\JsonCanonicalizer`. MySQL JSON columns may reorder associative-array keys on round-trip; canonicalize so re-emits / HMAC signatures / audit-replay diffs are byte-identical. Opaque-config JSON (`form_schemas.settings`, `translations`) is exempt — key order has no semantic meaning there. Tests on canonicalized data use `assertSame(JsonCanonicalizer::encode($a), JsonCanonicalizer::encode($b))`.
Other database rules:
- 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)
### Schema dumps (CI fast path)
Laravel uses `database/schema/{driver}-schema.sql` as a fast-path
baseline that `migrate:fresh` loads in one statement (~1s) instead of
replaying every migration (~6s × N migrations). For Crewli's backfill
/ migration tests that call `migrate:fresh` per test, this drops the
4 backfill-test classes from ~128s to ~28s combined (78% reduction).
The dump is **committed** at `api/database/schema/mysql-schema.sql`
and is the default code path. CI loads it automatically via
`migrate:fresh`.
**One-time host setup** (required for both generating and loading
the dump):
```bash
brew install mysql-client
echo 'export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
```
Verify with `which mysql && which mysqldump`. Both must resolve to a
real binary; Laravel's `schema:dump` and `migrate:fresh` schema-load
both shell out to these.
**After adding a new migration:**
```bash
make schema-dump
git add api/database/schema/mysql-schema.sql
```
`make schema-dump` brings `crewli_test` to head and runs
`php artisan schema:dump`. Commit the regenerated file in the same
PR as the migration.
`--prune` is NOT used: individual migration files stay readable in
`api/database/migrations/` for audit / rollback purposes.
### Roles and permissions
- 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
- 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 test architecture (5 tiers: Unit / Component / Integration /
Visual / E2E) is documented in `dev-docs/ARCH-TESTING.md`. Choose the
right tier per the decision tree there before adding new tests.
## Frontend rules (strict)
### UI framework strategy (migration-aware)
The SPA is migrating Vuetify/Vuexy → PrimeVue + Tailwind v4 per
[RFC-WS-FRONTEND-PRIMEVUE](./dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md).
During F4 (sub-packages F4aF4d), both frameworks ship in the same build
on different surfaces. The component-selection rules depend on which side
of the migration the surface is on.
**Always read [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md)
before any frontend task** — it is the authoritative reference for
component selection, theming, forms, and DataTable conventions across
both phases.
#### On migrated surfaces (target state)
PrimeVue is the framework. Follow [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md):
1. **Can a Tailwind utility do this?** (layout, spacing, typography) → use it.
2. **Does PrimeVue provide a component?** → use it (see §3 component mapping).
3. **Forms**`@primevue/forms` + Zod resolver via `<FormField>` (§5; full API in [RFC Appendix A](./dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md#appendix-a--formfield-api-specification)).
4. **DataTables**`<DataTable>` with `:lazy="true"` for server-side (§6).
5. **None of the above?** → cross-reference https://primevue.org/ for the closest match. Add a note in `PRIMEVUE_COMPONENTS.md` §3 if it's a recurring need.
Customization order: Tailwind utilities (layout) → `pt` API (component-internal) → Aura preset extension (brand-wide) → `<style scoped>` (last resort, with comment).
#### On un-migrated surfaces (legacy, transient)
Vuetify + Vuexy `@core/` components remain in use until the surface's F4
sub-package lands. When extending these surfaces during the transition:
- Match the surrounding code (`<VBtn>`, `<VTextField>`, `<v-data-table-server>`, etc.)
- Reference the pre-F2 Vuexy registry via git: `git show 1c449ff6204cae6371da08c34ea8934d6b2ffcb8:dev-docs/VUEXY_COMPONENTS.md`
- Vuexy template reference (when needed): `resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/` — TypeScript Vue version is the only valid path
Do **not** introduce PrimeVue components inside an un-migrated surface
("no back-porting" — see `PRIMEVUE_COMPONENTS.md` §9).
#### On new surfaces (created during or after F4)
Start in PrimeVue. The migration phase is not a license to add new
Vuetify code.
### Vue components
- Always `<script setup lang="ts">` — never the Options API
- Type props with `defineProps<{...}>()`
- Declare emits with `defineEmits<{...}>()`
### API calls
- Use TanStack Query (`useQuery` / `useMutation`) for **all** API calls
- Never call axios directly from a component — always via a composable under `composables/api/use[Module].ts`
- `src/lib/axios.ts` is the canonical axios instance — the only place axios is imported directly
- Use Pinia stores for cross-component state — no prop drilling
### TypeScript
- No `any` types — ever. Every variable, prop, emit, return type, ref, computed must be fully typed
- Types first: create `src/types/[module].ts` before composables or components
- Mirror backend PHP Enums as `as const` objects in `src/types/`:
```typescript
export const ShiftStatus = { PENDING: 'pending', APPROVED: 'approved' } as const
export type ShiftStatus = typeof ShiftStatus[keyof typeof ShiftStatus]
```
### Forms
The canonical form pattern depends on the migration phase of the surface:
**Target state (migrated surfaces, new surfaces):** `@primevue/forms` +
Zod resolver via the `<FormField>` wrapper. Full API specification in
[RFC-WS-FRONTEND-PRIMEVUE Appendix A](./dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md#appendix-a--formfield-api-specification);
Crewli conventions in [`PRIMEVUE_COMPONENTS.md` §5](./dev-docs/PRIMEVUE_COMPONENTS.md).
One Zod schema per form, field names mirror backend Form Request keys
(snake_case), 422 errors merge via `useFormError(formRef)`.
**Legacy state (un-migrated surfaces, transient until each F4 sub-package):**
- `ref({ field: ... })` for form state
- `VForm` ref + per-field rules drawn from `@core/utils/validators`
(`requiredValidator`, `emailValidator`, etc.)
- A separate `errors: Ref<Record<string, string>>` for server-validation
feedback (mapped from 422 responses)
- **Zod** for runtime validation of API payloads/responses (in
`apps/app/src/schemas/*.ts`) — schemas already mirror backend Form
Requests and carry forward unchanged into the target state
- No inline validation logic in components
A single form is either fully Zod-resolver-validated (target) or fully
`:rules`-validated (legacy) — never a hybrid. VeeValidate is **NOT** in
the stack on either side of the migration.
Reference forms (legacy pattern, will migrate during F4):
`apps/app/src/components/sections/CreateShiftDialog.vue`,
`apps/app/src/components/timetable/AddPerformanceDialog.vue`,
`apps/app/src/pages/register/[public_token].vue`.
### Naming
- 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
- Component framework selection: see "UI framework strategy" above and [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md). PrimeVue + Tailwind v4 on migrated/new surfaces; Vuetify on un-migrated surfaces during F4
- Do not write custom CSS when a framework utility (Tailwind on migrated surfaces, Vuetify utilities on legacy surfaces) exists
- Responsive: mobile-first, usable from 375px width
- **Three states per page:** every data-driven view must handle loading (skeleton), error (`Message` / `v-alert` with retry button), and empty (helpful message with action button) — both frameworks support this pattern
- Responsive layout: Tailwind grid (`grid grid-cols-12 gap-4` + `col-span-N md:col-span-M`) on migrated surfaces; Vuetify `v-row` + `v-col` with breakpoint props on legacy surfaces — no fixed pixel widths
- Custom CSS via `<style scoped>` only as last resort when no framework utility / `pt` API / Aura token can do the job
## Forbidden patterns
- 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`)
- Never: TypeScript `any` type (use proper types, generics, or `unknown` with type guards)
- Never: import axios directly in a component (use `src/lib/axios.ts` via a composable)
## Frontend import boundaries (apps/app/)
`apps/app/` enforces a layered import architecture via
`eslint-plugin-boundaries`. Ten zones (`types` → `utils` → `lib` →
`plugins` / `composables` / `stores` / `navigation` → `components` →
`layouts` → `pages`); each zone may only import from the zones below
it in the matrix. Vendored `@core/` and `@layouts/` are exempt.
Cross-zone violations are lint errors, not warnings.
Matrix details + rationale: `dev-docs/WS-3-SESSION-1C-AUDIT.md`.
Config: `apps/app/.eslintrc.cjs`.
When adding a new file: pick the zone first. If your file imports
from a zone the matrix forbids, the structural answer is usually to
hoist a type to `types/` or extract a helper to `utils/` /
`composables/` — not to disable the rule. Per-line disables are
allowed only with a `TODO TECH-*` reference to a backlog item.
## Order of work for each new module
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. TypeScript types in `src/types/[module].ts`
11. API composable in `src/composables/api/use[Module].ts`
12. Pinia store in `src/stores/use[Module]Store.ts` (only if cross-component state is needed)
13. Vue page component in `src/pages/[module]/`
14. Add route in Vue Router
> **Framework note for steps 1314 during F4 migration:** new pages
> follow the PrimeVue + Tailwind conventions in [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md).
> If the new module is grafted onto a not-yet-migrated surface (rare),
> match the surrounding Vuetify style and let the surface's F4
> sub-package migrate it later.
## Diagnostic discipline: audit before assume
When debugging or fixing any bug, the first action is to verify the
canonical model against the artifact in question — not to write a fix
based on the symptom or hypothesis.
This applies to:
- Schema drift (verify the resource shape against the Zod schema, line
by line)
- Filter logic (verify the data model in SCHEMA.md before assuming a
controller is wrong)
- UX divergence (verify the prototype line-by-line, not via spot check)
- Test failures (verify the assertion matches the documented contract,
not the current implementation)
Phase A of every fix prompt is STOP-and-report. No code is written
before the audit is reviewed.
This principle was formalised after three consecutive incidents where
initial hypotheses were wrong and the audit gate caught them: B1
(controller assumed buggy, seeder was wrong), B5 (enum-shape assumed
drifted, decimals were wrong), and timetable UX (test-passing layer
diverged from prototype, mechanical-vs-UX split surfaced via browser
test).
## User Documentation (VitePress)
End-user documentation lives in /docs/ as a VitePress site.
Developer documentation (SCHEMA.md, API.md, etc.) lives in /dev-docs/.
### When to write docs
When completing a feature that introduces or changes **user-facing behaviour**,
create or update the corresponding documentation page under /docs/.
### How to write docs
1. Read /docs/.templates/style-guide.md for terminology, tone, and structure rules
2. Use /docs/.templates/feature-page.md or concept-page.md as your starting template
3. Every page MUST have frontmatter with: title, description, tags
4. Use Dutch language for all content (informal "je/jij")
5. Use Crewli terminology from the style guide — never English equivalents in user docs
### What to include
- What the feature does (2-3 sentences)
- Step-by-step instructions from the user's perspective
- Which roles have access (table format)
- Screenshot placeholders where visual guidance helps: `![Beschrijving](./images/placeholder.png)`
- Links to related pages
### File placement
Match the existing structure under /docs/:
- Organizer features → /docs/organizer/[category]/
- Volunteer features → /docs/volunteer/
- Portal features → /docs/portal/
- General concepts → /docs/guide/
## Git Commit Policy
**After every successful task completion, commit the changes immediately.**
Rules:
1. After all tests pass and build succeeds, stage and commit the changed files
2. Use conventional commit messages: `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`
3. One commit per logical unit of work (one feature, one bugfix, one refactor)
4. Never bundle unrelated changes in a single commit
5. Never commit with failing tests
6. Do NOT push automatically — only commit locally. The developer will push manually.
Commit message format:
```
feat: short description of the feature
fix: short description of the bug fix
refactor: short description of the refactoring
docs: short description of documentation changes
test: short description of test additions
```
- After a feature branch is merged into main, delete it locally
(`git branch -d <branch>`) and remotely (`git push origin --delete
<branch>`) immediately. Stale feature branches accumulate cognitive
load and obscure SHA-level history when squash-merges leave commit
hashes orphaned on the original branch.
Examples:
- `feat: person tags system with org-level skills and sync endpoint`
- `fix: auth race condition on page refresh`
- `docs: update SCHEMA.md with person_identity_matches table`