feat(aanvragen): richer rows (purpose + status) linking to a case-detail page
- Aanvragen rows now show what the aanvraag is for (purpose subtitle) and an explicit status label (In behandeling / Goedgekeurd / Afgewezen) alongside the reference + submit date, via an expanded aanvraag-view (purposeLabel, statusLabel, referentie, detailRows) + spec. - Rows link to a new /aanvraag/:id case-detail page, so the CIBG chevron shows and each aanvraag opens as a (stub) case — it lists soort/waarvoor/status/ referentie/ingediend in a Datablock, with a note that full handling is future. GREEN: lint, tokens, 183 tests, build, 137 axe stories. Verified visually (dashboard aanvragen rows, upload drop-zone, datablock) via Storybook screenshots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -11,6 +11,7 @@ export const routes: Routes = [
|
|||||||
{ path: 'login', loadComponent: () => import('@auth/ui/login.page').then(m => m.LoginPage) },
|
{ path: 'login', loadComponent: () => import('@auth/ui/login.page').then(m => m.LoginPage) },
|
||||||
{ path: 'dashboard', canActivate: [authGuard], loadComponent: () => import('@registratie/ui/dashboard.page').then(m => m.DashboardPage) },
|
{ path: 'dashboard', canActivate: [authGuard], loadComponent: () => import('@registratie/ui/dashboard.page').then(m => m.DashboardPage) },
|
||||||
{ path: 'registratie', canActivate: [authGuard], loadComponent: () => import('@registratie/ui/registration-detail.page').then(m => m.RegistrationDetailPage) },
|
{ path: 'registratie', canActivate: [authGuard], loadComponent: () => import('@registratie/ui/registration-detail.page').then(m => m.RegistrationDetailPage) },
|
||||||
|
{ path: 'aanvraag/:id', canActivate: [authGuard], loadComponent: () => import('@registratie/ui/aanvraag-detail.page').then(m => m.AanvraagDetailPage) },
|
||||||
{ path: 'registreren', canActivate: [authGuard], loadComponent: () => import('@registratie/ui/registratie.page').then(m => m.RegistratiePage) },
|
{ path: 'registreren', canActivate: [authGuard], loadComponent: () => import('@registratie/ui/registratie.page').then(m => m.RegistratiePage) },
|
||||||
{ path: 'herregistratie', canActivate: [authGuard], loadComponent: () => import('@herregistratie/ui/herregistratie.page').then(m => m.HerregistratiePage) },
|
{ path: 'herregistratie', canActivate: [authGuard], loadComponent: () => import('@herregistratie/ui/herregistratie.page').then(m => m.HerregistratiePage) },
|
||||||
{ path: 'intake', canActivate: [authGuard], loadComponent: () => import('@herregistratie/ui/intake.page').then(m => m.IntakePage) },
|
{ path: 'intake', canActivate: [authGuard], loadComponent: () => import('@herregistratie/ui/intake.page').then(m => m.IntakePage) },
|
||||||
|
|||||||
@@ -1,30 +1,45 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { submittedRow, TYPE_LABELS } from './aanvraag-view';
|
import { submittedRow, detailRows, purposeLabel, statusLabel, TYPE_LABELS } from './aanvraag-view';
|
||||||
import { Aanvraag } from './aanvraag';
|
import { Aanvraag } from './aanvraag';
|
||||||
|
|
||||||
const base = { id: '1', type: 'herregistratie' as const, documentIds: [], createdAt: '', updatedAt: '', submittedAt: '2024-05-12' };
|
const base = { id: '1', type: 'herregistratie' as const, documentIds: [], createdAt: '', updatedAt: '', submittedAt: '2024-05-12' };
|
||||||
|
|
||||||
describe('submittedRow', () => {
|
describe('submittedRow', () => {
|
||||||
it('InBehandeling: reference + submit date; manual note only when manual', () => {
|
it('heading is the type, subtitle is the purpose', () => {
|
||||||
const auto = submittedRow({ ...base, status: { tag: 'InBehandeling', referentie: 'R1', manual: false } } as Aanvraag);
|
const row = submittedRow({ ...base, status: { tag: 'InBehandeling', referentie: 'R1', manual: false } } as Aanvraag);
|
||||||
expect(auto.heading).toBe(TYPE_LABELS.herregistratie);
|
expect(row.heading).toBe(TYPE_LABELS.herregistratie);
|
||||||
expect(auto.status).toContain('R1');
|
expect(row.subtitle).toBe(purposeLabel('herregistratie'));
|
||||||
expect(auto.status).toContain('12 mei 2024');
|
});
|
||||||
expect(auto.subtitle).toBe('');
|
|
||||||
|
|
||||||
|
it('status line carries the status label, reference and submit date', () => {
|
||||||
|
const row = submittedRow({ ...base, status: { tag: 'InBehandeling', referentie: 'R1', manual: false } } as Aanvraag);
|
||||||
|
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 adds a note; rejection adds its reason', () => {
|
||||||
const manual = submittedRow({ ...base, status: { tag: 'InBehandeling', referentie: 'R1', manual: true } } as Aanvraag);
|
const manual = submittedRow({ ...base, status: { tag: 'InBehandeling', referentie: 'R1', manual: true } } as Aanvraag);
|
||||||
expect(manual.subtitle).not.toBe('');
|
expect(manual.status).toContain('handmatig');
|
||||||
});
|
const rejected = submittedRow({ ...base, status: { tag: 'Afgewezen', referentie: 'R2', reden: 'Onvoldoende uren' } } as Aanvraag);
|
||||||
|
expect(rejected.status).toContain('Onvoldoende uren');
|
||||||
it('Afgewezen: reason becomes the subtitle', () => {
|
});
|
||||||
const row = submittedRow({ ...base, status: { tag: 'Afgewezen', referentie: 'R2', reden: 'Onvoldoende uren' } } as Aanvraag);
|
});
|
||||||
expect(row.status).toContain('R2');
|
|
||||||
expect(row.subtitle).toBe('Onvoldoende uren');
|
describe('detailRows', () => {
|
||||||
});
|
it('lists soort/waarvoor/status/referentie/ingediend, plus reason when rejected', () => {
|
||||||
|
const rows = detailRows({ ...base, status: { tag: 'Afgewezen', referentie: 'R2', reden: 'Onvoldoende uren' } } as Aanvraag);
|
||||||
it('Goedgekeurd: reference only', () => {
|
const values = rows.map((r) => r.value);
|
||||||
const row = submittedRow({ ...base, status: { tag: 'Goedgekeurd', referentie: 'R3' } } as Aanvraag);
|
expect(values).toContain(TYPE_LABELS.herregistratie);
|
||||||
expect(row.status).toContain('R3');
|
expect(values).toContain('R2');
|
||||||
expect(row.subtitle).toBe('');
|
expect(values).toContain('Onvoldoende uren');
|
||||||
|
expect(rows.length).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reference falls back to em dash for a Concept', () => {
|
||||||
|
const rows = detailRows({ ...base, submittedAt: undefined, status: { tag: 'Concept', stepIndex: 0, stepCount: 3 } } as Aanvraag);
|
||||||
|
const ref = rows.find((r) => r.value === '—');
|
||||||
|
expect(ref).toBeTruthy();
|
||||||
|
expect(rows.length).toBe(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Aanvraag, AanvraagType } from './aanvraag';
|
import { Aanvraag, AanvraagStatus, AanvraagType } from './aanvraag';
|
||||||
|
|
||||||
/** View-model mapping for an aanvraag: type → label, and a submitted aanvraag →
|
/** View-model mapping for an aanvraag: type → labels, status → label, and the fields
|
||||||
the CIBG "aanvragen" row fields (heading/status/subtitle). Pure, no Angular —
|
for a CIBG "aanvragen" row / the case-detail page. Pure, no Angular — the UI
|
||||||
the UI renders these, it does not derive them. */
|
renders these, it does not derive them. */
|
||||||
|
|
||||||
export const TYPE_LABELS: Record<AanvraagType, string> = {
|
export const TYPE_LABELS: Record<AanvraagType, string> = {
|
||||||
registratie: $localize`:@@aanvraagBlock.type.registratie:Inschrijving`,
|
registratie: $localize`:@@aanvraagBlock.type.registratie:Inschrijving`,
|
||||||
@@ -10,35 +10,66 @@ export const TYPE_LABELS: Record<AanvraagType, string> = {
|
|||||||
intake: $localize`:@@aanvraagBlock.type.intake:Herregistratie-intake`,
|
intake: $localize`:@@aanvraagBlock.type.intake:Herregistratie-intake`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** What the aanvraag is for (shown under the title). */
|
||||||
|
export function purposeLabel(type: AanvraagType): string {
|
||||||
|
switch (type) {
|
||||||
|
case 'registratie': return $localize`:@@aanvraag.purpose.registratie:Inschrijving in het BIG-register`;
|
||||||
|
case 'herregistratie': return $localize`:@@aanvraag.purpose.herregistratie:Verlenging van uw BIG-registratie`;
|
||||||
|
case 'intake': return $localize`:@@aanvraag.purpose.intake:Intake-vragenlijst voor uw herregistratie`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The status as a plain label (what state the aanvraag is in). */
|
||||||
|
export function statusLabel(status: AanvraagStatus): string {
|
||||||
|
switch (status.tag) {
|
||||||
|
case 'Concept': return $localize`:@@aanvraag.status.concept:Concept (nog niet ingediend)`;
|
||||||
|
case 'InBehandeling': return $localize`:@@aanvraag.status.inBehandeling:In behandeling`;
|
||||||
|
case 'Goedgekeurd': return $localize`:@@aanvraag.status.goedgekeurd:Goedgekeurd`;
|
||||||
|
case 'Afgewezen': return $localize`:@@aanvraag.status.afgewezen:Afgewezen`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The reference number, or '' for a Concept (which has none yet). */
|
||||||
|
export function referentie(status: AanvraagStatus): string {
|
||||||
|
return status.tag === 'Concept' ? '' : status.referentie;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AanvraagRow {
|
export interface AanvraagRow {
|
||||||
heading: string;
|
heading: string;
|
||||||
/** Reference + submit-date line (the `.status` line of an aanvragen row). */
|
/** What the aanvraag is for (the `.subtitle` line). */
|
||||||
status: string;
|
|
||||||
/** Secondary note: manual-review notice or rejection reason. */
|
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
/** The status: label + reference + submit date (+ any note) — the `.status` line. */
|
||||||
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNL(iso?: string): string {
|
function formatNL(iso?: string): string {
|
||||||
return iso ? new Date(iso).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' }) : '';
|
return iso ? new Date(iso).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' }) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fields for a submitted aanvraag's row (Concept has no row — it renders as a
|
/** Fields for a submitted aanvraag's row in the dashboard "aanvragen" list (Concept
|
||||||
resumable melding, see aanvraag-block). */
|
has no row — it renders as a resumable melding, see aanvraag-block). */
|
||||||
export function submittedRow(a: Aanvraag): AanvraagRow {
|
export function submittedRow(a: Aanvraag): AanvraagRow {
|
||||||
const s = a.status;
|
const s = a.status;
|
||||||
const heading = TYPE_LABELS[a.type];
|
const parts = [statusLabel(s)];
|
||||||
switch (s.tag) {
|
const ref = referentie(s);
|
||||||
case 'Concept':
|
if (ref) parts.push($localize`:@@aanvraag.row.ref:Referentie ${ref}:ref:`);
|
||||||
return { heading, status: '', subtitle: '' };
|
if (a.submittedAt) parts.push($localize`:@@aanvraag.row.ingediend:ingediend op ${formatNL(a.submittedAt)}:datum:`);
|
||||||
case 'InBehandeling':
|
if (s.tag === 'InBehandeling' && s.manual) parts.push($localize`:@@aanvraagBlock.manual:Uw aanvraag wordt handmatig beoordeeld in de backoffice.`);
|
||||||
return {
|
if (s.tag === 'Afgewezen') parts.push(s.reden);
|
||||||
heading,
|
return { heading: TYPE_LABELS[a.type], subtitle: purposeLabel(a.type), status: parts.join(' · ') };
|
||||||
status: $localize`:@@aanvraagBlock.inBehandeling:Referentie ${s.referentie}:ref: · ingediend op ${formatNL(a.submittedAt)}:datum:`,
|
}
|
||||||
subtitle: s.manual ? $localize`:@@aanvraagBlock.manual:Uw aanvraag wordt handmatig beoordeeld in de backoffice.` : '',
|
|
||||||
};
|
/** Key/value rows for the case-detail page (CIBG Datablock). */
|
||||||
case 'Goedgekeurd':
|
export function detailRows(a: Aanvraag): { key: string; value: string }[] {
|
||||||
return { heading, status: $localize`:@@aanvraagBlock.goedgekeurd:Referentie ${s.referentie}:ref:`, subtitle: '' };
|
const rows = [
|
||||||
case 'Afgewezen':
|
{ key: $localize`:@@aanvraag.detail.soort:Soort aanvraag`, value: TYPE_LABELS[a.type] },
|
||||||
return { heading, status: $localize`:@@aanvraagBlock.afgewezen:Referentie ${s.referentie}:ref:`, subtitle: s.reden };
|
{ key: $localize`:@@aanvraag.detail.waarvoor:Waarvoor`, value: purposeLabel(a.type) },
|
||||||
}
|
{ key: $localize`:@@aanvraag.detail.status:Status`, value: statusLabel(a.status) },
|
||||||
|
{ key: $localize`:@@aanvraag.detail.referentie:Referentie`, value: referentie(a.status) || '—' },
|
||||||
|
{ key: $localize`:@@aanvraag.detail.ingediend:Ingediend op`, value: a.submittedAt ? formatNL(a.submittedAt) : '—' },
|
||||||
|
];
|
||||||
|
if (a.status.tag === 'Afgewezen') {
|
||||||
|
rows.push({ key: $localize`:@@aanvraag.detail.reden:Reden van afwijzing`, value: a.status.reden });
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/app/registratie/ui/aanvraag-detail.page.ts
Normal file
50
src/app/registratie/ui/aanvraag-detail.page.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||||
|
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||||
|
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||||
|
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||||
|
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||||
|
import { ASYNC } from '@shared/ui/async/async.component';
|
||||||
|
import { ApplicationsStore } from '@registratie/application/applications.store';
|
||||||
|
import { Aanvraag } from '@registratie/domain/aanvraag';
|
||||||
|
import { detailRows } from '@registratie/domain/aanvraag-view';
|
||||||
|
|
||||||
|
/** Page: a single aanvraag ("case"). Stub — it renders the aanvraag's known fields
|
||||||
|
(soort, waarvoor, status, referentie, ingediend) in a CIBG Datablock; full case
|
||||||
|
handling is future work. The dashboard "Mijn aanvragen" rows link here. */
|
||||||
|
@Component({
|
||||||
|
selector: 'app-aanvraag-detail-page',
|
||||||
|
imports: [PageShellComponent, SkeletonComponent, AlertComponent, DataBlockComponent, DataRowComponent, ...ASYNC],
|
||||||
|
template: `
|
||||||
|
<app-page-shell i18n-heading="@@aanvraagDetail.heading" heading="Aanvraag" backLink="/dashboard">
|
||||||
|
<app-async [data]="store.applications()">
|
||||||
|
<ng-template appAsyncLoaded let-list>
|
||||||
|
@let a = find($any(list));
|
||||||
|
@if (a) {
|
||||||
|
<app-data-block i18n-ariaLabel="@@aanvraagDetail.ariaLabel" ariaLabel="Aanvraaggegevens">
|
||||||
|
@for (row of rows(a); track row.key) {
|
||||||
|
<div app-data-row [key]="row.key" [value]="row.value"></div>
|
||||||
|
}
|
||||||
|
</app-data-block>
|
||||||
|
<app-alert class="app-section" type="info" i18n="@@aanvraagDetail.stub">
|
||||||
|
De volledige afhandeling van deze aanvraag is nog niet beschikbaar in deze POC.
|
||||||
|
</app-alert>
|
||||||
|
} @else {
|
||||||
|
<app-alert type="warning" i18n="@@aanvraagDetail.nietGevonden">Deze aanvraag is niet gevonden.</app-alert>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
<ng-template appAsyncLoading>
|
||||||
|
<app-skeleton height="2.5rem" [count]="5" />
|
||||||
|
</ng-template>
|
||||||
|
</app-async>
|
||||||
|
</app-page-shell>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class AanvraagDetailPage {
|
||||||
|
protected store = inject(ApplicationsStore);
|
||||||
|
private id = inject(ActivatedRoute).snapshot.paramMap.get('id') ?? '';
|
||||||
|
|
||||||
|
protected find = (list: Aanvraag[]): Aanvraag | undefined => list.find((a) => a.id === this.id);
|
||||||
|
protected rows = detailRows;
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
|||||||
<app-application-list>
|
<app-application-list>
|
||||||
@for (a of ingediend(); track a.id) {
|
@for (a of ingediend(); track a.id) {
|
||||||
@let row = submittedRow(a);
|
@let row = submittedRow(a);
|
||||||
<li app-application-link animate.enter="app-item-enter" animate.leave="app-item-leave" [heading]="row.heading" [status]="row.status" [subtitle]="row.subtitle"></li>
|
<li app-application-link animate.enter="app-item-enter" animate.leave="app-item-leave" [heading]="row.heading" [subtitle]="row.subtitle" [status]="row.status" [to]="'/aanvraag/' + a.id"></li>
|
||||||
}
|
}
|
||||||
</app-application-list>
|
</app-application-list>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const meta: Meta<ApplicationListComponent> = {
|
|||||||
render: () => ({
|
render: () => ({
|
||||||
template: `
|
template: `
|
||||||
<app-application-list>
|
<app-application-list>
|
||||||
<li app-application-link heading="Inschrijving" status="Stap 2 van 3" cta="Verder gaan" clickable="true"></li>
|
<li app-application-link heading="Herregistratie" subtitle="Verlenging van uw BIG-registratie" status="In behandeling · Referentie 2024-00123 · ingediend op 12 mei 2024" to="/aanvraag/1"></li>
|
||||||
<li app-application-link heading="Herregistratie" status="Referentie 2024-00123 · ingediend op 12 mei 2024"></li>
|
<li app-application-link heading="Inschrijving" subtitle="Inschrijving in het BIG-register" status="Goedgekeurd · Referentie 2024-00088" to="/aanvraag/2"></li>
|
||||||
<li app-application-link heading="Inschrijven" subtitle="Schrijf u in in het BIG-register." to="/registreren"></li>
|
<li app-application-link heading="Inschrijven" subtitle="Schrijf u in in het BIG-register." cta="Start inschrijving" to="/registreren"></li>
|
||||||
</app-application-list>`,
|
</app-application-list>`,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user