Restructures into apps/ssp + apps/behandelportal (two Angular projects) plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's separate sibling repo. That split had already produced real drift: a hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree forked and silently diverging (7 files), and beheer + the styles.scss token bridge duplicated byte-for-byte across both repos. - git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/, environments/, the Storybook docs/*.mdx, and styles.scss into libs/shared + libs/beheer (all confirmed identical between the two repos before merging). auth stays deliberately duplicated per ADR-0002 (actor-specific, expected to diverge) - amended there. - One generated API client (libs/shared), no more vendored swagger.json. - .dependency-cruiser split into a base factory + one config per app, and Storybook into .storybook-ssp/.storybook-behandelportal - both forced by the @auth/* alias resolving to different directories per app. - SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/ HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies its own nav/admin-links/dev-panel instead of one being hardcoded. - CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated; WP-67 backlog entry documents the full decision trail. npm run ci green (lint, dep:check x2, 360 tests across ssp/ behandelportal/shared/beheer, both localized builds, backend tests, snippet + api-client drift); both dev servers, both Storybook instances, and docker compose verified working. The old sibling repo (/home/eho/repos/behandelportal) is left untouched, not deleted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
130 lines
5.4 KiB
TypeScript
130 lines
5.4 KiB
TypeScript
import { Component, computed, effect, inject } from '@angular/core';
|
|
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
|
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
|
import { ButtonComponent } from '@shared/ui/button/button.component';
|
|
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
|
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
|
import { ASYNC } from '@shared/ui/async/async.component';
|
|
import { AccessStore } from '@shared/application/access.store';
|
|
import { formatDatumNl } from '@shared/kernel/datum';
|
|
import { Aanvraag } from '@registratie/domain/aanvraag';
|
|
import { TYPE_LABELS, statusLabel, referentie } from '@registratie/domain/aanvraag-view';
|
|
import { AdminCasesStore } from '@registratie/application/admin-cases.store';
|
|
|
|
/**
|
|
* Admin page: every case across all owners, with an admin delete (WP-36). Lives in
|
|
* `registratie` (which owns the Aanvraag aggregate) — the back-office counterpart of the
|
|
* user's dashboard, reusing the same view labels + trust-boundary parse. Deny-by-default
|
|
* capability gate (`cases:manage`): a denial alert for non-admins, the list for admins.
|
|
* Delete is guarded by a native confirm — it is irreversible and may remove submitted cases.
|
|
*/
|
|
@Component({
|
|
selector: 'app-admin-cases-page',
|
|
imports: [
|
|
PageShellComponent,
|
|
AlertComponent,
|
|
ButtonComponent,
|
|
DataBlockComponent,
|
|
DataRowComponent,
|
|
...ASYNC,
|
|
],
|
|
styles: [
|
|
`
|
|
.case {
|
|
margin-block-end: var(--rhc-space-max-lg);
|
|
}
|
|
`,
|
|
],
|
|
template: `
|
|
<app-page-shell [heading]="heading" [intro]="intro" backLink="/dashboard">
|
|
@if (!access.ready()) {
|
|
<!-- wait for /me before deciding — avoids flashing the denial to an admin -->
|
|
} @else if (!canManage()) {
|
|
<app-alert type="error">{{ deniedText }}</app-alert>
|
|
} @else {
|
|
<app-async [data]="store.cases()">
|
|
<ng-template appAsyncError>
|
|
<app-alert type="error">{{ failedText }}</app-alert>
|
|
<app-button variant="secondary" (click)="reload()">{{ retryText }}</app-button>
|
|
</ng-template>
|
|
<ng-template appAsyncLoaded>
|
|
@if (cases().length === 0) {
|
|
<app-alert type="info">{{ emptyText }}</app-alert>
|
|
} @else {
|
|
@for (c of cases(); track c.id) {
|
|
<div class="case">
|
|
<app-data-block [heading]="typeLabel(c)" [level]="2">
|
|
@for (row of rows(c); track row.key) {
|
|
<div app-data-row [key]="row.key" [value]="row.value"></div>
|
|
}
|
|
</app-data-block>
|
|
<app-button variant="secondary" (click)="confirmDelete(c)">{{
|
|
deleteText
|
|
}}</app-button>
|
|
</div>
|
|
}
|
|
}
|
|
</ng-template>
|
|
</app-async>
|
|
}
|
|
</app-page-shell>
|
|
`,
|
|
})
|
|
export class AdminCasesPage {
|
|
protected store = inject(AdminCasesStore);
|
|
protected access = inject(AccessStore);
|
|
|
|
protected canManage = computed(() => this.access.can('cases:manage'));
|
|
protected cases = computed(() => {
|
|
const rd = this.store.cases();
|
|
return rd.tag === 'Success' ? rd.value : [];
|
|
});
|
|
|
|
protected heading = $localize`:@@adminCases.heading:Aanvragen beheren`;
|
|
protected intro = $localize`:@@adminCases.intro:Alle aanvragen in het register. Een aanvraag verwijderen kan niet ongedaan worden gemaakt.`;
|
|
protected deniedText = $localize`:@@adminCases.denied:U hebt geen rechten om aanvragen te beheren.`;
|
|
protected failedText = $localize`:@@adminCases.failed:De aanvragen konden niet worden geladen.`;
|
|
protected emptyText = $localize`:@@adminCases.empty:Er zijn geen aanvragen.`;
|
|
protected retryText = $localize`:@@adminCases.retry:Opnieuw proberen`;
|
|
protected deleteText = $localize`:@@adminCases.delete:Verwijderen`;
|
|
|
|
private ownerKey = $localize`:@@adminCases.owner:Eigenaar (BSN)`;
|
|
private statusKey = $localize`:@@adminCases.status:Status`;
|
|
private refKey = $localize`:@@adminCases.referentie:Referentie`;
|
|
private ingediendKey = $localize`:@@adminCases.ingediend:Ingediend op`;
|
|
|
|
protected typeLabel = (c: Aanvraag) => TYPE_LABELS[c.type];
|
|
|
|
/** Key/value rows for one case (owner + lifecycle facts; the type is the block heading). */
|
|
protected rows(c: Aanvraag): { key: string; value: string }[] {
|
|
return [
|
|
{ key: this.ownerKey, value: c.owner ?? '—' },
|
|
{ key: this.statusKey, value: statusLabel(c.status) },
|
|
{ key: this.refKey, value: referentie(c.status) || '—' },
|
|
{ key: this.ingediendKey, value: c.submittedAt ? formatDatumNl(c.submittedAt) : '—' },
|
|
];
|
|
}
|
|
|
|
private loadRequested = false;
|
|
constructor() {
|
|
// Load once the capability resolves to allowed (a 403 GET would be wasted otherwise).
|
|
// Depends only on canManage() + a plain flag — never the store model (WP-26 loop lesson).
|
|
effect(() => {
|
|
if (this.canManage() && !this.loadRequested) {
|
|
this.loadRequested = true;
|
|
void this.store.load();
|
|
}
|
|
});
|
|
}
|
|
|
|
protected reload() {
|
|
void this.store.load();
|
|
}
|
|
|
|
/** Native confirm — no dialog component exists, and admin delete is irreversible. */
|
|
protected confirmDelete(c: Aanvraag) {
|
|
const msg = $localize`:@@adminCases.confirm:Deze aanvraag definitief verwijderen?`;
|
|
if (confirm(msg)) void this.store.delete(c.id);
|
|
}
|
|
}
|