feat(fp): WP-23 — org-template backend + admin role

Second template axis (org identity: letterhead, footer, signature,
margins) server-side: OrgTemplateStore with JSON version history,
publish/rollback, sent-brief version pinning, admin role + capability,
5 admin endpoints, org-logo upload category. FE seam widened only
(Role/Capability unions, interceptor); WP-24/26 consume it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 11:17:05 +02:00
parent 44eb2d2186
commit 5a610c10f0
31 changed files with 5017 additions and 1834 deletions

View File

@@ -0,0 +1,90 @@
# WP-26 — Admin org-template editor
Status: todo
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
## Why
The org template (WP-23) needs its editor: an admin edits, per sub-organization, the
letter's appearance **in place on the same canvas** the drafter composes on — the
mirror image (`editableRegions='template'`: letterhead/footer/signature editable,
content a read-only sample). PRD Brief v2 §5, §7h.
## Read first
- PRD Brief v2 §5, §7h; `docs/backlog/WP-23/24/25` (endpoints, canvas, proefbrief)
- `src/app/shared/application/access.store.ts` (`can('orgtemplate:edit')`)
- `.claude/skills/form-machine` — the house form idiom this editor follows
- `src/app/shared/ui/upload/single-upload` (logo upload reuse)
## Decisions (pre-made, don't relitigate)
- **Lives in the `brief` context** (route `/brief/huisstijl`, lazy) — same bounded
capability, no new context. Gated by `AccessStore.can('orgtemplate:edit')` with a
denial alert (deny-by-default); no new route guard.
- **House form-machine idiom**: `org-template.machine.ts`
(`OrgTemplateState`/`OrgTemplateMsg`, pure `reduce` + spec) — draft fields are form
state, not brief state. Store (`org-template.store.ts`, root singleton) does
debounced draft save (mirror `BriefStore.scheduleSave`), publish, rollback.
- **Publish shows impact first**: confirmation displays the WP-23 impact count
("Dit raakt N nog niet verzonden brieven") before the POST.
- **Version history is a list, rollback copies into draft** (WP-23 semantics) —
no side-by-side rendered diff (deferred; field-level history list is enough here).
- **Logo upload reuses `single-upload`** against the `org-logo` category; the canvas
shows `<img src="/api/v1/uploads/{id}/content">`.
- **Proefbrief** = the WP-25 admin preview endpoint; just a button.
- Margins are bounded number inputs (server re-validates, WP-23).
## Files
- `src/app/brief/domain/org-template.machine.ts` (+spec)
- `src/app/brief/application/org-template.store.ts`
- `src/app/brief/infrastructure/org-template.adapter.ts` (+spec, `parseOrgTemplateAdminView`)
- `src/app/brief/ui/org-template-editor/*` (organism + stories)
- `src/app/brief/ui/org-template.page.ts`
- `src/app/app.routes.ts` (route `brief/huisstijl`)
## Steps
1. Machine (fields, `FieldEdited`/`MarginEdited`/`LogoSet`/`DraftLoaded`/save-publish
outcome Msgs) + spec.
2. Adapter (generated client CRUD + parse boundary) + spec.
3. Store: load (sub-org list + selected), debounced save, publish (impact confirm),
rollback.
4. Editor UI: sub-org switcher, canvas in `'template'` mode with inline-editable
regions, margins inputs, logo upload, version history + rollback, proefbrief
button, publish bar showing live version + published-at.
5. Route + capability gate + stories (axe).
## Acceptance criteria
- [ ] `?role=admin` can switch sub-orgs, edit all template fields in place on the
canvas, and see the canvas update live.
- [ ] Draft saves are debounced; publish asks for confirmation showing the impact
count; after publish the drafter's canvas (WP-24) reflects it on reload.
- [ ] Version history lists published versions (who is faked, when is real);
rollback copies an old version into the draft.
- [ ] Non-admin on `/brief/huisstijl` sees the denial alert; API would 403 anyway.
- [ ] Logo upload validates type/size client-side (existing `rejectReason`) and
renders on the canvas after upload.
- [ ] Machine spec covers field edits, dirty tracking, publish/rollback outcomes.
- [ ] Full GREEN.
## Verification
GREEN one-liner; manual walk: admin edits footer + margin → canvas live-updates →
proefbrief shows draft → publish (impact count) → `?role=drafter` reload shows new
appearance; second sub-org unaffected.
## Out of scope
Template approval chain (four-eyes on templates — PRD open question, out for POC).
Rendered side-by-side version diff. Soft locks. New shared overlay/modal component —
the publish confirmation uses the existing inline confirmation pattern, not a dialog.
## Risks
The editor is the first consumer of `editableRegions='template'` — WP-24 built the
input but nothing exercised it; budget for canvas fixes here. Debounced draft save +
publish can race — flush the draft save before publishing (same
`clearTimeout`+`flushSave` discipline as `BriefStore.transition`).