docs: registration form fields, section preferences & form redesign

Update SCHEMA.md (v1.8), design-document.md (v1.9), and API.md with
EAV system for dynamic event-specific registration fields, section
preferences, tag picker sync architecture, and field templates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 21:42:36 +02:00
parent 2102a35688
commit fcff3b0344
3 changed files with 385 additions and 22 deletions

View File

@@ -4,7 +4,7 @@ Product Design & Technical Specification
Full Stack SaaS — Event & Festival Management Platform
**Version:** 1.8 | **Datum:** April 2026 | **Status:** Concept | **Tech Stack:** Laravel 12 + Vue 3
**Version:** 1.9 | **Datum:** April 2026 | **Status:** Concept | **Tech Stack:** Laravel 12 + Vue 3
# 1. Product Vision & Scope
@@ -329,7 +329,7 @@ Onderstaand het volledige, productie-waardige datamodel. Alle 12 bevindingen uit
| **organisations** | id (ULID), name, slug, billing_status, settings (JSON: display prefs only), created_at, deleted_at | hasMany events, crowd_types, accreditation_categories. settings JSON alleen voor opaque UI-config, niet voor queryable data. Soft delete: ja. |
| **organisation_user** | id (int AI), user_id, organisation_id, role | Pivot. Integer PK voor join-performance. FK: users, organisations. Spatie role via pivot. |
| **user_invitations** | id (ULID), email, invited_by_user_id, organisation_id, event_id (nullable), role, token (ULID, unique), status (pending\|accepted\|expired), expires_at | Token in uitnodigingsmail. Bij accept: zoek bestaand account op email of maak nieuw aan. INDEX: (token), (email, status). |
| **events** | id (ULID), organisation_id, parent_event_id (ULID FK nullable → events, nullOnDelete), name, slug, start_date, end_date, timezone, status (draft\|published\|registration_open\|buildup\|showday\|teardown\|closed), event_type (enum: event\|festival\|series, default: event), event_type_label (string nullable), sub_event_label (string nullable), is_recurring (bool, default: false), recurrence_rule (string nullable), recurrence_exceptions (JSON nullable), deleted_at | belongsTo organisation. belongsTo event as parent (parent_event_id). hasMany events as children (parent_event_id). hasMany festival_sections, time_slots, persons, artists, briefings. Soft delete: ja. INDEX: (organisation_id, status), (parent_event_id), UNIQUE(organisation_id, slug). Zie sectie 3.4.1 voor event type model. |
| **events** | id (ULID), organisation_id, parent_event_id (ULID FK nullable → events, nullOnDelete), name, slug, start_date, end_date, timezone, status (draft\|published\|registration_open\|buildup\|showday\|teardown\|closed), event_type (enum: event\|festival\|series, default: event), event_type_label (string nullable), sub_event_label (string nullable), is_recurring (bool, default: false), recurrence_rule (string nullable), recurrence_exceptions (JSON nullable), registration_show_section_preferences (bool, default: true — v1.9), registration_show_availability (bool, default: true — v1.9), deleted_at | belongsTo organisation. belongsTo event as parent (parent_event_id). hasMany events as children (parent_event_id). hasMany festival_sections, time_slots, persons, artists, briefings. Soft delete: ja. INDEX: (organisation_id, status), (parent_event_id), UNIQUE(organisation_id, slug). Zie sectie 3.4.1 voor event type model. |
| **event_user_roles** | id (int AI), user_id, event_id, role | Pivot. Integer PK. FK: users, events. |
### 3.5.2 Locaties (nieuw — oplossing probleem 3)
@@ -371,10 +371,13 @@ Oplossing probleem 1 (identiteitsfragmentatie): persons krijgt user_id (nullable
| **Tabel** | **Belangrijkste kolommen** | **Relaties, constraints & opmerkingen** |
|----|----|----|
| **crowd_types** | id (ULID), organisation_id, name, system_type (CREW\|GUEST\|ARTIST\|VOLUNTEER\|PRESS\|PARTNER\|SUPPLIER), color, icon, is_active | Org-level configuratie. INDEX: (organisation_id, system_type). |
| **persons** | id (ULID), user_id (nullable FK users), event_id, crowd_type_id, company_id (nullable), name, email, phone, status (invited\|applied\|pending\|approved\|rejected\|no_show), is_blacklisted, admin_notes, custom_fields (JSON), deleted_at | user_id nullable: externe gasten/artiesten hebben geen platform-account. UNIQUE(event_id, user_id) WHERE user_id IS NOT NULL. INDEX: (event_id, crowd_type_id, status), (email, event_id), (user_id, event_id). custom_fields JSON OK: event-specifieke velden, niet queryable. Soft delete: ja. |
| **persons** | id (ULID), user_id (nullable FK users), event_id, crowd_type_id, company_id (nullable), first_name, last_name, date_of_birth (date nullable — v1.9), email, phone, status (invited\|applied\|pending\|approved\|rejected\|no_show), is_blacklisted, admin_notes, remarks (text nullable — v1.9, vrijwilliger-bewerkbaar), custom_fields (JSON), deleted_at | user_id nullable: externe gasten/artiesten hebben geen platform-account. UNIQUE(event_id, user_id) WHERE user_id IS NOT NULL. INDEX: (event_id, crowd_type_id, status), (email, event_id), (user_id, event_id). custom_fields JSON: backward compat + opaque data; voor queryable registratiedata gebruik `person_field_values`. Soft delete: ja. |
| **companies** | id (ULID), organisation_id, name, type (supplier\|partner\|agency\|venue\|other), contact_name, contact_email, contact_phone, deleted_at | Gedeeld over events binnen org. Soft delete: ja. INDEX: (organisation_id). |
| **crowd_lists** | id (ULID), event_id, crowd_type_id, name, type (internal\|external), recipient_company_id (nullable), auto_approve (bool), max_persons (int nullable) | hasMany persons via crowd_list_persons pivot. INDEX: (event_id, type). |
| **crowd_list_persons** | id (int AI), crowd_list_id, person_id, added_at, added_by_user_id | Nieuw pivot (oplossing probleem 4). Koppelt person aan crowd_list. UNIQUE(crowd_list_id, person_id). INDEX: (person_id). |
| **registration_form_fields** | id (ULID), event_id, label, slug (string 100), field_type (text\|textarea\|select\|multiselect\|checkbox\|radio\|boolean\|number\|tag_picker), options (JSON nullable), tag_category (string 50 nullable), is_required (bool), is_portal_visible (bool), is_admin_only (bool), is_filterable (bool), section (string 100 nullable), help_text (text nullable), sort_order (int), created_at, updated_at | v1.9 EAV-systeem voor dynamische registratievelden per event. tag_picker: toont organisatie-tags als selecteerbare opties. options JSON OK: opaque config. UNIQUE(event_id, slug). INDEX: (event_id, sort_order), (event_id, is_portal_visible, sort_order). Geen soft delete. |
| **person_field_values** | id (int AI), person_id, registration_form_field_id, value (text nullable), selected_options (JSON nullable) | v1.9 Antwoorden op registratievelden. value voor enkelvoudige velden, selected_options voor multiselect/checkbox/tag_picker. UNIQUE(person_id, registration_form_field_id). INDEX: (registration_form_field_id, value(191)). Geen soft delete. |
| **person_section_preferences** | id (int AI), person_id, festival_section_id, priority (tinyint 1-5) | v1.9 Sectievoorkeuren van vrijwilligers. Zachte hints, geen beloftes. UNIQUE(person_id, festival_section_id). INDEX: (festival_section_id, priority), (person_id). Geen soft delete. |
### 3.5.5a Person Tags & Vaardigheden (v1.8)
@@ -387,7 +390,15 @@ Tag-gebaseerd vaardigheden-/competentiesysteem voor vrijwilligers en crew. Tags
PersonResource wordt verrijkt met tags wanneer `user_id` is ingevuld. Filter endpoints: `?tag={id}` (enkelvoudig) en `?tags=id1,id2` (AND-logica: moet alle tags hebben).
### 3.5.5b Identiteitsmatching: Person ↔ User (v1.8 — ontwerp)
### 3.5.5b Registratievelden & Sectievoorkeuren (v1.9)
EAV-systeem voor dynamische event-specifieke registratievelden, ter vervanging van queryable gebruik van `persons.custom_fields` JSON. Organisatoren configureren per event welke extra velden verschijnen in het registratieformulier. Antwoorden worden queryable opgeslagen in `person_field_values`. Sectievoorkeuren worden apart vastgelegd in `person_section_preferences`.
Speciaal veldtype `tag_picker`: toont organisatie-tags als selecteerbare opties. Na account-aanmaak synchroniseert `TagSyncService` selecties naar `user_organisation_tags` met `source = self_reported`.
Tabellen: `registration_form_fields`, `person_field_values`, `person_section_preferences` (zie sectie 3.5.5 tabeloverzicht hierboven). Templates: `registration_field_templates` (organisatie-niveau, zelfde patroon als `crowd_types`).
### 3.5.5c Identiteitsmatching: Person ↔ User (v1.8 — ontwerp)
Enterprise-grade identiteitsresolutie met drie stappen: detectie → suggestie → bevestiging. Er vindt nooit stilzwijgend automatische koppeling plaats.
@@ -602,17 +613,33 @@ Vrijwilligers zijn de kern van elke festival-organisatie. Dit module ontlast de
### 4.4.1 Publiek registratieformulier (meerdelige structuur):
- **Deel 1 — Over jou: Naam, e-mail, telefoon (met landcode).**
- **Deel 1 — Over jou:** Naam, e-mail, telefoon (met landcode), geboortedatum.
Vaste velden, altijd aanwezig.
- **Deel 2 — Meer over jou: Shirtmaat, EHBO, allergieën, rijbewijs. Geconfigureerd via de formulierbouwer.**
- **Deel 2 — Extra informatie:** Dynamische velden geconfigureerd per event via
Registratievelden beheer (sectie 4.4.7). Voorbeelden: shirtmaat, allergieën,
dieetwensen, vergoedingskeuze, toestemming gegevensverwerking, noodcontact,
rijbewijs, EHBO-ervaring, opmerkingen. Elk veld heeft een configureerbaar type
(tekst, selectie, checkbox, ja/nee, getal, tag-kiezer). Kan verplicht worden
gemaakt en als filterbaar worden gemarkeerd voor gebruik bij shift-toewijzing.
- **Deel 3 — Motivatie: Waarom wil je vrijwilliger zijn? Dropdown + vrije tekst.**
Speciaal veldtype 'Tag-kiezer': toont de organisatie-tags (bijv. vaardigheden,
certificaten) als selecteerbare opties. Selecties worden na account-aanmaak
automatisch gesynchroniseerd naar het tagprofiel van de gebruiker als
self-reported tags.
- **Deel 4 — Voorkeurssecties: Selecteer secties en rangschik prioriteit 1-5 (drag-to-prioritize).**
- **Deel 3 — Voorkeurssecties:** Selecteer secties en rangschik prioriteit 1-5
(drag-to-prioritize). Begeleidende tekst: 'We proberen hier zoveel mogelijk
rekening mee te houden, maar de uiteindelijke indeling wordt bepaald door de
organisatie.' Per event aan/uit te zetten via event-instellingen
(`registration_show_section_preferences`).
- **Deel 5 — Beschikbaarheid: Selecteer Time Slots (gefilterd op VOLUNTEER type). Toont minimumurendrempel voor festivalpas.**
- **Deel 4 — Beschikbaarheid:** Selecteer Time Slots (gefilterd op VOLUNTEER type).
Per event aan/uit te zetten via event-instellingen
(`registration_show_availability`).
- **Deel 6 — Admin only: Blacklist toggle, betaalstatus, algemene notities.**
- **Deel 5 — Admin only (niet zichtbaar voor deelnemer):** Blacklist toggle,
algemene notities (admin_notes).
### 4.4.2 Vrijwilligersprofiel (platform-breed, suggestie 1):
@@ -670,6 +697,62 @@ Elke vrijwilliger heeft één platform-breed profiel. Eenmalig invullen, herbrui
- Na afloop: automatisch bedankbericht aan alle vrijwilligers. Gepersonaliseerd: naam, sectie, uren gedraaid, 'Zonder jou was dit festival niet mogelijk.'
### 4.4.7 Registratievelden beheer (event settings)
Organisatoren configureren per event welke extra velden verschijnen in het
registratieformulier (Deel 2). Dit biedt maximale flexibiliteit zonder
hardcoded velden.
Beheer via: Event Settings > Registratievelden.
Per veld instelbaar:
- Label (vraagtekst)
- Type: vrije tekst, tekstvak, enkele keuze (select/radio), meervoudige keuze
(multiselect/checkbox), ja/nee, getal, tag-kiezer
- Opties (bij keuze-velden): lijst van antwoordmogelijkheden
- Tag-categorie (bij tag-kiezer): optioneel filteren op tag-categorie
- Verplicht: ja/nee
- Zichtbaar voor deelnemer: ja/nee (admin-only velden zijn alleen zichtbaar
voor de organisator)
- Filterbaar: ja/nee (beschikbaar als filter bij het inplannen van shifts)
- Sectie: groepering in het formulier (bijv. 'Vergoeding', 'Toestemming')
- Helptekst: toelichting onder het veld
Voorbeelden van dynamische velden:
- 'Shirtmaat' (select: XS/S/M/L/XL/XXL) — filterbaar
- 'Dieetwensen' (multiselect: Vegetarisch/Veganistisch/Halal/Glutenvrij/
Lactosevrij/Geen pinda's/Geen noten) — filterbaar
- 'Vergoeding' (radio: Pro Deo / Entreeticket / Vrijwilligersvergoeding)
- 'Toestemming gegevensverwerking' (checkbox: verplicht, met lange helptekst)
- 'Ben je eerder vrijwilliger geweest?' (ja/nee) — filterbaar
- 'Certificaten & vaardigheden' (tag-kiezer, categorie: Certificaat) — tags
worden na account-aanmaak gesynchroniseerd naar het tagprofiel
- 'Noodcontact naam' (tekst) + 'Noodcontact telefoon' (tekst)
- 'Betaald' (ja/nee, admin-only) — alleen zichtbaar voor organisator
- 'Opmerkingen' (tekstvak)
Antwoorden worden queryable opgeslagen (EAV-structuur, niet als JSON) en zijn
beschikbaar als filter in de personenlijst en bij shift-toewijzing.
Snel starten met veelgebruikte velden:
- 'Kies uit templates': de organisatie heeft een bibliotheek van herbruikbare
veldtemplates (shirtmaat, dieetwensen, vergoeding, toestemming, etc.). Bij
het aanmaken van een organisatie worden systeemtemplates automatisch
aangemaakt. Organisatie-admins kunnen templates aanpassen en eigen templates
toevoegen via Organisatie Settings > Registratieveld-templates. Bij het
toevoegen van een veld aan een event wordt een kopie gemaakt — de event-
kopie is onafhankelijk van de template.
- 'Importeer velden van vorig event': kopieert alle registratievelden van een
eerder event van dezelfde organisatie naar het huidige event. Bestaande
velden blijven intact.
Tag-kiezer synchronisatie: wanneer een vrijwilliger tags selecteert via een
tag-kiezer veld, worden deze opgeslagen als registratieveld-antwoorden. Zodra
de vrijwilliger een platform-account krijgt (na goedkeuring of via identity
matching), synchroniseert het systeem de selecties automatisch naar
user_organisation_tags als self-reported tags. Organisator-toegekende tags
worden hierbij nooit overschreven.
## 4.5 Communicatiehub (suggestie 2)
Communicatie is op show-dag een van de grootste operationele risico's. Het platform biedt een centrale hub met drie niveaus van urgentie. De coordinator kiest urgentie — het systeem kiest het juiste kanaal.