Behandelportal: monorepo merge + WP-64..67 backoffice arc (OpenZaak write closes it out) #1

Merged
eho merged 8 commits from monorepo-behandelportal into main 2026-08-04 12:56:34 +00:00
22 changed files with 958 additions and 198 deletions
Showing only changes of commit fe69caee63 - Show all commits
+1 -8
View File
@@ -15,9 +15,8 @@ export const routes: Routes = [
{
path: 'dashboard',
canActivate: [authGuard],
// TODO(create-ssp): stopgap landing page — point this at a real overview once you have one.
loadComponent: () =>
import('@behandeling/ui/behandeling.page').then((m) => m.BehandelingPage),
import('@behandeling/ui/werkvoorraad.page').then((m) => m.WerkvoorraadPage),
},
{
path: 'beheer/stamdata',
@@ -41,12 +40,6 @@ export const routes: Routes = [
loadComponent: () =>
import('@beheer/ui/feature-flags.page').then((m) => m.FeatureFlagsPage),
},
{
path: 'behandeling',
canActivate: [authGuard],
loadComponent: () =>
import('@behandeling/ui/behandeling.page').then((m) => m.BehandelingPage),
},
{ path: '**', redirectTo: 'login' },
],
},
@@ -0,0 +1,37 @@
import { Injectable, inject, signal } from '@angular/core';
import { RemoteData } from '@shared/application/remote-data';
import { WerkvoorraadItem } from '@behandeling/domain/werkvoorraad-item';
import {
WerkvoorraadAdapter,
parseWerkvoorraad,
} from '@behandeling/infrastructure/werkvoorraad.adapter';
type Err = Error | undefined;
/** The behandelaar's queue (WP-64) — a root singleton like `AdminCasesStore`'s ssp
counterpart. Fetch + parse at the trust boundary, publish as RemoteData. */
@Injectable({ providedIn: 'root' })
export class WerkvoorraadStore {
private adapter = inject(WerkvoorraadAdapter);
private state = signal<RemoteData<Err, WerkvoorraadItem[]>>({ tag: 'Loading' });
readonly items = this.state.asReadonly();
async load() {
if (this.state().tag !== 'Success') this.state.set({ tag: 'Loading' });
try {
const parsed = parseWerkvoorraad(await this.adapter.list());
this.state.set(
parsed.ok
? { tag: 'Success', value: parsed.value }
: { tag: 'Failure', error: new Error(parsed.error) },
);
} catch (e) {
this.state.set({ tag: 'Failure', error: e as Error });
}
}
reload() {
void this.load();
}
}
@@ -0,0 +1,48 @@
import { describe, it, expect } from 'vitest';
import { werkvoorraadRow, statusLabel, TYPE_LABELS } from './werkvoorraad-item-view';
import { WerkvoorraadItem } from './werkvoorraad-item';
const base: Omit<WerkvoorraadItem, 'status'> = {
id: '1',
type: 'herregistratie',
owner: '111222333',
submittedAt: '2024-05-12',
};
describe('werkvoorraadRow', () => {
it('heading is the type, subtitle carries the owner BSN', () => {
const row = werkvoorraadRow({
...base,
status: { tag: 'InBehandeling', referentie: 'R1', manual: false },
});
expect(row.heading).toBe(TYPE_LABELS.herregistratie);
expect(row.subtitle).toContain('111222333');
});
it('status line carries the status label, reference and submit date', () => {
const row = werkvoorraadRow({
...base,
status: { tag: 'InBehandeling', referentie: 'R1', manual: false },
});
expect(row.status).toContain(
statusLabel({ tag: 'InBehandeling', referentie: 'R1', manual: false }),
);
expect(row.status).toContain('R1');
expect(row.status).toContain('12 mei 2024');
});
it('manual review is called out distinctly from an automatic InBehandeling', () => {
const manual = statusLabel({ tag: 'InBehandeling', referentie: 'R1', manual: true });
const auto = statusLabel({ tag: 'InBehandeling', referentie: 'R1', manual: false });
expect(manual).not.toBe(auto);
});
it('a missing submit date leaves no dangling separator', () => {
const row = werkvoorraadRow({
...base,
submittedAt: undefined,
status: { tag: 'Ingediend', referentie: 'R9' },
});
expect(row.status).toBe(`${statusLabel({ tag: 'Ingediend', referentie: 'R9' })} · R9`);
});
});
@@ -0,0 +1,44 @@
import { formatDatumNl } from '@shared/kernel/datum';
import { WerkvoorraadItem, WerkvoorraadStatus, AanvraagType } from './werkvoorraad-item';
/** 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`;
}
}
export interface WerkvoorraadRow {
heading: string;
subtitle: string;
status: string;
}
/** Fields for one queue row: type as heading, owner (BSN) as subtitle, status +
reference + submit date as the status line. */
export function werkvoorraadRow(item: WerkvoorraadItem): WerkvoorraadRow {
const parts = [statusLabel(item.status), item.status.referentie];
if (item.submittedAt) {
parts.push(
$localize`:@@werkvoorraad.row.ingediend:ingediend op ${formatDatumNl(item.submittedAt)}:datum:`,
);
}
return {
heading: TYPE_LABELS[item.type],
subtitle: $localize`:@@werkvoorraad.row.bsn:BSN ${item.owner}:bsn:`,
status: parts.join(' · '),
};
}
@@ -0,0 +1,23 @@
/**
* A queue entry as the behandelportal sees it (WP-64) — the parsed, domain-side view
* of the backend's cross-owner `GET /werkvoorraad`. Pure types, no Angular.
*
* The status union is narrower than the SSP's full `AanvraagStatus` (ssp's
* `registratie/domain/aanvraag.ts`): the backend only ever puts a case in the queue
* while it is still open (`Ingediend`/`InBehandeling`), so a queue item literally
* cannot be `Concept`/`Goedgekeurd`/`Afgewezen` — illegal states unrepresentable.
*/
export type AanvraagType = 'registratie' | 'herregistratie' | 'intake';
export type WerkvoorraadStatus =
| { tag: 'Ingediend'; referentie: string }
| { tag: 'InBehandeling'; referentie: string; manual: boolean };
export interface WerkvoorraadItem {
id: string;
type: AanvraagType;
status: WerkvoorraadStatus;
/** The BSN of the citizen the aanvraag belongs to — always populated (cross-owner list). */
owner: string;
submittedAt?: string;
}
@@ -0,0 +1,53 @@
import { describe, it, expect } from 'vitest';
import { parseWerkvoorraadItem, parseWerkvoorraad } from './werkvoorraad.adapter';
const inBehandeling = {
id: 'a1',
type: 'herregistratie',
status: { tag: 'InBehandeling', referentie: 'BIG-1', manual: false },
documentIds: [],
createdAt: '2026-07-01T10:00:00Z',
updatedAt: '2026-07-01T10:05:00Z',
owner: '111222333',
};
describe('parseWerkvoorraadItem', () => {
it('parses Ingediend and InBehandeling', () => {
expect(parseWerkvoorraadItem(inBehandeling).ok).toBe(true);
expect(
parseWerkvoorraadItem({ ...inBehandeling, status: { tag: 'Ingediend', referentie: 'BIG-2' } })
.ok,
).toBe(true);
});
it('rejects a case whose status is not an open queue tag', () => {
expect(
parseWerkvoorraadItem({
...inBehandeling,
status: { tag: 'Goedgekeurd', referentie: 'BIG-1' },
}).ok,
).toBe(false);
expect(
parseWerkvoorraadItem({
...inBehandeling,
status: { tag: 'Concept', stepIndex: 0, stepCount: 1 },
}).ok,
).toBe(false);
});
it('rejects a missing owner, bad type, and non-objects', () => {
expect(parseWerkvoorraadItem({ ...inBehandeling, owner: undefined }).ok).toBe(false);
expect(parseWerkvoorraadItem({ ...inBehandeling, type: 'onbekend' }).ok).toBe(false);
expect(parseWerkvoorraadItem(null).ok).toBe(false);
});
});
describe('parseWerkvoorraad', () => {
it('parses a list and fails fast on a bad element', () => {
expect(parseWerkvoorraad([inBehandeling, inBehandeling]).ok).toBe(true);
expect(parseWerkvoorraad([inBehandeling, { ...inBehandeling, owner: undefined }]).ok).toBe(
false,
);
expect(parseWerkvoorraad({}).ok).toBe(false);
});
});
@@ -0,0 +1,72 @@
import { Injectable, inject } from '@angular/core';
import { Result, ok, err } from '@shared/kernel/fp';
import { ApiClient, ApplicationSummaryDto } from '@shared/infrastructure/api-client';
import {
WerkvoorraadItem,
WerkvoorraadStatus,
AanvraagType,
} from '@behandeling/domain/werkvoorraad-item';
/**
* Infrastructure adapter for the behandelportal's queue read (WP-64) — the only
* place its HTTP lives (ADR-0001 anti-corruption boundary). The untrusted response
* is validated + mapped to the (narrower) queue domain shape by the parse* boundary
* below; a case whose status isn't `Ingediend`/`InBehandeling` is a parse error, not
* a silently-rendered row — the endpoint's own filter is a guarantee this boundary enforces.
*/
@Injectable({ providedIn: 'root' })
export class WerkvoorraadAdapter {
private client = inject(ApiClient);
list(): Promise<ApplicationSummaryDto[]> {
return this.client.werkvoorraad();
}
}
const AANVRAAG_TYPES: readonly string[] = ['registratie', 'herregistratie', 'intake'];
function parseWerkvoorraadStatus(
s: ApplicationSummaryDto['status'] | undefined,
): Result<string, WerkvoorraadStatus> {
if (!s || typeof s.tag !== 'string') return err('werkvoorraad: missing status');
switch (s.tag) {
case 'Ingediend':
if (typeof s.referentie !== 'string') return err('werkvoorraad: bad Ingediend status');
return ok({ tag: 'Ingediend', referentie: s.referentie });
case 'InBehandeling':
if (typeof s.referentie !== 'string' || typeof s.manual !== 'boolean')
return err('werkvoorraad: bad InBehandeling status');
return ok({ tag: 'InBehandeling', referentie: s.referentie, manual: s.manual });
default:
return err(`werkvoorraad: a queue item cannot have status ${s.tag}`);
}
}
export function parseWerkvoorraadItem(json: unknown): Result<string, WerkvoorraadItem> {
if (typeof json !== 'object' || json === null) return err('werkvoorraad: not an object');
const dto = json as ApplicationSummaryDto;
if (typeof dto.id !== 'string') return err('werkvoorraad: missing id');
if (typeof dto.type !== 'string' || !AANVRAAG_TYPES.includes(dto.type))
return err(`werkvoorraad: bad type ${dto.type}`);
if (typeof dto.owner !== 'string' || !dto.owner) return err('werkvoorraad: missing owner');
const status = parseWerkvoorraadStatus(dto.status);
if (!status.ok) return status;
return ok({
id: dto.id,
type: dto.type as AanvraagType,
status: status.value,
owner: dto.owner,
submittedAt: dto.submittedAt,
});
}
export function parseWerkvoorraad(json: unknown): Result<string, WerkvoorraadItem[]> {
if (!Array.isArray(json)) return err('werkvoorraad: not an array');
const out: WerkvoorraadItem[] = [];
for (const item of json) {
const parsed = parseWerkvoorraadItem(item);
if (!parsed.ok) return parsed;
out.push(parsed.value);
}
return ok(out);
}
@@ -1,20 +0,0 @@
import { Component } from '@angular/core';
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
/**
* Scaffolded by `gen:context` (WP-44) — replace with the `behandeling` context's first
* feature slice (the `new-feature` skill: domain first, then infrastructure/application, UI last).
*/
@Component({
selector: 'app-behandeling-page',
imports: [PageShellComponent],
template: `
<app-page-shell [heading]="heading">
<p>{{ intro }}</p>
</app-page-shell>
`,
})
export class BehandelingPage {
protected heading = $localize`:@@behandeling.landing.heading:Behandeling`;
protected intro = $localize`:@@behandeling.landing.intro:Hier komt de eerste behandeling-functionaliteit.`;
}
@@ -0,0 +1,31 @@
import { Component, input } from '@angular/core';
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
import { WerkvoorraadItem } from '@behandeling/domain/werkvoorraad-item';
import { werkvoorraadRow } from '@behandeling/domain/werkvoorraad-item-view';
/** Organism: the behandelaar's queue as CIBG "aanvragen" rows (WP-64) — composition
of the two existing shared/ui molecules, no new atom. Rows are informational only
(no `to`): opening a case's detail is WP-65. */
@Component({
selector: 'app-werkvoorraad-list',
imports: [ApplicationListComponent, ApplicationLinkComponent],
template: `
<app-application-list>
@for (item of items(); track item.id) {
@let row = row_(item);
<li
app-application-link
[heading]="row.heading"
[subtitle]="row.subtitle"
[status]="row.status"
></li>
}
</app-application-list>
`,
})
export class WerkvoorraadListComponent {
items = input.required<WerkvoorraadItem[]>();
protected row_ = werkvoorraadRow;
}
@@ -0,0 +1,44 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { applicationConfig } from '@storybook/angular';
import { provideRouter } from '@angular/router';
import { WerkvoorraadListComponent } from './werkvoorraad-list.component';
import { WerkvoorraadItem } from '@behandeling/domain/werkvoorraad-item';
const items: WerkvoorraadItem[] = [
{
id: 'a1',
type: 'herregistratie',
owner: '111222333',
submittedAt: '2026-06-28T10:05:00Z',
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false },
},
{
id: 'a2',
type: 'registratie',
owner: '444555666',
submittedAt: '2026-06-27T09:00:00Z',
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456790', manual: true },
},
{
id: 'a3',
type: 'intake',
owner: '777888999',
status: { tag: 'Ingediend', referentie: 'BIG-2026-456791' },
},
];
const meta: Meta<WerkvoorraadListComponent> = {
title: 'Domein/Behandeling/Werkvoorraad List',
component: WerkvoorraadListComponent,
decorators: [applicationConfig({ providers: [provideRouter([])] })],
};
export default meta;
type Story = StoryObj<WerkvoorraadListComponent>;
export const Default: Story = {
args: { items },
};
export const Empty: Story = {
args: { items: [] },
};
@@ -0,0 +1,82 @@
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 { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
import { ASYNC } from '@shared/ui/async/async.component';
import { AccessStore } from '@shared/application/access.store';
import { WerkvoorraadStore } from '@behandeling/application/werkvoorraad.store';
import { WerkvoorraadListComponent } from '@behandeling/ui/werkvoorraad-list/werkvoorraad-list.component';
/**
* Page: the behandelaar's werkvoorraad (WP-64) — the behandelportal's landing page.
* Deny-by-default capability gate (`aanvraag:beoordelen`), same idiom as ssp's
* AdminCasesPage: a denial alert for a non-behandelaar, the queue for one. Opening
* a case's detail is out of scope here (WP-65).
*/
@Component({
selector: 'app-werkvoorraad-page',
imports: [
PageShellComponent,
AlertComponent,
ButtonComponent,
SkeletonComponent,
WerkvoorraadListComponent,
...ASYNC,
],
template: `
<app-page-shell [heading]="heading" [intro]="intro">
@if (!access.ready()) {
<!-- wait for /me before deciding — avoids flashing the denial to a behandelaar -->
} @else if (!canBeoordelen()) {
<app-alert type="error">{{ deniedText }}</app-alert>
} @else {
<app-async [data]="store.items()" (retryClicked)="store.reload()">
<ng-template appAsyncLoading>
<app-skeleton height="2.5rem" [count]="4" />
</ng-template>
<ng-template appAsyncError>
<app-alert type="error">{{ failedText }}</app-alert>
<app-button variant="secondary" (click)="store.reload()">{{ retryText }}</app-button>
</ng-template>
<ng-template appAsyncLoaded>
@if (items().length === 0) {
<app-alert type="info">{{ emptyText }}</app-alert>
} @else {
<app-werkvoorraad-list [items]="items()" />
}
</ng-template>
</app-async>
}
</app-page-shell>
`,
})
export class WerkvoorraadPage {
protected store = inject(WerkvoorraadStore);
protected access = inject(AccessStore);
protected canBeoordelen = computed(() => this.access.can('aanvraag:beoordelen'));
protected items = computed(() => {
const rd = this.store.items();
return rd.tag === 'Success' ? rd.value : [];
});
protected heading = $localize`:@@werkvoorraad.heading:Werkvoorraad`;
protected intro = $localize`:@@werkvoorraad.intro:Aanvragen die op beoordeling wachten.`;
protected deniedText = $localize`:@@werkvoorraad.denied:U hebt geen rechten om de werkvoorraad te bekijken.`;
protected failedText = $localize`:@@werkvoorraad.failed:De werkvoorraad kon niet worden geladen.`;
protected emptyText = $localize`:@@werkvoorraad.empty:Er staan geen aanvragen open.`;
protected retryText = $localize`:@@werkvoorraad.retry:Opnieuw proberen`;
private loadRequested = false;
constructor() {
// Load once the capability resolves to allowed (a 403 GET would be wasted otherwise) —
// same guard-against-the-loop idiom as AdminCasesPage (WP-26 lesson).
effect(() => {
if (this.canBeoordelen() && !this.loadRequested) {
this.loadRequested = true;
void this.store.load();
}
});
}
}
+54 -14
View File
@@ -2904,21 +2904,61 @@
<context context-type="linenumber">95</context>
</context-group>
</trans-unit>
<trans-unit id="behandeling.landing.heading" datatype="html">
<source>Behandeling</source>
<target datatype="html">Case handling</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/behandeling/ui/behandeling.page.ts</context>
<context context-type="linenumber">18</context>
</context-group>
<trans-unit id="werkvoorraad.type.registratie" datatype="html">
<source>Inschrijving</source>
<target datatype="html">Registration</target>
</trans-unit>
<trans-unit id="behandeling.landing.intro" datatype="html">
<source>Hier komt de eerste behandeling-functionaliteit.</source>
<target datatype="html">The first case-handling functionality will land here.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/behandeling/ui/behandeling.page.ts</context>
<context context-type="linenumber">19</context>
</context-group>
<trans-unit id="werkvoorraad.type.herregistratie" datatype="html">
<source>Herregistratie</source>
<target datatype="html">Re-registration</target>
</trans-unit>
<trans-unit id="werkvoorraad.type.intake" datatype="html">
<source>Herregistratie-intake</source>
<target datatype="html">Re-registration intake</target>
</trans-unit>
<trans-unit id="werkvoorraad.status.ingediend" datatype="html">
<source>Ingediend</source>
<target datatype="html">Submitted</target>
</trans-unit>
<trans-unit id="werkvoorraad.status.inBehandelingHandmatig" datatype="html">
<source>In behandeling (handmatig)</source>
<target datatype="html">In progress (manual)</target>
</trans-unit>
<trans-unit id="werkvoorraad.status.inBehandeling" datatype="html">
<source>In behandeling</source>
<target datatype="html">In progress</target>
</trans-unit>
<trans-unit id="werkvoorraad.row.ingediend" datatype="html">
<source>ingediend op <x id="datum" equiv-text="formatDatumNl(item.submittedAt)"/></source>
<target datatype="html">submitted on <x id="datum" equiv-text="formatDatumNl(item.submittedAt)"/></target>
</trans-unit>
<trans-unit id="werkvoorraad.row.bsn" datatype="html">
<source>BSN <x id="bsn" equiv-text="item.owner"/></source>
<target datatype="html">BSN <x id="bsn" equiv-text="item.owner"/></target>
</trans-unit>
<trans-unit id="werkvoorraad.heading" datatype="html">
<source>Werkvoorraad</source>
<target datatype="html">Queue</target>
</trans-unit>
<trans-unit id="werkvoorraad.intro" datatype="html">
<source>Aanvragen die op beoordeling wachten.</source>
<target datatype="html">Applications waiting for review.</target>
</trans-unit>
<trans-unit id="werkvoorraad.denied" datatype="html">
<source>U hebt geen rechten om de werkvoorraad te bekijken.</source>
<target datatype="html">You do not have permission to view the queue.</target>
</trans-unit>
<trans-unit id="werkvoorraad.failed" datatype="html">
<source>De werkvoorraad kon niet worden geladen.</source>
<target datatype="html">The queue could not be loaded.</target>
</trans-unit>
<trans-unit id="werkvoorraad.empty" datatype="html">
<source>Er staan geen aanvragen open.</source>
<target datatype="html">There are no open applications.</target>
</trans-unit>
<trans-unit id="werkvoorraad.retry" datatype="html">
<source>Opnieuw proberen</source>
<target datatype="html">Try again</target>
</trans-unit>
<trans-unit id="beheer.noTables" datatype="html">
<source>Er is geen stamdata om te beheren.</source>
+234 -150
View File
@@ -5,686 +5,770 @@
<trans-unit id="form.verplichteVelden" datatype="html">
<source>* verplichte velden</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
<context context-type="sourcefile">apps/behandelportal/src/app/auth/ui/login-form/login-form.component.ts</context>
<context context-type="linenumber">15,18</context>
</context-group>
</trans-unit>
<trans-unit id="login.bsnLabel" datatype="html">
<source>BSN</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
<context context-type="sourcefile">apps/behandelportal/src/app/auth/ui/login-form/login-form.component.ts</context>
<context context-type="linenumber">22,23</context>
</context-group>
</trans-unit>
<trans-unit id="login.bsnDescription" datatype="html">
<source>9-cijferig BSN, elfproef-geldig (demo: 123456782)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
<context context-type="sourcefile">apps/behandelportal/src/app/auth/ui/login-form/login-form.component.ts</context>
<context context-type="linenumber">25,28</context>
</context-group>
</trans-unit>
<trans-unit id="login.wachtwoordLabel" datatype="html">
<source>Wachtwoord</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
<context context-type="sourcefile">apps/behandelportal/src/app/auth/ui/login-form/login-form.component.ts</context>
<context context-type="linenumber">36,37</context>
</context-group>
</trans-unit>
<trans-unit id="login.submit" datatype="html">
<source>Inloggen met DigiD</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
<context context-type="sourcefile">apps/behandelportal/src/app/auth/ui/login-form/login-form.component.ts</context>
<context context-type="linenumber">41,43</context>
</context-group>
</trans-unit>
<trans-unit id="login.heading" datatype="html">
<source>Inloggen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/auth/ui/login.page.ts</context>
<context context-type="sourcefile">apps/behandelportal/src/app/auth/ui/login.page.ts</context>
<context context-type="linenumber">14,16</context>
</context-group>
</trans-unit>
<trans-unit id="login.intro" datatype="html">
<source>Log in op uw persoonlijke BIG-register omgeving.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/auth/ui/login.page.ts</context>
<context context-type="sourcefile">apps/behandelportal/src/app/auth/ui/login.page.ts</context>
<context context-type="linenumber">17,19</context>
</context-group>
</trans-unit>
<trans-unit id="behandeling.landing.heading" datatype="html">
<source>Behandeling</source>
<trans-unit id="werkvoorraad.type.registratie" datatype="html">
<source>Inschrijving</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/behandeling/ui/behandeling.page.ts</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/domain/werkvoorraad-item-view.ts</context>
<context context-type="linenumber">8</context>
</context-group>
</trans-unit>
<trans-unit id="behandeling.landing.intro" datatype="html">
<source>Hier komt de eerste behandeling-functionaliteit.</source>
<trans-unit id="werkvoorraad.type.herregistratie" datatype="html">
<source>Herregistratie</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/behandeling/ui/behandeling.page.ts</context>
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/domain/werkvoorraad-item-view.ts</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.type.intake" datatype="html">
<source>Herregistratie-intake</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/domain/werkvoorraad-item-view.ts</context>
<context context-type="linenumber">10</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.status.ingediend" datatype="html">
<source>Ingediend</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/domain/werkvoorraad-item-view.ts</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.status.inBehandelingHandmatig" datatype="html">
<source>In behandeling (handmatig)</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/domain/werkvoorraad-item-view.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.status.inBehandeling" datatype="html">
<source>In behandeling</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/domain/werkvoorraad-item-view.ts</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.row.ingediend" datatype="html">
<source>ingediend op <x id="datum" equiv-text="formatDatumNl(item.submittedAt)"/></source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/domain/werkvoorraad-item-view.ts</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.row.bsn" datatype="html">
<source>BSN <x id="bsn" equiv-text="item.owner"/></source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/domain/werkvoorraad-item-view.ts</context>
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.heading" datatype="html">
<source>Werkvoorraad</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/ui/werkvoorraad.page.ts</context>
<context context-type="linenumber">57</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.intro" datatype="html">
<source>Aanvragen die op beoordeling wachten.</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/ui/werkvoorraad.page.ts</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.denied" datatype="html">
<source>U hebt geen rechten om de werkvoorraad te bekijken.</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/ui/werkvoorraad.page.ts</context>
<context context-type="linenumber">59</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.failed" datatype="html">
<source>De werkvoorraad kon niet worden geladen.</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/ui/werkvoorraad.page.ts</context>
<context context-type="linenumber">60</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.empty" datatype="html">
<source>Er staan geen aanvragen open.</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/ui/werkvoorraad.page.ts</context>
<context context-type="linenumber">61</context>
</context-group>
</trans-unit>
<trans-unit id="werkvoorraad.retry" datatype="html">
<source>Opnieuw proberen</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/behandeling/ui/werkvoorraad.page.ts</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="header.nav.overzicht" datatype="html">
<source>Overzicht</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/shell/nav.config.ts</context>
<context context-type="linenumber">6</context>
</context-group>
</trans-unit>
<trans-unit id="header.nav.stamdata" datatype="html">
<source>Stamdata</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/shell/nav.config.ts</context>
<context context-type="linenumber">14</context>
</context-group>
</trans-unit>
<trans-unit id="admin.link.stamdata.desc" datatype="html">
<source>Business-tabellen onderhouden</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/shell/nav.config.ts</context>
<context context-type="linenumber">15</context>
</context-group>
</trans-unit>
<trans-unit id="header.nav.audit" datatype="html">
<source>Auditlog</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/shell/nav.config.ts</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="admin.link.audit.desc" datatype="html">
<source>Toegangs- en inzagebeslissingen bekijken</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/shell/nav.config.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="header.nav.functies" datatype="html">
<source>Functievlaggen</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/shell/nav.config.ts</context>
<context context-type="linenumber">26</context>
</context-group>
</trans-unit>
<trans-unit id="admin.link.functies.desc" datatype="html">
<source>Functionaliteit aan- of uitzetten</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/behandelportal/src/app/shell/nav.config.ts</context>
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.noTables" datatype="html">
<source>Er is geen stamdata om te beheren.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/application/stamdata.store.ts</context>
<context context-type="sourcefile">libs/beheer/src/application/stamdata.store.ts</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.validation.key" datatype="html">
<source>Vul de sleutelkolom in.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/domain/stamdata.ts</context>
<context context-type="sourcefile">libs/beheer/src/domain/stamdata.ts</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.validation.van" datatype="html">
<source>Vul een &apos;geldig van&apos;-datum in.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/domain/stamdata.ts</context>
<context context-type="sourcefile">libs/beheer/src/domain/stamdata.ts</context>
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.validation.range" datatype="html">
<source>&apos;Geldig tot&apos; moet ná &apos;geldig van&apos; liggen.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/domain/stamdata.ts</context>
<context context-type="sourcefile">libs/beheer/src/domain/stamdata.ts</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.load.failed" datatype="html">
<source>De stamdata kon niet worden geladen.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/infrastructure/stamdata.adapter.ts</context>
<context context-type="sourcefile">libs/beheer/src/infrastructure/stamdata.adapter.ts</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="audit.heading" datatype="html">
<source>Auditlog</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">102</context>
</context-group>
</trans-unit>
<trans-unit id="audit.intro" datatype="html">
<source>Toegangs- en inzagebeslissingen (autorisatie en het tonen van afgeschermde gegevens). Vastgelegd zonder persoonsgegevens.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">103</context>
</context-group>
</trans-unit>
<trans-unit id="audit.denied" datatype="html">
<source>U hebt geen rechten om de auditlog te bekijken.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">104</context>
</context-group>
</trans-unit>
<trans-unit id="audit.failed" datatype="html">
<source>De auditlog kon niet worden geladen.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">105</context>
</context-group>
</trans-unit>
<trans-unit id="audit.empty" datatype="html">
<source>Nog geen auditregels.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">106</context>
</context-group>
</trans-unit>
<trans-unit id="audit.retry" datatype="html">
<source>Opnieuw proberen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">107</context>
</context-group>
</trans-unit>
<trans-unit id="audit.col.tijd" datatype="html">
<source>Tijd</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">108</context>
</context-group>
</trans-unit>
<trans-unit id="audit.col.actie" datatype="html">
<source>Actie</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">109</context>
</context-group>
</trans-unit>
<trans-unit id="audit.col.resource" datatype="html">
<source>Resource</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">110</context>
</context-group>
</trans-unit>
<trans-unit id="audit.col.besluit" datatype="html">
<source>Besluit</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">111</context>
</context-group>
</trans-unit>
<trans-unit id="audit.col.rol" datatype="html">
<source>Rol</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="audit.col.cid" datatype="html">
<source>Correlatie-id</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/audit.page.ts</context>
<context context-type="linenumber">113</context>
</context-group>
</trans-unit>
<trans-unit id="flags.heading" datatype="html">
<source>Functievlaggen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/feature-flags.page.ts</context>
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit id="flags.intro" datatype="html">
<source>Zet functionaliteit aan of uit tijdens runtime. De catalogus staat vast in code; hier beheert u de status.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/feature-flags.page.ts</context>
<context context-type="linenumber">83</context>
</context-group>
</trans-unit>
<trans-unit id="flags.denied" datatype="html">
<source>U hebt geen rechten om functievlaggen te beheren.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/feature-flags.page.ts</context>
<context context-type="linenumber">84</context>
</context-group>
</trans-unit>
<trans-unit id="flags.failed" datatype="html">
<source>De functievlaggen konden niet worden geladen.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/feature-flags.page.ts</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit id="flags.retry" datatype="html">
<source>Opnieuw proberen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/feature-flags.page.ts</context>
<context context-type="linenumber">86</context>
</context-group>
</trans-unit>
<trans-unit id="flags.on" datatype="html">
<source>Aan</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/feature-flags.page.ts</context>
<context context-type="linenumber">87</context>
</context-group>
</trans-unit>
<trans-unit id="flags.off" datatype="html">
<source>Uit</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/feature-flags.page.ts</context>
<context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="flags.enable" datatype="html">
<source>Aanzetten</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/feature-flags.page.ts</context>
<context context-type="linenumber">89</context>
</context-group>
</trans-unit>
<trans-unit id="flags.disable" datatype="html">
<source>Uitzetten</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/feature-flags.page.ts</context>
<context context-type="linenumber">90</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.added" datatype="html">
<source>toegevoegd</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">228</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.edited" datatype="html">
<source>gewijzigd</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.removed" datatype="html">
<source>verwijderd</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">230</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.table" datatype="html">
<source>Tabel</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">236</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.peildatum" datatype="html">
<source>Toon geldig op</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">237</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.showAll" datatype="html">
<source>Toon alles</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">238</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.previewNote" datatype="html">
<source>Voorbeeld: alleen de rijen die op deze datum geldig zijn. Bewerken staat uit.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">239</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.actions" datatype="html">
<source>Acties</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">240</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.remove" datatype="html">
<source>Verwijderen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">241</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.expire" datatype="html">
<source>Sluiten per vandaag</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">242</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.removeConfirm" datatype="html">
<source>Rij verwijderen? Als andere gegevens ernaar verwijzen, faalt de build-controle (CI). Bij een tabel met een geldigheidsperiode kunt u de rij beter sluiten (geldig tot) in plaats van verwijderen.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">243</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.undo" datatype="html">
<source>Ongedaan maken</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.redo" datatype="html">
<source>Opnieuw uitvoeren</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">258</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.addRow" datatype="html">
<source>Rij toevoegen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">259</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.download" datatype="html">
<source>Download JSON</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">260</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.applyHint" datatype="html">
<source>Wijzigingen worden als JSON-bestand gedownload en via een pull request toegepast — de build (CI) controleert ze.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
<context context-type="linenumber">261</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.page.heading" datatype="html">
<source>Stamdata onderhouden</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata.page.ts</context>
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.page.intro" datatype="html">
<source>Beheer de business-tabellen die de registratie stuurt. Wijzigingen worden als JSON gedownload en via een pull request toegepast; de build blijft de bewaker.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata.page.ts</context>
<context context-type="linenumber">73</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.page.denied" datatype="html">
<source>U hebt geen rechten om stamdata te onderhouden.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata.page.ts</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.page.failed" datatype="html">
<source>De stamdata kon niet worden geladen.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata.page.ts</context>
<context context-type="linenumber">75</context>
</context-group>
</trans-unit>
<trans-unit id="beheer.page.retry" datatype="html">
<source>Opnieuw proberen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/beheer/ui/stamdata.page.ts</context>
<context context-type="sourcefile">libs/beheer/src/ui/stamdata.page.ts</context>
<context context-type="linenumber">76</context>
</context-group>
</trans-unit>
<trans-unit id="submit.failed" datatype="html">
<source>Het indienen is niet gelukt. Probeer het later opnieuw.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/application/submit.ts</context>
<context context-type="sourcefile">libs/shared/src/application/submit.ts</context>
<context context-type="linenumber">28</context>
</context-group>
</trans-unit>
<trans-unit id="validation.bsn" datatype="html">
<source>Voer een geldig BSN van 9 cijfers in.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/kernel/bsn.ts</context>
<context context-type="sourcefile">libs/shared/src/kernel/bsn.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="validation.bsnElfproef" datatype="html">
<source>Dit is geen geldig BSN (klopt niet met de elfproef).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/kernel/bsn.ts</context>
<context context-type="sourcefile">libs/shared/src/kernel/bsn.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="header.nav.stamdata" datatype="html">
<source>Stamdata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="admin.link.stamdata.desc" datatype="html">
<source>Business-tabellen onderhouden</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="header.nav.audit" datatype="html">
<source>Auditlog</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="admin.link.audit.desc" datatype="html">
<source>Toegangs- en inzagebeslissingen bekijken</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
<context context-type="linenumber">24</context>
</context-group>
</trans-unit>
<trans-unit id="header.nav.functies" datatype="html">
<source>Functievlaggen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
<context context-type="linenumber">29</context>
</context-group>
</trans-unit>
<trans-unit id="admin.link.functies.desc" datatype="html">
<source>Functionaliteit aan- of uitzetten</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="crumb.dashboard" datatype="html">
<source>Mijn overzicht</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="linenumber">12</context>
</context-group>
</trans-unit>
<trans-unit id="crumb.registratie" datatype="html">
<source>Mijn gegevens</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="crumb.registreren" datatype="html">
<source>Inschrijven</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="linenumber">14</context>
</context-group>
</trans-unit>
<trans-unit id="crumb.herregistratie" datatype="html">
<source>Herregistratie</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
<trans-unit id="crumb.intake" datatype="html">
<source>Herregistratie-intake</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="crumb.concepts" datatype="html">
<source>Functionele patronen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/breadcrumb/breadcrumb-trail.ts</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="breadcrumb.aria" datatype="html">
<source>Kruimelpad</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/breadcrumb/breadcrumb.component.ts</context>
<context context-type="linenumber">27,28</context>
</context-group>
</trans-unit>
<trans-unit id="breadcrumb.hier" datatype="html">
<source>U bevindt zich hier:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/breadcrumb/breadcrumb.component.ts</context>
<context context-type="linenumber">28,29</context>
</context-group>
</trans-unit>
<trans-unit id="lang.navLabel" datatype="html">
<source>Taal / Language</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/language-switcher/language-switcher.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/language-switcher/language-switcher.component.ts</context>
<context context-type="linenumber">95</context>
</context-group>
</trans-unit>
<trans-unit id="lang.heading" datatype="html">
<source>Kies een taal</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/language-switcher/language-switcher.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/language-switcher/language-switcher.component.ts</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit id="pageShell.backLabel" datatype="html">
<source>Terug naar overzicht</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/page-shell/page-shell.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/page-shell/page-shell.component.ts</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="shell.skipLink" datatype="html">
<source>Naar de inhoud</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/shell/shell.component.ts</context>
<context context-type="linenumber">53,54</context>
<context context-type="sourcefile">libs/shared/src/layout/shell/shell.component.ts</context>
<context context-type="linenumber">62,63</context>
</context-group>
</trans-unit>
<trans-unit id="footer.tagline" datatype="html">
<source>De Rijksoverheid. Voor Nederland.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/site-footer/site-footer.component.ts</context>
<context context-type="linenumber">85,86</context>
</context-group>
</trans-unit>
<trans-unit id="footer.ministry" datatype="html">
<source> CIBG — Ministerie van Volksgezondheid, Welzijn en Sport </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/site-footer/site-footer.component.ts</context>
<context context-type="linenumber">87,89</context>
</context-group>
</trans-unit>
<trans-unit id="footer.overSiteAria" datatype="html">
<source>Over deze site</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/site-footer/site-footer.component.ts</context>
<context context-type="linenumber">90,91</context>
</context-group>
</trans-unit>
<trans-unit id="footer.overSite" datatype="html">
<source>Over deze site</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/site-footer/site-footer.component.ts</context>
<context context-type="linenumber">91,92</context>
</context-group>
</trans-unit>
<trans-unit id="footer.privacy" datatype="html">
<source>Privacy</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/site-footer/site-footer.component.ts</context>
<context context-type="linenumber">99,101</context>
</context-group>
</trans-unit>
<trans-unit id="footer.cookies" datatype="html">
<source>Cookies</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/site-footer/site-footer.component.ts</context>
<context context-type="linenumber">108,110</context>
</context-group>
</trans-unit>
<trans-unit id="footer.toegankelijkheid" datatype="html">
<source>Toegankelijkheid</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/site-footer/site-footer.component.ts</context>
<context context-type="linenumber">117,120</context>
</context-group>
</trans-unit>
<trans-unit id="footer.demo" datatype="html">
<source>Demo / POC — geen echte gegevens.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
<context context-type="sourcefile">libs/shared/src/layout/site-footer/site-footer.component.ts</context>
<context context-type="linenumber">122,124</context>
</context-group>
</trans-unit>
<trans-unit id="header.nav.overzicht" datatype="html">
<source>Overzicht</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-header/site-header.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="header.sender" datatype="html">
<source>BIG-register</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-header/site-header.component.ts</context>
<context context-type="linenumber">54,55</context>
<context context-type="sourcefile">libs/shared/src/layout/site-header/site-header.component.ts</context>
<context context-type="linenumber">44,45</context>
</context-group>
</trans-unit>
<trans-unit id="header.ministry" datatype="html">
<source>Ministerie van Volksgezondheid, Welzijn en Sport</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-header/site-header.component.ts</context>
<context context-type="linenumber">56,58</context>
<context context-type="sourcefile">libs/shared/src/layout/site-header/site-header.component.ts</context>
<context context-type="linenumber">46,48</context>
</context-group>
</trans-unit>
<trans-unit id="header.uitloggen" datatype="html">
<source> Uitloggen </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-header/site-header.component.ts</context>
<context context-type="linenumber">78,79</context>
<context context-type="sourcefile">libs/shared/src/layout/site-header/site-header.component.ts</context>
<context context-type="linenumber">68,69</context>
</context-group>
</trans-unit>
<trans-unit id="header.navAria" datatype="html">
<source>Hoofdnavigatie</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/site-header/site-header.component.ts</context>
<context context-type="linenumber">86,87</context>
<context context-type="sourcefile">libs/shared/src/layout/site-header/site-header.component.ts</context>
<context context-type="linenumber">76,77</context>
</context-group>
</trans-unit>
<trans-unit id="alert.icon.info" datatype="html">
<source>Informatie</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/ui/alert/alert.component.ts</context>
<context context-type="sourcefile">libs/shared/src/ui/alert/alert.component.ts</context>
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit id="alert.icon.ok" datatype="html">
<source>Gelukt</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/ui/alert/alert.component.ts</context>
<context context-type="sourcefile">libs/shared/src/ui/alert/alert.component.ts</context>
<context context-type="linenumber">8</context>
</context-group>
</trans-unit>
<trans-unit id="alert.icon.warning" datatype="html">
<source>Waarschuwing</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/ui/alert/alert.component.ts</context>
<context context-type="sourcefile">libs/shared/src/ui/alert/alert.component.ts</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="alert.icon.error" datatype="html">
<source>Foutmelding</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/ui/alert/alert.component.ts</context>
<context context-type="sourcefile">libs/shared/src/ui/alert/alert.component.ts</context>
<context context-type="linenumber">10</context>
</context-group>
</trans-unit>
<trans-unit id="async.error" datatype="html">
<source>Er ging iets mis bij het laden van de gegevens.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/ui/async/async.component.ts</context>
<context context-type="sourcefile">libs/shared/src/ui/async/async.component.ts</context>
<context context-type="linenumber">105</context>
</context-group>
</trans-unit>
<trans-unit id="async.retry" datatype="html">
<source>Opnieuw proberen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/ui/async/async.component.ts</context>
<context context-type="sourcefile">libs/shared/src/ui/async/async.component.ts</context>
<context context-type="linenumber">106</context>
</context-group>
</trans-unit>
<trans-unit id="async.empty" datatype="html">
<source>Geen gegevens gevonden.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/ui/async/async.component.ts</context>
<context context-type="sourcefile">libs/shared/src/ui/async/async.component.ts</context>
<context context-type="linenumber">107</context>
</context-group>
</trans-unit>
<trans-unit id="spinner.aria" datatype="html">
<source>Bezig met laden</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/ui/spinner/spinner.component.ts</context>
<context context-type="sourcefile">libs/shared/src/ui/spinner/spinner.component.ts</context>
<context context-type="linenumber">36,40</context>
</context-group>
</trans-unit>
+31 -1
View File
@@ -409,6 +409,17 @@ api.MapGet("/admin/cases", (HttpContext ctx, IZaakSource zaken) => CasesAdmin(ct
.Produces<List<ApplicationSummaryDto>>()
.ProducesProblem(StatusCodes.Status403Forbidden);
// --- Werkvoorraad (WP-64): the behandelportal's queue of aanvragen needing treatment. ---
// Cross-owner like /admin/cases, but gated by the medewerker capability (`CanBeoordelen`,
// WP-62) rather than the admin role, and pre-filtered to the two "still open" status tags —
// a behandelaar never needs to see a Concept (not their business yet) or a terminal case.
api.MapGet("/werkvoorraad", (HttpContext ctx, IZaakSource zaken) => Werkvoorraad(ctx, () =>
Results.Ok(zaken.ListCases(DateTimeOffset.UtcNow)
.Where(c => c.Status.Tag is "Ingediend" or "InBehandeling")
.ToList())))
.Produces<List<ApplicationSummaryDto>>()
.ProducesProblem(StatusCodes.Status403Forbidden);
// OpenZaak's Notificaties API (NRC) calls this on every zaak event once an `abonnement` is
// provisioned (WP-52, out-of-band — see openzaak-integration.md, no app code subscribes it).
// The caller is NRC, not a user: no Principal, so this audits via AuthzAuditStore directly
@@ -462,7 +473,15 @@ api.MapGet("/admin/audit", (HttpContext ctx) => CasesAdmin(ctx, () =>
// PRD-0002 §6: coarse, role-derived capabilities for nav/menu-level checks (NOT
// tied to a specific brief's live status — see BriefDecisionsDto for that).
api.MapGet("/me", (HttpContext ctx) => new MeDto(Authz.RoleCapabilities(Authz.ResolvePrincipal(ctx))))
// WP-64: `aanvraag:beoordelen` is caller-kind-derived (CanBeoordelen), not role-derived like
// the rest of RoleCapabilities — appended here rather than folded into that switch, since it
// depends on CallerIdentity (medewerker rollen), not the dev X-Role stand-in.
api.MapGet("/me", (HttpContext ctx) =>
{
var caps = Authz.RoleCapabilities(Authz.ResolvePrincipal(ctx)).ToList();
if (Authz.CanBeoordelen(ctx.Caller())) caps.Add("aanvraag:beoordelen");
return new MeDto(caps);
})
.Produces<MeDto>();
// Feature flags (WP-47). GET is readable by any principal (it drives FE gating); the toggle is
@@ -689,6 +708,17 @@ IResult CasesAdmin(HttpContext ctx, Func<IResult> action)
statusCode: StatusCodes.Status403Forbidden);
}
// One gate for the werkvoorraad read — the enforce twin of `CanBeoordelen` (WP-62/64).
// Unlike the other *Admin gates above, this checks the CallerIdentity directly (medewerker
// rollen), not a role-only Principal — a zorgverlener with X-Role=admin still gets denied.
IResult Werkvoorraad(HttpContext ctx, Func<IResult> action)
{
if (Authz.CanBeoordelen(ctx.Caller())) return action();
AuditAuthz(ctx, "aanvraag:beoordelen", "werkvoorraad", false, Authz.ResolvePrincipal(ctx));
return Results.Problem(detail: "Alleen een behandelaar mag de werkvoorraad bekijken.",
statusCode: StatusCodes.Status403Forbidden);
}
// One gate for the feature-flag toggle — the enforce twin of `flags:manage` (WP-47).
IResult FlagsAdmin(HttpContext ctx, Func<IResult> action)
{
+32
View File
@@ -766,6 +766,38 @@
}
}
},
"/api/v1/werkvoorraad": {
"get": {
"tags": [
"BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ApplicationSummaryDto"
}
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/api/v1/admin/cases/{id}": {
"delete": {
"tags": [
@@ -0,0 +1,98 @@
using System.Net;
using System.Net.Http.Json;
using BigRegister.Api.Contracts;
using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
/// WP-64: the behandelportal's queue of aanvragen needing treatment, gated by the
/// medewerker capability `CanBeoordelen` (WP-62) — not the admin role.
public class WerkvoorraadTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
{
private readonly HttpClient _client = factory.CreateClient();
private HttpRequestMessage AsBehandelaar(string path)
{
var req = new HttpRequestMessage(HttpMethod.Get, path);
req.Headers.Add("X-Medewerker", "medewerker-1");
return req;
}
private async Task<ApplicationDetailDto> CreateAndSubmitHerregistratie()
{
var created = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "herregistratie" });
var a = (await created.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
(await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { uren = 200 }))
.EnsureSuccessStatusCode();
return a;
}
[Fact]
public async Task Behandelaar_sees_submitted_cases_in_the_queue()
{
var a = await CreateAndSubmitHerregistratie();
try
{
var res = await _client.SendAsync(AsBehandelaar("/api/v1/werkvoorraad"));
res.EnsureSuccessStatusCode();
var queue = (await res.Content.ReadFromJsonAsync<List<ApplicationSummaryDto>>())!;
var mine = queue.Single(x => x.Id == a.Id);
Assert.Equal("InBehandeling", mine.Status.Tag);
Assert.False(string.IsNullOrEmpty(mine.Owner)); // cross-owner, like /admin/cases
}
finally
{
await _client.SendAsync(new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/admin/cases/{a.Id}")
{
Headers = { { "X-Role", "admin" } },
});
}
}
[Fact]
public async Task Queue_excludes_concepts()
{
var created = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "herregistratie" });
var a = (await created.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
try
{
var res = await _client.SendAsync(AsBehandelaar("/api/v1/werkvoorraad"));
res.EnsureSuccessStatusCode();
var queue = (await res.Content.ReadFromJsonAsync<List<ApplicationSummaryDto>>())!;
Assert.DoesNotContain(queue, x => x.Id == a.Id);
}
finally
{
await _client.DeleteAsync($"/api/v1/applications/{a.Id}");
}
}
[Fact]
public async Task Zorgverlener_is_forbidden_even_with_admin_role()
{
var req = new HttpRequestMessage(HttpMethod.Get, "/api/v1/werkvoorraad");
req.Headers.Add("X-Role", "admin"); // admin role, but no X-Medewerker — still a zorgverlener
Assert.Equal(HttpStatusCode.Forbidden, (await _client.SendAsync(req)).StatusCode);
}
[Fact]
public async Task Medewerker_without_behandelaar_rol_is_forbidden()
{
var req = new HttpRequestMessage(HttpMethod.Get, "/api/v1/werkvoorraad");
req.Headers.Add("X-Medewerker", "medewerker-2");
req.Headers.Add("X-Rollen", "geen");
Assert.Equal(HttpStatusCode.Forbidden, (await _client.SendAsync(req)).StatusCode);
}
[Fact]
public async Task Me_reports_the_capability_only_for_a_behandelaar()
{
var behandelaar = new HttpRequestMessage(HttpMethod.Get, "/api/v1/me");
behandelaar.Headers.Add("X-Medewerker", "medewerker-1");
var caps = (await (await _client.SendAsync(behandelaar)).Content.ReadFromJsonAsync<MeDto>())!;
Assert.Contains("aanvraag:beoordelen", caps.Capabilities);
var zorgverlener = (await (await _client.GetAsync("/api/v1/me")).Content.ReadFromJsonAsync<MeDto>())!;
Assert.DoesNotContain("aanvraag:beoordelen", zorgverlener.Capabilities);
}
}
@@ -1,6 +1,6 @@
# WP-64 — Behandelportal: werkvoorraad (queue) screen
Status: todo
Status: done
Phase: 11 — Behandelportal
## Why
@@ -44,10 +44,36 @@ behandelportal app (domain/infrastructure/application/ui per the house layering)
## Acceptance criteria
- [ ] Werkvoorraad screen lists aanvragen needing treatment for an authenticated
- [x] Werkvoorraad screen lists aanvragen needing treatment for an authenticated
medewerker.
- [ ] `npm run ci` green in the behandelportal app; Storybook story present.
- [ ] Endpoint follows BFF-lite discipline (decision-enriched, not raw passthrough).
- [x] `npm run ci` green in the behandelportal app; Storybook story present.
- [x] Endpoint follows BFF-lite discipline (decision-enriched, not raw passthrough).
## Outcome
`GET /werkvoorraad` reuses the existing `ApplicationSummaryDto`/`IZaakSource.ListCases`
no new DTO — filtered server-side to `Status.Tag is "Ingediend" or "InBehandeling"`. Gated
by a new `Werkvoorraad` local-function twin of `CasesAdmin`, but checking
`Authz.CanBeoordelen(ctx.Caller())` (the CallerIdentity directly) rather than a
role-only `Principal` — a zorgverlener with `X-Role=admin` is still denied (covered by a test).
One course correction beyond the pre-made decisions: `GET /me` didn't expose any
capability a medewerker could hold (`RoleCapabilities` only switches on `PrincipalRole`,
which every `MedewerkerCaller` also carries but doesn't determine `CanBeoordelen`). Added
one line — `if (Authz.CanBeoordelen(ctx.Caller())) caps.Add("aanvraag:beoordelen")` — and a
matching `Capability` union member in `libs/shared`, so the FE page can use the same
deny-by-default `AccessStore`/`capabilityGuard` idiom every other gated page uses (avoids a
wasted 403 GET and a denial flash), instead of inventing a second gating mechanism.
FE: the queue item's domain type (`behandeling/domain/werkvoorraad-item.ts`) is
deliberately **narrower** than the SSP's full `AanvraagStatus` union — only
`Ingediend`/`InBehandeling` — so a case the backend's filter let through with any other
tag is a parse error, not a silently-rendered row. Composed entirely from existing
`libs/shared/ui` molecules (`ApplicationListComponent` + `ApplicationLinkComponent`, the
same ones ssp's dashboard uses) via one new organism, `WerkvoorraadListComponent` — no new
atom. The stopgap `behandeling.page.ts`/`BehandelingPage` (WP-61's scaffold placeholder,
its own TODO said to replace it) is gone; `/dashboard` now loads `WerkvoorraadPage`
directly, and the redundant `/behandeling` route (same placeholder, two paths) was dropped.
## Verification
+2 -1
View File
@@ -9,4 +9,5 @@ export type Capability =
| 'orgtemplate:edit'
| 'stamdata:edit'
| 'cases:manage'
| 'flags:manage';
| 'flags:manage'
| 'aanvraag:beoordelen';
@@ -1074,6 +1074,48 @@ export class ApiClient {
return Promise.resolve<ApplicationSummaryDto[]>(null as any);
}
/**
* @return OK
*/
werkvoorraad(): Promise<ApplicationSummaryDto[]> {
let url_ = this.baseUrl + "/api/v1/werkvoorraad";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processWerkvoorraad(_response);
});
}
protected processWerkvoorraad(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
*/