docs(form-builder): document S3a PR 2 complex field types and update FORM-05 stub note

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 20:34:34 +02:00
parent 9256c05db0
commit a97922d6a4
7 changed files with 328 additions and 12 deletions

View File

@@ -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:

View File

@@ -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.~~ ✅
---

View File

@@ -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."
```

View File

@@ -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/": [
{

View File

@@ -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:0018:00)
> ☐ Zaterdag avond (18:0002: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)

View File

@@ -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)

View File

@@ -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)