Closes phase 6 (Brief v2): a demo script mapping shipped scenarios to URL+click paths (no Brief v2 PRD ever existed to translate one from — written directly against the code instead), one e2e spec covering compose→approve→send and admin republish→drafter-sees-it, and Storybook state gaps (rejection diff, read-only viewer, org logo, upload rejection) that prior WPs left uncovered. Flags passage-picker as dead code, superseded by besluit-panel. npm run e2e is not verified green in this sandbox — see WP-28's Deviations section; the pre-existing, unmodified smoke.spec.ts fails identically here, pointing at a sandbox rendering issue rather than a regression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
17 KiB
PRD 0003 — Brief v2 demo script
Status: Reference · Date: 2026-07-27 · Context: phase 6 "Brief v2" (WP-23..28)
Cross-references: WP-23 (org-template backend), WP-24 (letter canvas), WP-25 (server-rendered preview), WP-26 (admin org-template editor), WP-27 (brief UX layer — undo/redo, besluit-driven guidance, rejection diff).
No standalone "Brief v2" PRD was ever committed to this repo — WP-23..27 cite one informally ("PRD Brief v2 §N") but it only ever existed as chat context. This script is the demonstrable spec now: every row below is a scenario that actually exists in the shipped code, not a scenario list carried over from an external document.
Phase 6 ships two independent axes over the same letter: content (what the letter
says — drafted by the behandelaar, reviewed by an approver) and appearance (how it
looks — the org template, edited by an admin). This script walks both, plus the
degraded states each page can be in. No new scenario code backs this — it is a map onto
toggles that already exist: ?role=drafter|approver|admin, ?scenario=slow|loading|error,
and POST /brief/reset.
Before you start
- Login:
/login→ BSN123456782, any password → "Inloggen met DigiD". - Toggles are sticky per tab (
sessionStorage, WP-33/WP-37):?role=and?scenario=in the URL win once per navigation, then persist for the tab. Change role by a full navigation (typing the URL, not an in-app link) —GET /meis fetched once per page load, so an in-app link keeps the stale role. Reset with?role=drafter&scenario=default, the⚙ statedev panel, or a fresh tab. - What the toggles don't reach:
/brief/preview,/admin/org-template/{id}/previewand/brief/reveal-bignummerare hand-writtenfetchcalls (same seam as uploads) and bypassscenarioInterceptor— they can't be forced into a scenario state from the URL.?scenario=emptysubstitutes[]for every/api/response, which breaks any object-returning endpoint (e.g.GET /api/v1/briefparse-fails into the error state) — use it only where a list/empty-state is actually being demonstrated. - Reset: the "Opnieuw beginnen (demo)" button on
/brief(POST /brief/reset) resets the letter. Org templates have no reset endpoint — section E's walk ends with a restore step (E9) so the demo doesn't drift.
All URLs are relative to http://localhost:4200.
A — Setup
| # | Scenario | URL | Clicks |
|---|---|---|---|
| A1 | Fresh demo state | /brief?role=drafter |
"Opnieuw beginnen (demo)" |
B — Compose (drafter, the besluit-driven workflow)
| # | Scenario | URL | Clicks / expect |
|---|---|---|---|
| B1 | Case-framed drafting step | /brief?role=drafter |
case header (referentie, naam, BIG-nummer, beroep) + stepper Beoordelen → Brief opstellen → Indienen |
| B2 | Submit blocked on empty kern | (same) | "Indienen ter beoordeling" disabled + hint "Vul eerst alle verplichte secties en los fouten op." |
| B3 | Guided drafting — positief | (same) | radio "Positief besluit (toewijzen)" → kern fills with standaardteksten + info alert "…standaardtekst(en) toegevoegd op basis van het besluit…" |
| B4 | Guided drafting — negatief needs a reden | (same) | radio "Negatief besluit (afwijzen)" → warning "Kies een reden…" → tick "Onvoldoende scholing" → alert flips to info, kern updates |
| B5 | Selection survives reload | reload /brief |
the besluit panel re-seeds from the letter's own kern passages (no separate storage) |
| B6 | Free text + placeholder | (same) | in a section, "Vrije tekst toevoegen" → type in the rich-text editor → "Veld invoegen" dropdown to insert a placeholder chip |
| B7 | Diagnostics (deprecated / not-fillable) | (same) | insert oud_kenmerk (deprecated) and specialisme_code (not fillable for this beroep) → diagnostics panel shows a warning and an error |
| B8 | Undo / redo | (same) | toolbar "Ongedaan maken" / "Opnieuw uitvoeren", or Ctrl+Z / Ctrl+Shift+Z outside a text field |
| B9 | Autosave states | (same) | "Concept opslaan…" → "Concept opgeslagen" in the toolbar's live region |
| B10 | Autosave failure + retry | /brief?role=drafter&scenario=error, then edit |
"Niet opgeslagen — opnieuw proberen" → "Opnieuw proberen" |
| B11 | PII reveal + step-up + audit | /brief?role=drafter |
"Toon BIG-nummer" → native confirm dialog ("Extra verificatie vereist…") → unmasked; then /beheer/audit?role=admin lists the attempt (no PII in the log) |
C — Preview (one rendering, used twice: in-app and as the sent artifact)
| # | Scenario | URL | Clicks / expect |
|---|---|---|---|
| C1 | In-app preview modal | /brief?role=drafter |
"Voorbeeld" → a <dialog> with the read-only letter canvas |
| C2 | Zoom + sample values + page break | (in the dialog) | −/+/100% zoom controls; "Voorbeeld met testwaarden" swaps placeholder chips for sample text; on a long letter, a "±pagina-einde — afdrukvoorbeeld is leidend" mark appears |
| C3 | Server-rendered HTML document | (in the dialog) | "Openen als document (PDF)" → new tab, Content-Type: text/html, watermarked as a draft |
| C4 | Sent letter serves its frozen archive | after D9 | same button on the sent letter → the archived HTML (no watermark), the org-template version pinned at send time even if templates change afterwards |
D — Review (approver, segregation-of-duty)
| # | Scenario | URL | Clicks / expect |
|---|---|---|---|
| D1 | Submit | /brief?role=drafter |
"Indienen ter beoordeling" |
| D2 | Drafter waits (four-eyes) | /brief?role=drafter |
info alert "De brief wacht op beoordeling door een collega." — no approve/reject controls |
| D3 | Read-only viewer (e.g. admin) | /brief?role=admin |
info alert "Alleen-lezen weergave. De behandelaar stelt de brief op." — a pure viewer has no approve/reject/send capability |
| D4 | Approver review | /brief?role=approver |
"Goedkeuren" button + a reject-comment entry, both available |
| D5 | Reject with reason | (same) | fill the reject textarea → "Afwijzen" |
| D6 | Rejection visible to drafter | /brief?role=drafter |
"Afgewezen:" alert showing the comment |
| D7 | Rework + resubmit | (same) | edit the kern → "Opnieuw indienen" |
| D8 | Rejection diff | /brief?role=approver, same tab session as D5 |
"Toon wijzigingen" → changed/added blocks get an orange/green badge, plus a "N blok(ken) verwijderd sinds afwijzing." alert if any were deleted. POC limit: the diff snapshot is in-memory and reload-fragile — do D5→D8 without reloading in between |
| D9 | Approve → send | /brief?role=approver |
"Goedkeuren" → "Versturen" → "De brief is verzonden." |
E — Appearance (admin, org templates)
| # | Scenario | URL | Clicks / expect |
|---|---|---|---|
| E1 | Reach the editor | /brief/huisstijl?role=admin |
(or via the Beheer navigation) — sub-org switcher defaults to "BIG-register" |
| E2 | Denied without the capability | /brief/huisstijl?role=drafter |
capabilityGuard redirects straight to /dashboard (no denial page is ever rendered for this route — the guard runs before the component loads) |
| E3 | Edit in place on the letterhead | /brief/huisstijl?role=admin |
type into Organisatienaam / Retouradres / Afsluiting / Naam ondertekenaar / Functie ondertekenaar / Contactgegevens (voettekst) / Juridische voettekst → the canvas re-renders live; "Concept opgeslagen" |
| E4 | Margins | (same) | Boven / Rechts / Onder / Links (mm) number inputs, clamped 10–40mm |
| E5 | Logo upload | (same) | choose a file via the Logo file input → the letterhead <img> appears on the canvas |
| E6 | Proefbrief | (same) | "Proefbrief" → new tab: the unpublished draft rendered over the fixed sample letter |
| E7 | Publish with impact confirm | (same) | "Publiceren" → warning "Dit raakt N nog niet verzonden brieven. Publiceren?" → "Bevestigen" → "Gepubliceerde versie: N+1" |
| E8 | Invalid draft blocks publish | (same) | clear Organisatienaam or a signer field → "Publiceren" disabled + hint "Vul organisatienaam en ondertekenaar in; marges tussen 10 en 40 mm." |
| E9 | Rollback (also: restore after the walk) | (same) | Versiegeschiedenis → "Terugzetten in concept" on an old version copies it into the draft; "Publiceren" again makes it live — use this to put a demo sub-org back the way you found it |
| E10 | Two axes, one render | /brief?role=drafter after E7 |
"Voorbeeld" → the new letterhead over the same letter body that section B composed |
| E11 | Sub-org isolation | /brief/huisstijl?role=admin |
switch the sub-org dropdown to "CIBG Vakbekwaamheid" → its own values, untouched by edits made to "BIG-register"; Proefbrief on each proves it |
| E12 | Version pinning | send a letter (D9), then E7, then C4 | the already-sent letter's archived HTML is unaffected by a later template publish |
F — Degraded states (?scenario= on data pages)
| # | Scenario | URL | Expect |
|---|---|---|---|
| F1 | Slow | /brief?role=drafter&scenario=slow |
delay-gated skeleton (~250ms gate, then a multi-second response) |
| F2 | Never resolves | /brief?role=drafter&scenario=loading |
skeleton stays, aria-busy="true" |
| F3 | Load error + retry | /brief?role=drafter&scenario=error |
error alert "De brief kon niet worden geladen." + "Opnieuw proberen" |
| F4 | Admin editor when /me fails |
/brief/huisstijl?role=admin&scenario=error |
capabilityGuard awaits /me, sees it fail, denies by default → redirects to /dashboard (not a denial alert on /brief/huisstijl itself — the guard never lets the page render) |
Out of scope
New scenario-interceptor cases, a scenario-switcher UI, screenshots/video (per WP-28's Decisions).