diff --git a/dev-docs/COPY_CATALOGUE.md b/dev-docs/COPY_CATALOGUE.md new file mode 100644 index 00000000..85e815a1 --- /dev/null +++ b/dev-docs/COPY_CATALOGUE.md @@ -0,0 +1,120 @@ +# Crewli — In-app copy catalogue + +> Single source of truth for user-facing Dutch copy. Seeded from +> `/dev-docs/ARCH-FORM-BUILDER.md` §30. + +## Purpose + +Centralises terminology and warnings so every screen, tooltip, and +error message stays consistent. Without this, "Dienst" / "Shift" / +"Taak" drift across pages and confuse organisers; "Privacy-gevoelig" +and "Bevat persoonsgegevens" appear side-by-side for the same flag. + +This catalogue is the canonical reference for both backend validation +messages (Form Requests) and frontend rendering (Vue components). +Whenever you write Dutch copy that a user will read, look here first. + +## Growth strategy + +Living document. Each session that adds user-facing UI: + +1. **Check first.** Search for the concept (in Dutch and English) before + coining a new term. If it's here, use the existing wording verbatim. +2. **Propose new terms in PR.** Add to the relevant section. Brief + rationale in the commit message. +3. **Reviewer (Bert) signs off** on terminology before merge. Once a + term is in the catalogue, it's binding for future sessions. + +The catalogue stays in `/dev-docs/` (developer-facing) rather than +`/docs/` (end-user-facing). End-user docs are written *using* this +catalogue, not *about* it. + +## Update workflow + +- Adding a tooltip → update §30.2 here. +- Adding a warning → update §30.3 here. +- Renaming a concept → update §30.1 here AND grep for all current + Dutch occurrences (frontend, validation messages, end-user docs) + and update them in the same PR. +- Wholesale restructure → discuss in design-document.md first; then + rewrite this catalogue in one commit. + +## Naming conventions + +| Concept | Canonical Dutch term | Never use | +| -------------------- | ---------------------------- | ---------------------------------- | +| `form_schema` | Formulier | Schema, template | +| `form_field` | Veld | Vraag, item | +| `form_template` | Formulier-sjabloon | Template (alleen in dev-docs) | +| `form_field_library` | Veldenbibliotheek | Library, bibliotheek alleen | +| `form_submission` | Inzending | Submission, antwoord | +| `is_filterable` | Filterbaar | Queryable, zoekbaar | +| `is_pii` | Bevat persoonsgegevens | Privacy-gevoelig | +| `freeze_on_submit` | Bevriezen na inzending | Vergrendelen | +| `consent_version` | Toestemmingsversie | Consent-versie | + +## Tooltip catalogue (selection) + +``` +is_filterable: + "Filterbaar — dit veld wordt extra geïndexeerd voor snelle filtering + in overzichten. Alleen aanvinken voor velden die je daadwerkelijk als + filter gebruikt (bijvoorbeeld: shirtmaat wel, motivatie niet)." + +is_pii: + "Bevat persoonsgegevens — bij retentie-verwerking worden deze waardes + geanonimiseerd volgens je privacy-instellingen. Vink aan voor velden + zoals telefoon, e-mail, noodcontact, medische info." + +is_unique: + "Uniek per formulier — waardes van dit veld moeten uniek zijn over alle + inzendingen heen. Geschikt voor bijvoorbeeld BSN of werknemersnummer. + Dubbele waardes worden afgewezen." + +freeze_on_submit: + "Bevriezen na eerste inzending — zodra iemand het formulier indient + kunnen de velden niet meer gewijzigd worden. Gebruik dit voor + contracten, signatures, of formulieren waar de structuur vast moet + staan voor audit-doeleinden." + +snapshot_mode: + never: "Geen snapshot — wijzigingen worden alleen in het activity log + bijgehouden." + on_submit: "Snapshot bij inzending — bij elke indiening wordt het + complete formulier gesnapshot voor audit-doeleinden." + always: "Altijd snapshot — elke wijziging (ook drafts) wordt + gesnapshot. Gebruikt meer opslag maar biedt het volledige + audit-spoor." + +retention_days: + "Bewaartermijn — na deze periode (vanaf inzendingsdatum) worden + PII-velden automatisch geanonimiseerd. Typische waardes: 1095 dagen + (3 jaar) voor vrijwilligers, 2555 dagen (7 jaar) voor contracten, + null voor onbeperkt bewaren." +``` + +## Warning catalogue (selection) + +``` +binding_change_with_submissions: + "Je staat op het punt de koppeling van dit veld te wijzigen terwijl er + al {count} ingediende inzendingen zijn. De historische waardes blijven + bestaan, maar zijn niet meer de bron-van-waarheid. Dit kan niet + ongedaan worden gemaakt." + +delete_schema_with_submissions: + "Dit formulier heeft {count} inzendingen. Als je het verwijdert, blijven + de inzendingen bewaard als archief maar zijn niet meer nieuw in te + dienen. Type de naam van het formulier om te bevestigen:" + +field_type_change: + "Je wijzigt het veldtype van {old} naar {new}. Bestaande waardes worden + mogelijk niet correct omgezet — sommige kunnen onleesbaar worden. + Aanbevolen: maak een nieuw veld aan in plaats van dit veld te + wijzigen." + +public_token_rotation: + "Je roteert de publieke link voor dit formulier. Bestaande gebruikers + kunnen nog 7 dagen inzenden met de oude link; daarna krijgen ze een + 410 Gone foutmelding." +``` diff --git a/dev-docs/SCHEMA.md b/dev-docs/SCHEMA.md index c6ebc8ab..cff27a3d 100644 --- a/dev-docs/SCHEMA.md +++ b/dev-docs/SCHEMA.md @@ -669,6 +669,13 @@ $effectiveDate = $shift->end_date ?? $shift->timeSlot->date; ## 3.5.4 Volunteer Profile & History +> **SUPERSEDED by S1 of the form-builder refactor.** The old +> `volunteer_profiles` table was a planning placeholder that was never +> physically created. It has been split: user-universal columns moved +> to the new `user_profiles` table (§3.5.12), and event-variable / skill +> columns moved to `form_fields` and `person_tags`. See §3.5.12 for the +> new structure and the column-by-column crosswalk. + ### `volunteer_profiles` | Column | Type | Notes | @@ -1924,3 +1931,59 @@ Immutable audit record of every email sent. No soft deletes. **Indexes:** `(organisation_id, created_at)`, `(recipient_email, created_at)`, `(template_type, status)`, `(event_id)`, `(person_id)` **Relations:** `belongsTo` organisation (nullable), event (nullable), person (nullable), user (nullable), triggeredBy → user **Soft delete:** no — immutable audit table + +--- + +## 3.5.12 Form Builder + +> See `/dev-docs/ARCH-FORM-BUILDER.md` v1.2 for the authoritative +> specification. This SCHEMA.md section is a summary only and will be +> fully rewritten at the end of S6. +> +> **Legacy tables retained intentionally.** The tables +> `registration_form_fields`, `person_field_values`, and +> `registration_field_templates` remain in the schema through end of S1. +> They are dropped atomically in the first S2 commit together with the +> removal of legacy controllers, services, requests, resources, policies, +> and routes. DevSeeder and FormBuilderDevSeeder no longer write to them; +> they hold zero rows in dev but the schema is preserved for environments +> with real legacy data that will be migrated via +> `forms:migrate-legacy-data`. + +**Crosswalk: legacy `volunteer_profiles` → new locations** + +| Legacy column | New location | +| ------------------------------ | ----------------------------------------------- | +| `bio` | `user_profiles.bio` | +| `photo_url` | `user_profiles.photo_url` | +| `tshirt_size` | `form_fields` (Pattern B, per event) | +| `first_aid` | `person_tags` (system-seeded, S3) | +| `driving_licence` | `person_tags` (system-seeded, S3) | +| `allergies` | `form_fields` (Pattern B, per event) | +| `access_requirements` | `form_fields` (Pattern B, per event) | +| `emergency_contact_name` | `user_profiles.emergency_contact_name` | +| `emergency_contact_phone` | `user_profiles.emergency_contact_phone` | +| `reliability_score` | `user_profiles.reliability_score` | +| `is_ambassador` | `user_profiles.is_ambassador` | + +### New tables (S1) + +| Table | Purpose | +| --- | --- | +| `user_profiles` | User-universal profile (bio, photo, emergency contact, reliability_score, is_ambassador, opaque settings JSON). 1:1 with `users`, auto-created by `UserObserver`. ARCH §4.13. | +| `form_schemas` | Form definition. Polymorphic owner (event / user_profile / artist / company / null). Carries purpose, submission_mode, is_published, public_token, schema_snapshot policy, freeze_on_submit, retention/consent. ARCH §4.1. | +| `form_schema_sections` | Optional sections within a schema (used when `section_level_submit=true`). ARCH §4.8. | +| `form_field_library` | Reusable cross-schema field definitions. ARCH §4.7. | +| `form_fields` | Field within a schema. `field_type` stored as string (custom types via `CustomFieldTypeRegistry`). Carries binding (Pattern A/B/C), is_filterable, is_pii, conditional_logic, value_storage_hint. ARCH §4.2. | +| `form_submissions` | One submission per `(schema, subject)`. Polymorphic subject. Carries status, review_status, schema_snapshot copy, submission lifecycle timestamps, search_index. ARCH §4.3. | +| `form_submission_section_statuses` | Per-section status when `section_level_submit=true`. ARCH §4.9. | +| `form_submission_delegations` | "X fills in this submission on behalf of Y". ARCH §4.10. | +| `form_values` | EAV row per `(submission, field)`. Integer AI PK for fast joins. Typed columns (value_indexed/number/date/bool) populated by `FormValueObserver`. ARCH §4.4. | +| `form_value_options` | Filter pivot for multi-value fields (MULTISELECT/CHECKBOX_LIST/TAG_PICKER). Rebuilt by observer. ARCH §4.5. | +| `form_templates` | Org-scoped reusable schema snapshots. `is_system` for shipped templates. ARCH §4.6. | +| `form_schema_webhooks` | Webhook subscriptions per schema. URL/secret encrypted via Eloquent cast. ARCH §4.11. | +| `form_webhook_deliveries` | Webhook delivery audit + retry queue. ARCH §4.12. | + +**Activity log strategy:** explicit calls via `FormSchema::logSchemaChange()` and `FormField::logFieldChange()` — no `LogsActivity` trait (would produce noise). Only impactful events logged (publish toggle, purpose change, binding change, is_pii toggle, etc.). Bulk-fixture suppression via `App\Support\ActivityLog::suppressed(fn() => …)` which flips `config('activitylog.enabled')` for the callback. + +**Multi-tenancy:** `OrganisationScope` applied on `FormSchema`, `FormTemplate`, `FormFieldLibrary`. Other form-builder tables inherit isolation through their parent schema; `FormSchemaWebhook` documents this discipline explicitly via a docblock warning to never query directly without an eager constraint. diff --git a/dev-docs/form-builder-getting-started.md b/dev-docs/form-builder-getting-started.md new file mode 100644 index 00000000..73edd1c8 --- /dev/null +++ b/dev-docs/form-builder-getting-started.md @@ -0,0 +1,230 @@ +# Form Builder — Developer Getting Started + +> Onboarding for developers writing code against the new form-builder +> tables. Companion to `/dev-docs/ARCH-FORM-BUILDER.md` v1.2 (the +> authoritative spec) and `/dev-docs/SCHEMA.md` §3.5.12. + +--- + +## Prerequisites + +- Laravel 12 / PHP 8.2+. +- Familiarity with Eloquent, ULIDs, polymorphic relationships. +- Read ARCH §0 (TL;DR) and §3 (FormPurpose catalogue). + +--- + +## The mental model in one paragraph + +A `FormSchema` is a form definition. `FormFields` belong to it. A +`FormSubmission` is one filled-in instance of a schema, identified by its +polymorphic subject (a Person, User, Company, Organisation, Event, or +nothing for public forms). `FormValues` are the EAV row per +`(submission, field)` pair, JSON payload + typed columns populated by an +observer based on the field's storage hint and filterability. Every +schema declares its purpose (`event_registration`, `incident_report`, +…) which constrains lifecycle and integrations. + +--- + +## Creating a new schema with two fields + +```php +use App\Enums\FormBuilder\FormFieldType; +use App\Enums\FormBuilder\FormPurpose; +use App\Enums\FormBuilder\FormSubmissionMode; +use App\Models\Event; +use App\Models\FormBuilder\FormField; +use App\Models\FormBuilder\FormSchema; + +$event = Event::find($eventId); + +$schema = FormSchema::create([ + 'organisation_id' => $event->organisation_id, + 'owner_type' => 'event', + 'owner_id' => $event->id, + 'name' => "{$event->name} — registratie", + 'slug' => "{$event->slug}-registratie", + 'purpose' => FormPurpose::EVENT_REGISTRATION, + 'submission_mode' => FormSubmissionMode::DRAFT_SINGLE, + 'is_published' => false, + 'locale' => 'nl', +]); + +FormField::create([ + 'form_schema_id' => $schema->id, + 'field_type' => FormFieldType::SELECT->value, + 'slug' => 'shirtmaat', + 'label' => 'Shirtmaat', + 'options' => ['XS', 'S', 'M', 'L', 'XL', 'XXL'], + 'is_required' => true, + 'is_filterable' => true, + 'value_storage_hint' => FormFieldType::SELECT->recommendedValueStorageHint(), + 'sort_order' => 1, +]); + +FormField::create([ + 'form_schema_id' => $schema->id, + 'field_type' => FormFieldType::TEXTAREA->value, + 'slug' => 'opmerkingen', + 'label' => 'Opmerkingen', + 'value_storage_hint' => FormFieldType::TEXTAREA->recommendedValueStorageHint(), + 'sort_order' => 2, +]); +``` + +Then to log a meaningful change: + +```php +$schema->is_published = true; +$schema->save(); +$schema->logSchemaChange('schema.published'); +``` + +Trivial label/help_text edits should NOT call `logSchemaChange` — see +ARCH §17.1 for the curated event list. + +--- + +## Creating a submission and its values + +```php +use App\Enums\FormBuilder\FormSubmissionStatus; +use App\Models\FormBuilder\FormSubmission; +use App\Models\FormBuilder\FormValue; + +$person = \App\Models\Person::find($personId); + +$submission = FormSubmission::create([ + 'form_schema_id' => $schema->id, + 'subject_type' => 'person', + 'subject_id' => $person->id, + 'submitted_by_user_id' => $person->user_id, + 'status' => FormSubmissionStatus::SUBMITTED, + 'submitted_at' => now(), + 'is_test' => false, +]); + +// One value per field. The shape of `value` is JSON-wrapped per field type. +FormValue::create([ + 'form_submission_id' => $submission->id, + 'form_field_id' => $shirtmaatField->id, + 'value' => ['value' => 'L'], +]); + +FormValue::create([ + 'form_submission_id' => $submission->id, + 'form_field_id' => $opmerkingenField->id, + 'value' => ['value' => 'Graag een nachtshift'], +]); +``` + +`FormValueObserver` automatically populates `value_indexed` for filterable +single-value fields, `value_number` for NUMBER hint, `value_date` for +DATE hint, `value_bool` for BOOL hint, and the `form_value_options` pivot +for multi-value filterable fields (MULTISELECT / CHECKBOX_LIST / +TAG_PICKER). You don't write to those columns directly. + +--- + +## Adding a new subject type + +Three places to update — keep them in sync: + +1. **Morph map** (`app/Providers/AppServiceProvider.php`): + ```php + Relation::enforceMorphMap([ + // …existing entries… + 'artist' => \App\Models\Artist::class, + ]); + ``` + +2. **Subject registry** (`config/form_subjects.php`): + ```php + 'artist' => [ + 'model' => \App\Models\Artist::class, + 'display_attribute' => 'name', + 'permission_check' => \App\Policies\ArtistPolicy::class.'@view', + ], + ``` + +3. **Reverse relation on the model** itself: + ```php + public function formSubmissions(): MorphMany + { + return $this->morphMany(\App\Models\FormBuilder\FormSubmission::class, 'subject'); + } + ``` + +The verifier (`forms:verify-data-integrity`) cross-checks subject_type +against the `config/form_subjects.php` keys; an unknown subject_type +will fail the submission-coherence check. + +--- + +## Registering a custom field type + +The `field_type` column on `form_fields` is a string, not a DB enum, so +custom types can be added at runtime via configuration. Under the hood +this is the `CustomFieldTypeRegistry` planned for ARCH §17.2 — for now, +register the type's value in `config/form_builder.php`: + +```php +'custom_field_types' => [ + 'COLOR_PICKER' => [ + 'label' => 'Kleurkeuze', + 'storage_hint' => 'string', + 'filterable' => true, + ], +], +``` + +The verifier accepts any field_type that's either in `FormFieldType::values()` +or in `config('form_builder.custom_field_types')`. Frontend rendering +and validation handlers come in S6 when the registry interface lands. + +--- + +## Suppressing activity log during bulk operations + +`logSchemaChange` and `logFieldChange` produce one activity-log row per +call. In bulk fixture runs (DevSeeder, the data-migration command, +import scripts) this floods `activity_log` with hundreds of rows that +provide no audit value. Wrap the bulk operation: + +```php +use App\Support\ActivityLog; + +ActivityLog::suppressed(function () use ($schemas): void { + foreach ($schemas as $schemaData) { + $schema = FormSchema::create($schemaData); + // logSchemaChange calls inside this closure are silent no-ops. + } +}); +``` + +The helper flips `config('activitylog.enabled')` for the duration of +the callback and restores it in `finally`. Both our explicit calls AND +the spatie `LogsActivity` trait (used on `Organisation` elsewhere) +respect the flag via `ActivityLogger::log()`. + +Do NOT use this in regular request paths — the activity log is the +audit trail; suppressing it silently is an antipattern outside of +fixtures and one-shot commands. + +--- + +## Where to look next + +- **ARCH-FORM-BUILDER.md §4** — full column specs for every table. +- **ARCH-FORM-BUILDER.md §6** — binding patterns (entity-owned / + form-owned / mirrored). +- **ARCH-FORM-BUILDER.md §7** — filter architecture and the + `FilterQueryBuilder` interface (S4). +- **`config/form_binding.php`** — Entity Column Registry (Pattern A/C + binding targets). +- **`config/form_subjects.php`** — Subject Type Registry. +- **`config/form_builder.php`** — limits, webhook policy, captcha, + retention, feature flags. +- **`forms:migrate-legacy-data`** + **`forms:verify-data-integrity`** — + see the migration playbook in this directory for the operator's view. diff --git a/dev-docs/form-builder-migration-playbook.md b/dev-docs/form-builder-migration-playbook.md new file mode 100644 index 00000000..f325e621 --- /dev/null +++ b/dev-docs/form-builder-migration-playbook.md @@ -0,0 +1,186 @@ +# Form Builder — Migration Playbook + +> Operator runbook for migrating legacy `registration_form_fields` / +> `person_field_values` / `registration_field_templates` data into the new +> `form_*` structure. Companion to `/dev-docs/ARCH-FORM-BUILDER.md` §11. + +--- + +## Status (post-S1) + +**Legacy tables retained intentionally.** The tables `registration_form_fields`, +`person_field_values`, and `registration_field_templates` remain in the +schema through end of S1. They are dropped atomically in the first S2 +commit together with the removal of legacy controllers, services, requests, +resources, policies, and routes. DevSeeder and FormBuilderDevSeeder no +longer write to them; they hold zero rows in dev but the schema is preserved +for environments with real legacy data that will be migrated via +`forms:migrate-legacy-data`. + +Until S2 lands, the legacy registration UI remains functional for any +environment that depends on it. + +--- + +## When to use this playbook + +Run this when promoting an environment that holds real legacy +registration data into the new form-builder schema. For development +machines starting from `migrate:fresh --seed`, no migration is needed — +DevSeeder produces new-structure data directly. + +--- + +## Pre-flight audit + +Before touching any data, confirm the working state matches expectations. + +```bash +# 1. Working tree clean? +git status + +# 2. On the expected branch? +git log -1 --oneline + +# 3. Database backup exists? (production / staging only — your call for dev) + +# 4. Verify the new tables are present and the legacy tables still hold data +php artisan db:show | grep -E "form_|registration_form_fields|person_field_values|registration_field_templates" +``` + +If the legacy tables are absent, the migration command will detect this +and exit cleanly without doing work — there is nothing to migrate. + +--- + +## Step 1 — Dry run + +Always start with a dry run. It writes nothing and prints the same summary +table the real run would produce. + +```bash +php artisan forms:migrate-legacy-data --dry-run +``` + +**What to look for:** +- The per-organisation log lines list every event that will get a new + schema and the number of fields/submissions per event. +- The summary table shows projected counts for `form_schemas`, + `form_fields`, `form_submissions`, `form_values`, `form_templates`. +- Exit code is `0`. + +If anything looks surprising — wildly off counts, organisations missing, +templates duplicated — STOP and investigate. Do not proceed to step 2 until +the dry-run output is what you expect. + +--- + +## Step 2 — Run the migration + +```bash +php artisan forms:migrate-legacy-data +``` + +**What happens:** +- Per organisation, inside a transaction (one bad org rolls back only itself). +- Per event in that org: one `form_schemas` row + N `form_fields` rows. +- Per person with `person_field_values` for that event: one `form_submissions` + row (status = `submitted` if person.status ∈ applied/approved/no_show + else `draft`) + one `form_values` row per legacy value. +- `FormValueObserver` populates typed columns and the + `form_value_options` pivot during the inserts (per ARCH §7.2). +- All `registration_field_templates` rows are wrapped in the ARCH §4.6.1 + `schema_snapshot` shape and inserted as `form_templates`. +- The whole run is wrapped in `App\Support\ActivityLog::suppressed(...)` + so the activity log isn't flooded by hundreds of `logSchemaChange` / + `logFieldChange` entries from the bulk inserts. +- After migration completes, verification runs automatically. Final + exit code is whatever `forms:verify-data-integrity` returns. + +**Idempotent:** if a `form_schemas` row already exists for `(org, event, +event_registration)`, that event is skipped silently. Safe to re-run. + +--- + +## Step 3 — Verify + +Verification runs automatically at the end of step 2. To re-run it +standalone (or to verify state without touching data): + +```bash +php artisan forms:verify-data-integrity +``` + +Or, equivalently: + +```bash +php artisan forms:migrate-legacy-data --verify-only +``` + +**Nine checks:** +1. Schema coherence (purpose enum, custom_purpose_slug ⇄ purpose=custom, public_token uniqueness) +2. Field coherence (FK valid, field_type ∈ FormFieldType ∪ custom registry, is_filterable matches type, slug unique within schema, binding JSON valid against `config/form_binding.php`) +3. Submission coherence (subject_type/subject_id consistency, subject_type ∈ `config/form_subjects.php`, status ⇄ submitted_at) +4. Value coherence (orphans, unique pair, length, multi-value sanity, value_indexed only set when field.is_filterable) +5. User profile coherence (every user has one profile; reliability_score ∈ [0, 5]) +6. Data migration counts (skipped when legacy tables absent) +7. Orphan records (pivot rows, section statuses) +8. Section/schema relation consistency +9. (`--strict`) Strict reachability: value.field.schema must equal value.submission.schema + +Exit code `0` means all checks passed. Anything else is a real failure +worth investigating before proceeding. + +--- + +## Step 4 — Spot-check a sample + +After verification is green, eyeball a sample submission to confirm the +shape is right. + +```bash +php artisan tinker +>>> use App\Models\FormBuilder\FormSubmission; +>>> $sub = FormSubmission::with(['schema', 'values.field'])->latest()->first(); +>>> $sub->schema->name; +>>> $sub->values->map(fn($v) => [$v->field->slug => $v->value]); +``` + +Compare against the legacy view of the same person's data in +`person_field_values` to confirm nothing was silently lost. + +--- + +## Rollback + +The data-migration command does not have a `down()` step. If a migration +went wrong, the recovery path is: + +1. Restore from backup (production / staging). +2. For dev: `php artisan migrate:fresh --seed` rebuilds everything from scratch. + +The legacy tables are preserved during the migration — they are not +modified. So even after running `forms:migrate-legacy-data`, the legacy +data is still there (until S2 drops the tables). This means a production +data-migration error can be recovered by truncating the new form_* tables +and re-running with the underlying legacy data still intact. + +--- + +## Common gotchas + +- **`registration_form_fields` table absent:** the command detects this + and exits 0 silently. Expected behaviour for fresh environments. +- **PII heuristic miss:** the heuristic in §11.2.1 of the ARCH covers + obvious patterns (email, phone, geboort, allergie, …) but is not + exhaustive. After migration, organisers can fine-tune `is_pii` per + field via the form-builder UI (S5). +- **`is_published` derived from event status:** the command considers an + event "published" when its status is one of registration_open / buildup + / showday / teardown / closed. If the legacy registration was open on + events with other statuses, adjust `is_published` manually after migration. +- **Custom field types:** the command maps the 10 legacy field types + (text/textarea/select/multiselect/checkbox/radio/boolean/number/tag_picker/heading) + into the new uppercase enum values. If your legacy data has values + outside this set, the command will set them to `TEXT` and the verifier + will flag them as invalid `field_type`. diff --git a/docs/organizer/forms/concepts/wat-is-een-formulier.md b/docs/organizer/forms/concepts/wat-is-een-formulier.md new file mode 100644 index 00000000..928b8c2e --- /dev/null +++ b/docs/organizer/forms/concepts/wat-is-een-formulier.md @@ -0,0 +1,77 @@ +--- +title: Wat is een formulier +description: Uitleg van het formulierensysteem in Crewli — één bouwsteen voor registratie, advancing, incidenten en meer. +tags: [formulieren, introductie, organisator] +--- + +# {{ $frontmatter.title }} + +{{ $frontmatter.description }} + +## In het kort + +Een **formulier** in Crewli is een verzameling velden die je kunt +samenstellen voor één specifiek doel — bijvoorbeeld een +vrijwilligersregistratie, een artist advance, of een incidentrapportage. +Iedere ingevulde versie van een formulier heet een **inzending**, en die +inzending hangt aan een onderwerp: een persoon, een artiest, een +bedrijf, een evenement, of niemand (bij een publiek formulier). + +Eén systeem, veel doelen. Of je nu wilt dat vrijwilligers zich aanmelden +voor je festival of dat je crew incidenten kan rapporteren tijdens een +shift — je gebruikt steeds dezelfde formulierbouwer. + +## Hoe past dit in Crewli? + +Vroeger had Crewli alleen registratieformulieren per evenement. Het +nieuwe formulierensysteem doet hetzelfde, maar voor élk soort +gegevensverzameling. Je vindt formulieren terug op: + +- De **registratiepagina** van een evenement (vrijwilligers melden zich aan). +- De **artist portal** (artiesten leveren hun rider en advancing aan). +- Het **incidentdashboard** (crew rapporteert wat er is gebeurd). +- De **profielpagina** van vrijwilligers (bio, noodcontact, voorkeuren). +- **Publieke webpagina's** (perskaart-aanvragen, klachten, RSVP's). + +Per formulier bepaal je wie het mag invullen, of het meerdere keren of +één keer ingevuld kan worden, of het bevroren wordt na inzending, en +hoe lang de gegevens bewaard blijven. + +## Voorbeeld + +### 1. Vrijwilligersregistratie + +Je organiseert *Echt Feesten 2026*. Je maakt een formulier met velden +zoals shirtmaat, dieetwensen, noodcontact en motivatie. Je publiceert +het formulier; vrijwilligers vullen het in via de aanmeldlink. Iedere +ingediende registratie verschijnt in je personenoverzicht, klaar om +shifts toe te wijzen. + +### 2. Artist advance + +Een artiest komt over twee weken optreden. Je stuurt ze de +advance-link. Ze openen het formulier en vullen per sectie hun +gegevens in: contactpersoon, technische rider, hospitality-wensen, +transport. Jij ziet per sectie of de artiest klaar is en kunt per +sectie goedkeuren of feedback vragen. + +### 3. Incidentrapportage + +Tijdens het festival valt iemand van de bar. Een crewlid pakt z'n +telefoon, opent het incidentformulier, vult het tijdstip, de locatie, +de ernst en wat er is gedaan in. Direct na inzending wordt het +formulier bevroren — niemand kan de inhoud nog wijzigen, wat belangrijk +is voor verzekering en juridische opvolging. + +## Gerelateerde concepten + +In de volgende sessies komen er meer pagina's bij over: + +- Velden toevoegen en sorteren +- Formulieren publiceren en de publieke link +- Inzendingen beoordelen en exporteren +- Sjablonen hergebruiken tussen evenementen + +Voor nu: maak een evenement aan en bekijk het meegeleverde +registratieformulier — dat is een goede startplek om het systeem te +verkennen.