feat(behandelportal): WP-65a beoordeling detail (read) + fix unreachable medewerker login
CI / changes (pull_request) Successful in 17s
CI / lint (pull_request) Failing after 54s
CI / frontend (pull_request) Successful in 2m38s
CI / storybook-a11y (pull_request) Failing after 3m28s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m9s
CI / e2e (pull_request) Successful in 2m55s
CI / api-client-drift (pull_request) Successful in 2m1s
CI / changes (pull_request) Successful in 17s
CI / lint (pull_request) Failing after 54s
CI / frontend (pull_request) Successful in 2m38s
CI / storybook-a11y (pull_request) Failing after 3m28s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m9s
CI / e2e (pull_request) Successful in 2m55s
CI / api-client-drift (pull_request) Successful in 2m1s
New GET /beoordeling/{id} shows one aanvraag's status, linked documents, and a
canBesluiten decision flag, gated by the same CanBeoordelen capability as the
werkvoorraad list. Reads through IZaakSource.ListCases rather than a new seam
method (WP-66 needs one anyway for the real write); owner BSN is masked.
Fixes a real gap found while wiring this up: the behandelportal's login was still
WP-61's copied citizen/BSN DigiD flow, so nothing ever sent X-Medewerker and the
werkvoorraad screen (WP-64) always denied in a real browser. A dev-only
medewerkerInterceptor (mirrors the existing ?role= stand-in as ?rollen=) fixes that.
WP-65's own Risks note authorized splitting read from write across sessions given
its size; this is the read half. The decision-recording mutation is next (65b).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { statusLabel, detailRows, TYPE_LABELS } from './beoordeling-view';
|
||||
import { BeoordelingView } from './beoordeling';
|
||||
|
||||
const base: Omit<BeoordelingView, 'status'> = {
|
||||
id: '1',
|
||||
type: 'herregistratie',
|
||||
owner: '*****2333',
|
||||
submittedAt: '2024-05-12',
|
||||
documenten: [],
|
||||
canBesluiten: true,
|
||||
};
|
||||
|
||||
describe('statusLabel', () => {
|
||||
it('labels every tag distinctly', () => {
|
||||
const labels = [
|
||||
statusLabel({ tag: 'Ingediend', referentie: 'R1' }),
|
||||
statusLabel({ tag: 'InBehandeling', referentie: 'R1', manual: false }),
|
||||
statusLabel({ tag: 'InBehandeling', referentie: 'R1', manual: true }),
|
||||
statusLabel({ tag: 'MeerInfoGevraagd', referentie: 'R1', reden: 'x' }),
|
||||
statusLabel({ tag: 'Goedgekeurd', referentie: 'R1' }),
|
||||
statusLabel({ tag: 'Afgewezen', referentie: 'R1', reden: 'x' }),
|
||||
];
|
||||
expect(new Set(labels).size).toBe(labels.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detailRows', () => {
|
||||
it('lists soort/status/referentie/eigenaar/ingediend', () => {
|
||||
const rows = detailRows({
|
||||
...base,
|
||||
status: { tag: 'InBehandeling', referentie: 'R1', manual: false },
|
||||
});
|
||||
const values = rows.map((r) => r.value);
|
||||
expect(values).toContain(TYPE_LABELS.herregistratie);
|
||||
expect(values).toContain('R1');
|
||||
expect(values).toContain(base.owner);
|
||||
expect(rows.length).toBe(5);
|
||||
});
|
||||
|
||||
it('adds a reden row for Afgewezen and MeerInfoGevraagd only', () => {
|
||||
const afgewezen = detailRows({
|
||||
...base,
|
||||
status: { tag: 'Afgewezen', referentie: 'R1', reden: 'Onvoldoende uren' },
|
||||
});
|
||||
expect(afgewezen.length).toBe(6);
|
||||
expect(afgewezen.map((r) => r.value)).toContain('Onvoldoende uren');
|
||||
|
||||
const meerInfo = detailRows({
|
||||
...base,
|
||||
status: { tag: 'MeerInfoGevraagd', referentie: 'R1', reden: 'Diploma ontbreekt' },
|
||||
});
|
||||
expect(meerInfo.length).toBe(6);
|
||||
|
||||
const goedgekeurd = detailRows({ ...base, status: { tag: 'Goedgekeurd', referentie: 'R1' } });
|
||||
expect(goedgekeurd.length).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { formatDatumNl } from '@shared/kernel/datum';
|
||||
import { AanvraagType } from './werkvoorraad-item';
|
||||
import { BeoordelingStatus, BeoordelingView } from './beoordeling';
|
||||
|
||||
/** View-model mapping shared by the werkvoorraad list (WP-64) and the beoordeling
|
||||
detail screen (WP-65): type/status → labels. Pure, no Angular. Lives here (not in
|
||||
`werkvoorraad-item-view.ts`) because `BeoordelingStatus` is the wider of the two
|
||||
status unions — `werkvoorraad-item-view.ts` re-exports these for its own use. */
|
||||
|
||||
export const TYPE_LABELS: Record<AanvraagType, string> = {
|
||||
registratie: $localize`:@@werkvoorraad.type.registratie:Inschrijving`,
|
||||
herregistratie: $localize`:@@werkvoorraad.type.herregistratie:Herregistratie`,
|
||||
intake: $localize`:@@werkvoorraad.type.intake:Herregistratie-intake`,
|
||||
};
|
||||
|
||||
export function statusLabel(status: BeoordelingStatus): string {
|
||||
switch (status.tag) {
|
||||
case 'Ingediend':
|
||||
return $localize`:@@werkvoorraad.status.ingediend:Ingediend`;
|
||||
case 'InBehandeling':
|
||||
return status.manual
|
||||
? $localize`:@@werkvoorraad.status.inBehandelingHandmatig:In behandeling (handmatig)`
|
||||
: $localize`:@@werkvoorraad.status.inBehandeling:In behandeling`;
|
||||
case 'MeerInfoGevraagd':
|
||||
return $localize`:@@beoordeling.status.meerInfoGevraagd:Meer informatie gevraagd`;
|
||||
case 'Goedgekeurd':
|
||||
return $localize`:@@beoordeling.status.goedgekeurd:Goedgekeurd`;
|
||||
case 'Afgewezen':
|
||||
return $localize`:@@beoordeling.status.afgewezen:Afgewezen`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Key/value rows for the beoordeling detail page (CIBG Datablock). */
|
||||
export function detailRows(view: BeoordelingView): { key: string; value: string }[] {
|
||||
const s = view.status;
|
||||
const rows = [
|
||||
{ key: $localize`:@@beoordeling.detail.soort:Soort aanvraag`, value: TYPE_LABELS[view.type] },
|
||||
{ key: $localize`:@@beoordeling.detail.status:Status`, value: statusLabel(s) },
|
||||
{ key: $localize`:@@beoordeling.detail.referentie:Referentie`, value: s.referentie },
|
||||
{ key: $localize`:@@beoordeling.detail.eigenaar:Eigenaar (BSN)`, value: view.owner },
|
||||
{
|
||||
key: $localize`:@@beoordeling.detail.ingediend:Ingediend op`,
|
||||
value: view.submittedAt ? formatDatumNl(view.submittedAt) : '—',
|
||||
},
|
||||
];
|
||||
if (s.tag === 'Afgewezen' || s.tag === 'MeerInfoGevraagd') {
|
||||
rows.push({ key: $localize`:@@beoordeling.detail.reden:Reden`, value: s.reden });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { AanvraagType } from './werkvoorraad-item';
|
||||
|
||||
/**
|
||||
* A case's full status lifecycle as the beoordeling detail screen sees it (WP-65) —
|
||||
* wider than `WerkvoorraadStatus` (WP-64), which only ever sees the two "still open"
|
||||
* tags. This is the same five-tag union ssp's `AanvraagStatus` models (minus `Concept`
|
||||
* — the detail endpoint 404s a Concept, it isn't a case a behandelaar can treat yet).
|
||||
*/
|
||||
export type BeoordelingStatus =
|
||||
| { tag: 'Ingediend'; referentie: string }
|
||||
| { tag: 'InBehandeling'; referentie: string; manual: boolean }
|
||||
| { tag: 'MeerInfoGevraagd'; referentie: string; reden: string }
|
||||
| { tag: 'Goedgekeurd'; referentie: string }
|
||||
| { tag: 'Afgewezen'; referentie: string; reden: string };
|
||||
|
||||
export interface BeoordelingDocument {
|
||||
documentId: string;
|
||||
categoryId: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface BeoordelingView {
|
||||
id: string;
|
||||
type: AanvraagType;
|
||||
status: BeoordelingStatus;
|
||||
/** The BSN of the citizen the aanvraag belongs to — masked by the server. */
|
||||
owner: string;
|
||||
submittedAt?: string;
|
||||
documenten: BeoordelingDocument[];
|
||||
/** Decision flag (ADR-0001): the server computes whether a decision may be recorded;
|
||||
the FE renders it, it never recomputes the lifecycle. */
|
||||
canBesluiten: boolean;
|
||||
}
|
||||
@@ -1,25 +1,12 @@
|
||||
import { formatDatumNl } from '@shared/kernel/datum';
|
||||
import { WerkvoorraadItem, WerkvoorraadStatus, AanvraagType } from './werkvoorraad-item';
|
||||
import { WerkvoorraadItem } from './werkvoorraad-item';
|
||||
import { TYPE_LABELS, statusLabel } from './beoordeling-view';
|
||||
|
||||
/** View-model mapping for a queue row: type/status → the fields for a CIBG
|
||||
"aanvragen" row. Pure, no Angular — the UI renders these, it does not derive them. */
|
||||
|
||||
export const TYPE_LABELS: Record<AanvraagType, string> = {
|
||||
registratie: $localize`:@@werkvoorraad.type.registratie:Inschrijving`,
|
||||
herregistratie: $localize`:@@werkvoorraad.type.herregistratie:Herregistratie`,
|
||||
intake: $localize`:@@werkvoorraad.type.intake:Herregistratie-intake`,
|
||||
};
|
||||
|
||||
export function statusLabel(status: WerkvoorraadStatus): string {
|
||||
switch (status.tag) {
|
||||
case 'Ingediend':
|
||||
return $localize`:@@werkvoorraad.status.ingediend:Ingediend`;
|
||||
case 'InBehandeling':
|
||||
return status.manual
|
||||
? $localize`:@@werkvoorraad.status.inBehandelingHandmatig:In behandeling (handmatig)`
|
||||
: $localize`:@@werkvoorraad.status.inBehandeling:In behandeling`;
|
||||
}
|
||||
}
|
||||
"aanvragen" row. Pure, no Angular — the UI renders these, it does not derive them.
|
||||
`TYPE_LABELS`/`statusLabel` live in `./beoordeling-view` (the wider status union) and
|
||||
are re-exported here so existing imports of this file keep working. */
|
||||
export { TYPE_LABELS, statusLabel };
|
||||
|
||||
export interface WerkvoorraadRow {
|
||||
heading: string;
|
||||
|
||||
Reference in New Issue
Block a user