## What & why
Finishes **S-12 · Behandel-portal — werkbak + beoordeling**. The backend sub-slices (S-12a/b/c-1/c-2) were merged, but the slice's stated outcome — a behandel *portal* with medewerker login, a werkbak, and decide — had no frontend. This adds it.
- **`libs/auth`**: `MedewerkerAuthService` + `provideMedewerkerAuth` (Keycloak `medewerker` realm), a `roles`/`hasRole` surface on the shared `AuthService`, and a realm-roles protocol mapper so the SPA can read `behandelaar`/`teamlead` from the token. The BFF remains the security boundary (ADR-0013).
- **`apps/behandel`**: a new Nx Angular app mirroring self-service — medewerker OIDC login and a **werkbak** page listing registrations awaiting beoordeling (`GET /behandel/werkbak`) with per-row **Goedkeuren/Afwijzen** actions (`POST /behandel/registrations/{id}/decide`) that refresh the list. NL DS/Utrecht, standalone + signals.
- **e2e**: the walking-skeleton happy path now approves through the real portal (behandelaar logs in, finds the row by reference, clicks Goedkeuren) instead of the temporary admin endpoint.
- **infra/docs**: behandel service in compose (`:8142`, depends on Keycloak); added to the smoke `WAIT_SVCS` + CI log dump; `frontend-decisions.md` and `demo-script.md` updated.
Closes #13
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation.
- [x] Implementation makes the test pass; refactor commit if structure improved.
- [x] Conventional Commits referencing the issue (`refs #13`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes. *(behandel image + container verified locally; full stack gated in CI.)*
- [x] Docs updated if behaviour, contracts, or operations changed.
- [x] ADR added — ADR-0013 (merged with the backend sub-slices) already covers the wiring; no new decision here.
- [x] Demo note in `docs/demo-script.md`.
## Notes for reviewers
- Verified locally: auth + behandel + all frontend projects pass lint & unit tests (incl. axe WCAG 2.1 AA); production build green; the behandel Docker image builds and serves with the correct baked `medewerker` config + SPA fallback.
- The full compose-up smoke, e2e, and mutation are CI-gated (known local full-stack verify limits).
- **Follow-ups (not in scope):** the `WerkbakItem` contract has no citizen name (werkbak shows the BSN) — adding one is a BFF+domain contract change; and the domain's temporary admin `approve` endpoint is now unused by the e2e and could be removed.
Reviewed-on: #87
66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
import { Component, inject, signal } from '@angular/core';
|
|
import { BffApiV1Service, type WerkbakItem } from 'api-client';
|
|
import { UtrechtComponentsModule } from 'ui';
|
|
|
|
/** The two decisions a behandelaar can make; the BFF validates these exact values (ADR-0013). */
|
|
type Besluit = 'goedkeuren' | 'afwijzen';
|
|
|
|
/**
|
|
* The behandel werkbak: a signed-in behandelaar sees the registrations awaiting beoordeling (the open
|
|
* Flowable `Beoordelen` tasks, read through the domain) and decides each — goedkeuren or afwijzen. A
|
|
* decision posts to the BFF, which applies the domain transition and completes the workflow task
|
|
* (ADR-0013; S-12). After a decision the werkbak refreshes so the handled item drops off the list.
|
|
*/
|
|
@Component({
|
|
selector: 'app-werkbak-page',
|
|
imports: [UtrechtComponentsModule],
|
|
templateUrl: './werkbak-page.html',
|
|
})
|
|
export class WerkbakPage {
|
|
private readonly bff = inject(BffApiV1Service);
|
|
|
|
protected readonly items = signal<WerkbakItem[]>([]);
|
|
protected readonly loading = signal(false);
|
|
protected readonly loaded = signal(false);
|
|
protected readonly failed = signal(false);
|
|
protected readonly deciding = signal<string | undefined>(undefined);
|
|
|
|
constructor() {
|
|
this.load();
|
|
}
|
|
|
|
load(): void {
|
|
this.loading.set(true);
|
|
this.failed.set(false);
|
|
this.bff.getBehandelWerkbak().subscribe({
|
|
next: (rows: WerkbakItem[]) => {
|
|
this.items.set(rows);
|
|
this.loading.set(false);
|
|
this.loaded.set(true);
|
|
},
|
|
// Surface the failure (e.g. 403 for a non-behandelaar) instead of swallowing it.
|
|
error: () => {
|
|
this.items.set([]);
|
|
this.loading.set(false);
|
|
this.loaded.set(true);
|
|
this.failed.set(true);
|
|
},
|
|
});
|
|
}
|
|
|
|
decide(registrationId: string, besluit: Besluit): void {
|
|
this.deciding.set(registrationId);
|
|
this.bff.postBehandelRegistrationsIdDecide(registrationId, { besluit }).subscribe({
|
|
// Refresh so the decided registration drops off the werkbak (its task is now completed).
|
|
next: () => {
|
|
this.deciding.set(undefined);
|
|
this.load();
|
|
},
|
|
error: () => {
|
|
this.deciding.set(undefined);
|
|
this.failed.set(true);
|
|
},
|
|
});
|
|
}
|
|
}
|