Admin-only overview of all cases across owners + an admin delete, gated by a new
`cases:manage` capability (Authz role→cap + CanManageCases + CasesAdmin gate;
FE capability + guard + nav + role.interceptor prefix — the org-template/stamdata
recipe). Backend adds ApplicationStore.ListAll()/DeleteAny() and GET /admin/cases +
DELETE /admin/cases/{id}; admin delete removes ANY case incl. submitted. Page lives
in registratie/ui (owns the Aanvraag aggregate; reuses aanvraag-view + parse),
routed /beheer/zaken; delete guarded by a native confirm, optimistic with rollback.
Typed client regenerated (documents the new endpoints + owner field).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
50 lines
3.1 KiB
Markdown
50 lines
3.1 KiB
Markdown
# WP-36 — Admin cases page + admin delete
|
|
|
|
Status: done
|
|
Phase: 7 — refinements
|
|
|
|
## Why
|
|
|
|
Admins can maintain stamdata and org-templates but have no view of the cases (aanvragen) in the
|
|
register, and no way to remove an erroneous one. This WP adds an admin-only overview of **all**
|
|
cases across owners and an admin **delete** that can remove any case — the back-office counterpart
|
|
of the user's own dashboard.
|
|
|
|
## Decisions (made while building — no spec existed; flagged for review)
|
|
|
|
- **Single capability `cases:manage`** covers both the list and the delete (one back-office
|
|
concern), following the `orgtemplate:edit` / `stamdata:edit` precedent exactly (Authz role→cap +
|
|
a `CanManageCases` gate + a `CasesAdmin(ctx,…)` helper; FE `Capability` union + `me.adapter`
|
|
`KNOWN` + `capabilityGuard` + nav item + `role.interceptor` prefix).
|
|
- **Page lives in `registratie` (not `beheer`).** `registratie` owns the `Aanvraag` aggregate, so
|
|
the admin view reuses its `aanvraag-view` labels + `parseApplications` trust boundary instead of
|
|
duplicating them — and it respects the layer boundary (`beheer` may not import `registratie`).
|
|
This matches the existing pattern (stamdata-admin lives in `beheer` because `beheer` owns
|
|
stamdata; org-template-admin in `brief`). Routed at `/beheer/zaken` for a legible admin URL.
|
|
- **Admin delete removes ANY case** — any owner, submitted or not — unlike the user-facing
|
|
`DELETE /applications/{id}` (owner-scoped, 409 on a submitted case). That is the admin power.
|
|
- **Native `confirm()` guards the delete.** No confirm-dialog component exists (the only precedent
|
|
is a native `confirm()` in behandel-scherm); the delete is irreversible, so it gets a prompt
|
|
rather than the dashboard's no-confirm optimistic cancel.
|
|
- **Single owner in practice.** Only `DemoOwner` exists, so the list shows that owner's cases with
|
|
an Owner column; no fake multi-user seed was added (the endpoint is cross-owner-capable —
|
|
`ListAll()` — so real multi-owner data would just appear).
|
|
|
|
## Files
|
|
|
|
- Backend: `ApplicationStore.ListAll()` + `DeleteAny(id)`; `ApplicationSummaryDto.Owner` +
|
|
`ToAdminSummaryDto`; `Authz` cap + `CanManageCases`; `Program.cs` `CasesAdmin` gate + `GET
|
|
/admin/cases` + `DELETE /admin/cases/{id}`; `AdminCasesTests` (+ update the org-template `/me`
|
|
cap-list assertion). SQLite can't `ORDER BY DateTimeOffset` → `ListAll` sorts client-side.
|
|
- FE: `capability.ts` + `me.adapter` `KNOWN` + `role.interceptor` (`/api/v1/admin/cases`);
|
|
`aanvraag.ts` `owner?`; `applications.adapter` `listAll`/`deleteAny` + parse owner;
|
|
`registratie/application/admin-cases.store.ts` (+spec); `registratie/ui/admin-cases.page.ts`;
|
|
route in `app.routes.ts`; nav item in `site-header`; new `$localize` ids + English targets.
|
|
|
|
## Acceptance criteria
|
|
|
|
- [x] Admin-only page at `/beheer/zaken` lists all cases (owner + type + status), gated by
|
|
`cases:manage` (denial alert for non-admins; server re-enforces via `CasesAdmin`).
|
|
- [x] Admin delete removes any case (incl. submitted); confirmed, optimistic with rollback.
|
|
- [x] `npm run ci` green (336 FE tests, backend 129, localized build, drift clean after commit).
|