From a97922d6a4c6f8cc0477f1f3449db330de89ad2f Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Thu, 23 Apr 2026 20:34:34 +0200 Subject: [PATCH] docs(form-builder): document S3a PR 2 complex field types and update FORM-05 stub note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add VitePress pages for AVAILABILITY_PICKER and SECTION_PRIORITY and a TAG_PICKER configuration note. Wire them into the organisator sidebar under a new Formulieren section alongside the existing "Wat is een formulier" page. - BACKLOG.md: nuance FORM-05 — the stub-shaped behaviour for public event_registration submissions is already shipping via the existing TriggerPersonIdentityMatchOnFormSubmit listener (writes 'pending'). The real work (PersonIdentityService::detectMatchesByValues + an extra branch in resolveStatus) is what remains. Added a done entry for S3a PR 2 to the Opgeloste items list. - API.md: add VALIDATION_FAILED to the public-form error code table and document the SECTION_PRIORITY shape error messages (Dutch copy served under errors."values.{slug}"). - COPY_CATALOGUE.md: new S3a PR 2 section capturing the seeder help_text, the IdentityMatchBanner copy (clearly marking the backend message as authoritative), all empty/error state copy for the three new components, and the SECTION_PRIORITY shape error strings. Co-Authored-By: Claude Opus 4.7 (1M context) --- dev-docs/API.md | 19 ++++ dev-docs/BACKLOG.md | 32 ++++--- dev-docs/COPY_CATALOGUE.md | 86 +++++++++++++++++++ docs/.vitepress/config.ts | 21 +++++ .../forms/field-types/availability-picker.md | 72 ++++++++++++++++ .../forms/field-types/section-priority.md | 84 ++++++++++++++++++ .../organizer/forms/field-types/tag-picker.md | 26 ++++++ 7 files changed, 328 insertions(+), 12 deletions(-) create mode 100644 docs/organizer/forms/field-types/availability-picker.md create mode 100644 docs/organizer/forms/field-types/section-priority.md create mode 100644 docs/organizer/forms/field-types/tag-picker.md diff --git a/dev-docs/API.md b/dev-docs/API.md index 5001f774..b5795021 100644 --- a/dev-docs/API.md +++ b/dev-docs/API.md @@ -809,10 +809,29 @@ Codes: | `SCHEMA_NOT_FOUND` | 404 | public token does not resolve | | `SUBMISSION_ALREADY_SUBMITTED`| 409 | submit on finalised submission | | `RATE_LIMITED` | 429 | includes `Retry-After` header | +| `VALIDATION_FAILED` | 422 | per-field validation — see `errors` map below | Authoritative source: `api/app/Exceptions/FormBuilder/PublicFormApiException.php` and its concrete subclasses. +`VALIDATION_FAILED` returns the `errors` field keyed by `values.{slug}` +with an array of Dutch messages. Field-shape triggers include: + +- **SECTION_PRIORITY** — values must be `{ section_id, priority }[]` + with unique section_ids and priorities in `1..5`, max 5 entries, and + section_ids scoped to the schema's owner event tree. Specific + messages land under `errors."values.{slug}"`: + - `"Dezelfde sectie mag slechts één keer worden opgegeven."` + - `"Elke prioriteit mag slechts één keer worden toegekend."` + - `"priority moet tussen 1 en 5 liggen (positie {n})."` + - `"Je kunt maximaal 5 voorkeuren opgeven."` + - `"Eén of meer secties horen niet bij dit evenement."` + - `"Ongeldig formaat voor sectievoorkeuren."` / `"Ongeldig voorkeur-element op positie {n}."` + / `"section_id ontbreekt op positie {n}."` / `"priority ontbreekt of is ongeldig op positie {n}."` + +Authoritative source for shape rules: +`api/app/Services/FormBuilder/FormValueService::validateSectionPriorityShape`. + ### `GET /public/forms/{public_token}` Returns `PublicFormSchemaResource`. Shape: diff --git a/dev-docs/BACKLOG.md b/dev-docs/BACKLOG.md index 5881de65..3a8cc77a 100644 --- a/dev-docs/BACKLOG.md +++ b/dev-docs/BACKLOG.md @@ -341,21 +341,28 @@ shifts claimen zonder toegang tot de Organizer app. ### FORM-05 — Smart identity-match on public submission values -Public form submissions (subject_type=null) currently always get -identity_match_status='pending' because the listener needs a Person -to match against and public submissions don't create one yet. +**Stub-status (S3a PR 2, 2026-04-23):** Public event_registration +submissions landen al met `identity_match_status='pending'` via de +bestaande `TriggerPersonIdentityMatchOnFormSubmit` listener. De portal +`IdentityMatchBanner` leest dit veld en toont de juiste copy. Contract +ligt vast in `tests/Feature/FormBuilder/Listeners/TriggerPersonIdentityMatchOnFormSubmitTest`. -Improve: when a public event_registration submission arrives, extract -email + first_name + last_name from the submission values (via the -schema's binding config) and call a new PersonIdentityService method: - detectMatchesByValues(array $values, string $organisationId): MatchResult +**Resterend werk (de eigenlijke FORM-05):** public form submissions +(subject_type=null) krijgen momenteel *altijd* 'pending' omdat er nog +geen Person bestaat om tegen te matchen. Breid uit met: -Set identity_match_status to matched / pending / none based on actual -lookup. This gives the portal-form UX a meaningful signal instead of -a constant pending. +- Nieuwe methode op PersonIdentityService: + `detectMatchesByValues(array $values, string $organisationId): MatchResult` +- Een extra tak in `TriggerPersonIdentityMatchOnFormSubmit::resolveStatus` + die voor public submissions de values uit `FormSubmission->values` + extraheert (email / first_name / last_name via de schema binding), + deze methode aanroept, en 'matched' / 'pending' / 'none' schrijft. -Priority: Medium. Can bundle with organizer person_identity_matches UI -(which is also still a frontend gap). +Zo krijgt de portal-UX een betekenisvol signaal in plaats van een +constante 'pending'. + +Prioriteit: Medium. Kan gebundeld worden met de organizer +`person_identity_matches` UI (ook nog een frontend gap). --- @@ -491,6 +498,7 @@ De volgende items zijn geïmplementeerd en afgerond (673+ tests): - ~~Registration settings (show_in_registration)~~ ✅ - ~~Premium portal wizard (banner, branding, success page)~~ ✅ - ~~Global error handling (useNotificationStore + axios 422 interceptor)~~ ✅ +- ~~S3a PR 2: TAG_PICKER / AVAILABILITY_PICKER / SECTION_PRIORITY renderen in het publieke registratieformulier. Seeder uitgebreid met twee showcase-velden + parent-level VOLUNTEER time slot + duplicate section name voor dedup-dekking. SECTION_PRIORITY waarde-shape gevalideerd in FormValueService. `FormSubmissionResource` krijgt admin-facing `identity_match` block. 64 nieuwe assertions over backend + Vitest.~~ ✅ --- diff --git a/dev-docs/COPY_CATALOGUE.md b/dev-docs/COPY_CATALOGUE.md index 85e815a1..4b6d9c53 100644 --- a/dev-docs/COPY_CATALOGUE.md +++ b/dev-docs/COPY_CATALOGUE.md @@ -118,3 +118,89 @@ public_token_rotation: kunnen nog 7 dagen inzenden met de oude link; daarna krijgen ze een 410 Gone foutmelding." ``` + +## S3a PR 2 — Public form field types & identity-match + +### Seeder help_text (showcase demo fields) + +``` +beschikbaarheid.help_text: + "Vink alle dagdelen aan waarop je kunt werken." + +sectie_voorkeur.help_text: + "Sleep je voorkeuren in volgorde. Nummer 1 is je eerste keuze." +``` + +### IdentityMatchBanner — single source of truth is the backend + +Copy is served by PublicFormSubmissionResource::formatIdentityMatch() +in `identity_match.message`. The frontend fallbacks in +`IdentityMatchBanner.vue` duplicate these exactly and must stay in sync +whenever the backend copy changes. + +``` +identity_match.pending.title: "We controleren je gegevens" +identity_match.pending.body: + "We kijken of je al bekend bent bij de organisator. Je gegevens + worden automatisch gekoppeld zodra zij dit bevestigen." +(backend message: + "We controleren of je al bekend bent bij de organisator. Je gegevens + worden gekoppeld zodra zij dit bevestigen.") + +identity_match.matched.title: "Gegevens gekoppeld" +identity_match.matched.body: + "Je bent automatisch gekoppeld aan je bestaande account bij de + organisator." +(backend message: + "Je account is gekoppeld aan een bekende deelnemer.") + +identity_match.none.title: "Aanmelding ontvangen" +identity_match.none.body: + "De organisator neemt contact met je op zodra je aanmelding is + verwerkt." +(backend message: + "Geen bestaand account gevonden — je wordt als nieuwe deelnemer + geregistreerd.") +``` + +### Empty-state copy (public form field components) + +``` +FieldTagPicker.empty: + "Er zijn nog geen tags beschikbaar voor dit formulier." + +FieldAvailabilityPicker.empty: + "Er zijn nog geen tijdsloten beschikbaar." + +FieldAvailabilityPicker.error: + "Kon beschikbaarheidsopties niet laden." [button: "Opnieuw proberen"] + +FieldSectionPriority.empty: + "Er zijn nog geen secties gepubliceerd voor registratie." + +FieldSectionPriority.error: + "Kon secties niet laden." [button: "Opnieuw proberen"] + +FieldSectionPriority.cap_hint: + "Maximaal {max_priorities} voorkeuren" + +FieldSectionPriority.first_rank_hint: + "Tik of sleep een sectie hieronder om je eerste voorkeur te kiezen." +``` + +### SECTION_PRIORITY — FormValueService shape errors + +Returned under `errors."values.{slug}"` in the standard +`VALIDATION_FAILED` envelope. + +``` +"Ongeldig formaat voor sectievoorkeuren." +"Je kunt maximaal 5 voorkeuren opgeven." +"Ongeldig voorkeur-element op positie {n}." +"section_id ontbreekt op positie {n}." +"priority ontbreekt of is ongeldig op positie {n}." +"priority moet tussen 1 en 5 liggen (positie {n})." +"Dezelfde sectie mag slechts één keer worden opgegeven." +"Elke prioriteit mag slechts één keer worden toegekend." +"Eén of meer secties horen niet bij dit evenement." +``` diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 1b3a4c7e..a252cc51 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -124,6 +124,27 @@ export default defineConfig({ { text: "Berichten", link: "/organizer/communication/messages" }, ], }, + { + text: "Formulieren", + items: [ + { + text: "Wat is een formulier", + link: "/organizer/forms/concepts/wat-is-een-formulier", + }, + { + text: "Beschikbaarheid", + link: "/organizer/forms/field-types/availability-picker", + }, + { + text: "Sectievoorkeur", + link: "/organizer/forms/field-types/section-priority", + }, + { + text: "Tags kiezen", + link: "/organizer/forms/field-types/tag-picker", + }, + ], + }, ], "/volunteer/": [ { diff --git a/docs/organizer/forms/field-types/availability-picker.md b/docs/organizer/forms/field-types/availability-picker.md new file mode 100644 index 00000000..8cb9be7c --- /dev/null +++ b/docs/organizer/forms/field-types/availability-picker.md @@ -0,0 +1,72 @@ +--- +title: Beschikbaarheid opgeven (AVAILABILITY_PICKER) +description: Laat vrijwilligers aanvinken op welke dagdelen ze kunnen werken — gekoppeld aan de tijdslots van je evenement. +tags: [formulieren, veldtypes, beschikbaarheid, tijdslot, vrijwilliger] +--- + +# {{ $frontmatter.title }} + +{{ $frontmatter.description }} + +## Wat doet dit veld + +Het veldtype **Beschikbaarheid** toont alle tijdslots van het evenement +waar het formulier bij hoort, en laat de invuller aanvinken op welke +slots hij of zij kan werken. Je gebruikt dit veld in +vrijwilligersregistraties, zodat je bij het indelen van diensten direct +weet wanneer iemand beschikbaar is. + +Crewli haalt de opties automatisch op: alle tijdslots van het evenement +(of bij een festival ook van de onderliggende dagen) met +**persoonstype vrijwilliger**. Je hoeft zelf geen opties toe te voegen +— zodra je een tijdslot aanmaakt, verschijnt die in het formulier. + +## Hoe ziet het eruit voor de invuller + +De vrijwilliger ziet de tijdslots gegroepeerd per dag, bijvoorbeeld: + +> **Zaterdag 11 juli** +> +> ☐ Zaterdag middag (12:00–18:00) +> ☐ Zaterdag avond (18:00–02:00) + +Bij een festival met meerdere dagen verschijnt boven elk blok de naam +van het onderliggende evenement (bijvoorbeeld "Dag 1 — Vrijdag"), zodat +duidelijk is bij welk programma-onderdeel het tijdslot hoort. Bij een +flat evenement — alle tijdslots onder één evenement — laat Crewli die +extra kop weg, zodat de lijst rustig blijft. + +## Wanneer gebruik je dit veld + +- **Vrijwilligersregistratie** voor een festival of evenement met + meerdere dagdelen. +- **Aanmeldformulieren** waar je in de intake al wil weten welke + slots iemand kan draaien. +- **Post-event wijzigingen** — een vrijwilliger die achteraf nog een + slot aanvult. + +Gebruik dit veld **niet** wanneer je op shift-niveau (niet tijdslot) +wil uitvragen — daar is het dienstenoverzicht in het +vrijwilligersportaal voor bedoeld. + +## Koppeling met diensten + +Als je het formulier bij een evenement publiceert, koppelt Crewli de +aangevinkte slots automatisch aan de juiste persoon bij het indelen. +Hiermee kun je bij het toewijzen van diensten direct filteren op +"alleen personen die voor dit tijdslot beschikbaar zijn". + +## Rollen en toegang + +| Rol | Kan veld toevoegen aan formulier | Kan inzendingen lezen | +|---|---|---| +| Organisatie-admin | Ja | Ja | +| Evenementmanager | Ja | Ja | +| Vrijwilligerscoördinator | Nee | Ja | +| Vrijwilliger | Nee | Alleen eigen inzending | + +## Gerelateerde pagina's + +- [Wat is een formulier](../concepts/wat-is-een-formulier.md) +- [Tijdslots](../../shifts/time-slots.md) +- [Sectievoorkeur opgeven](./section-priority.md) diff --git a/docs/organizer/forms/field-types/section-priority.md b/docs/organizer/forms/field-types/section-priority.md new file mode 100644 index 00000000..5d982cef --- /dev/null +++ b/docs/organizer/forms/field-types/section-priority.md @@ -0,0 +1,84 @@ +--- +title: Sectievoorkeur opgeven (SECTION_PRIORITY) +description: Laat vrijwilligers hun top-voorkeuren voor secties op volgorde zetten, zodat je bij het indelen de meest wenselijke plek weet. +tags: [formulieren, veldtypes, sectie, voorkeur, vrijwilliger] +--- + +# {{ $frontmatter.title }} + +{{ $frontmatter.description }} + +## Wat doet dit veld + +Het veldtype **Sectievoorkeur** laat de invuller een top-lijst van +secties maken: eerste keus, tweede keus, enzovoort. Bij het indelen +van diensten zie je per persoon direct waar ze het liefst werken, en +kun je daar — waar mogelijk — op sturen. + +Je hoeft zelf geen opties te configureren. Crewli toont automatisch +alle secties van het evenement die je hebt gemarkeerd als zichtbaar op +de registratiepagina. Bij een festival worden ook de secties van de +onderliggende dagen getoond, en secties met dezelfde naam op meerdere +dagen worden samengevoegd tot één optie. + +## Hoe ziet het eruit voor de invuller + +De vrijwilliger ziet twee lijstjes onder elkaar: + +- **Jouw voorkeuren** — de secties die al gekozen zijn, op volgorde + genummerd (1, 2, 3…). Je kunt ze op desktop slepen om de volgorde te + wijzigen; op mobiel tik je het kruisje om een keuze te verwijderen + en kies je opnieuw. +- **Nog te kiezen** — de overgebleven secties, als aantikbare kaarten. + Tikken voegt de sectie als volgende voorkeur toe. + +Bij elke sectie kun je een korte omschrijving meegeven +(bijvoorbeeld "Tap bier en drankjes voor festivalgangers") — die +verschijnt onder de sectienaam en helpt de invuller te kiezen. + +## Configuratie + +### Maximum aantal voorkeuren + +Standaard mag een invuller maximaal **5 voorkeuren** opgeven. Wil je +minder? Zet `max_priorities` in de validatieregels van het veld op +bijvoorbeeld 3 — dan kan de invuller alleen een top-3 maken. Hoger dan +5 wordt niet toegestaan. + +### Welke secties verschijnen + +Crewli toont alleen secties die: + +- **Zichtbaar op registratiepagina** zijn (zet je aan op de sectie). +- Niet als **cross-event sectie** zijn gemarkeerd (die zijn bedoeld + voor interne indeling, niet voor vrijwilligersvoorkeur). + +Wil je een sectie niet meer tonen op het formulier? Zet dan +"Zichtbaar op registratiepagina" uit — je sectie blijft gewoon bestaan +voor intern gebruik. + +## Wanneer gebruik je dit veld + +- **Vrijwilligersregistratie** wanneer je meerdere secties hebt en + vrijwilligers liever op de ene dan op de andere plek staan. +- **Crewaanmelding** voor een festival waar sommige secties populair + zijn en je inzicht wil in de verdeling. + +Gebruik het niet wanneer je maar één sectie hebt, of wanneer indeling +puur op basis van beschikbaarheid gebeurt — dan is een +enkele-keuze-veld of helemaal geen veld duidelijker. + +## Rollen en toegang + +| Rol | Kan veld toevoegen aan formulier | Kan inzendingen lezen | +|---|---|---| +| Organisatie-admin | Ja | Ja | +| Evenementmanager | Ja | Ja | +| Vrijwilligerscoördinator | Nee | Ja | +| Vrijwilliger | Nee | Alleen eigen inzending | + +## Gerelateerde pagina's + +- [Wat is een formulier](../concepts/wat-is-een-formulier.md) +- [Secties beheren](../../shifts/sections.md) +- [Beschikbaarheid opgeven](./availability-picker.md) diff --git a/docs/organizer/forms/field-types/tag-picker.md b/docs/organizer/forms/field-types/tag-picker.md new file mode 100644 index 00000000..31548dcc --- /dev/null +++ b/docs/organizer/forms/field-types/tag-picker.md @@ -0,0 +1,26 @@ +--- +title: Tags kiezen (TAG_PICKER) +description: Laat invullers hun eigen vaardigheden, certificaten of interesses aanvinken uit jouw tag-bibliotheek. +tags: [formulieren, veldtypes, tags, vaardigheden, vrijwilliger] +--- + +# {{ $frontmatter.title }} + +{{ $frontmatter.description }} + +## Configuratie: alleen specifieke categorieën tonen + +Standaard toont een tag-veld **alle actieve tags** van je organisatie, +gegroepeerd per categorie. Wil je dat de invuller alleen uit een +bepaalde set kan kiezen — bijvoorbeeld alleen veiligheidscertificaten, +niet persoonlijke voorkeuren — dan vul je in de validatieregels de +gewenste categorieën in via `tag_categories`. + +Tags zonder categorie verschijnen automatisch onder de kop +**"Overig"**, zodat ze vindbaar blijven zonder dat je elke tag +expliciet hoeft in te delen. + +## Gerelateerde pagina's + +- [Tags beheren](../../persons/tags.md) +- [Wat is een formulier](../concepts/wat-is-een-formulier.md)