# 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` → BSN `123456782`, 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 /me` is fetched once per page load, so an in-app link keeps the stale role. Reset with `?role=drafter&scenario=default`, the `⚙ state` dev panel, or a fresh tab. - **What the toggles don't reach**: `/brief/preview`, `/admin/org-template/{id}/preview` and `/brief/reveal-bignummer` are hand-written `fetch` calls (same seam as uploads) and bypass `scenarioInterceptor` — they can't be forced into a scenario state from the URL. `?scenario=empty` substitutes `[]` for every `/api/` response, which breaks any object-returning endpoint (e.g. `GET /api/v1/brief` parse-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 `` 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 `` 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).