feat(registratie): WP-36 — admin cases page + admin delete

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>
This commit is contained in:
eho
2026-07-23 12:23:34 +02:00
co-authored by Claude Opus 4.8
parent d1abd35b0d
commit 446ea9474b
23 changed files with 786 additions and 9 deletions
+6 -1
View File
@@ -3,4 +3,9 @@
* Server-resolved and opaque to the FE — never derived from a role client-side.
*/
export type Capability =
'brief:approve' | 'brief:reject' | 'brief:send' | 'orgtemplate:edit' | 'stamdata:edit';
| 'brief:approve'
| 'brief:reject'
| 'brief:send'
| 'orgtemplate:edit'
| 'stamdata:edit'
| 'cases:manage';
@@ -1032,6 +1032,94 @@ export class ApiClient {
return Promise.resolve<SubmitApplicationResponse>(null as any);
}
/**
* @return OK
*/
casesAll(): Promise<ApplicationSummaryDto[]> {
let url_ = this.baseUrl + "/api/v1/admin/cases";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processCasesAll(_response);
});
}
protected processCasesAll(response: Response): Promise<ApplicationSummaryDto[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ApplicationSummaryDto[];
return result200;
});
} else if (status === 403) {
return response.text().then((_responseText) => {
let result403: any = null;
result403 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails;
return throwException("Forbidden", status, _responseText, _headers, result403);
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<ApplicationSummaryDto[]>(null as any);
}
/**
* @return No Content
*/
cases(id: string): Promise<void> {
let url_ = this.baseUrl + "/api/v1/admin/cases/{id}";
if (id === undefined || id === null)
throw new globalThis.Error("The parameter 'id' must be defined.");
url_ = url_.replace("{id}", encodeURIComponent("" + id));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "DELETE",
headers: {
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processCases(_response);
});
}
protected processCases(response: Response): Promise<void> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 204) {
return response.text().then((_responseText) => {
return;
});
} else if (status === 403) {
return response.text().then((_responseText) => {
let result403: any = null;
result403 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails;
return throwException("Forbidden", status, _responseText, _headers, result403);
});
} else if (status === 404) {
return response.text().then((_responseText) => {
return throwException("Not Found", status, _responseText, _headers);
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<void>(null as any);
}
/**
* @return OK
*/
@@ -1674,6 +1762,7 @@ export interface ApplicationSummaryDto {
createdAt?: string | undefined;
updatedAt?: string | undefined;
submittedAt?: string | undefined;
owner?: string | undefined;
}
export interface BriefDecisionsDto {
@@ -9,6 +9,7 @@ const KNOWN: readonly Capability[] = [
'brief:send',
'orgtemplate:edit',
'stamdata:edit',
'cases:manage',
];
/**
@@ -12,6 +12,7 @@ import { currentRole } from './role';
const ROLE_AWARE = [
'/api/v1/brief',
'/api/v1/admin/org-template',
'/api/v1/admin/cases',
'/api/v1/stamdata',
'/api/v1/me',
];
@@ -33,6 +33,11 @@ const ADMIN_NAV_ITEMS: readonly (HeaderNavItem & { readonly cap: Capability })[]
to: '/beheer/stamdata',
cap: 'stamdata:edit',
},
{
label: $localize`:@@header.nav.zaken:Aanvragen`,
to: '/beheer/zaken',
cap: 'cases:manage',
},
];
/** Organism: CIBG Huisstijl site header — logo block, robijn titlebar (breadcrumb +