feat(behandelportal): WP-64 werkvoorraad (queue) screen
CI / changes (pull_request) Successful in 15s
CI / lint (pull_request) Successful in 57s
CI / frontend (pull_request) Successful in 2m36s
CI / storybook-a11y (pull_request) Failing after 3m14s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m10s
CI / e2e (pull_request) Successful in 3m3s
CI / api-client-drift (pull_request) Successful in 2m1s
CI / changes (pull_request) Successful in 15s
CI / lint (pull_request) Successful in 57s
CI / frontend (pull_request) Successful in 2m36s
CI / storybook-a11y (pull_request) Failing after 3m14s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m10s
CI / e2e (pull_request) Successful in 3m3s
CI / api-client-drift (pull_request) Successful in 2m1s
New GET /werkvoorraad endpoint lists aanvragen still open (Ingediend/InBehandeling), gated by the medewerker capability (CanBeoordelen) rather than the admin role — reuses the existing ApplicationSummaryDto, no new DTO. GET /me now surfaces aanvraag:beoordelen for a behandelaar so the FE can gate with the same AccessStore/capabilityGuard idiom every other page uses. FE: a behandeling domain type deliberately narrower than ssp's full AanvraagStatus union (only the two open tags — illegal states unrepresentable), composed into a werkvoorraad-list organism from existing shared/ui molecules. Replaces WP-61's scaffold placeholder as the app's real landing page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.`;
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
+44
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user