style: format frontend, docs and skills with prettier; add .prettierignore

One-time prettier --write so the new format:check CI gate starts green.
.prettierignore excludes generated (api-client.ts, documentation.json),
vendored (public/cibg-huisstijl), and backend (dotnet format owns it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 13:39:31 +02:00
parent 546097434d
commit e82309786d
176 changed files with 5069 additions and 1471 deletions

View File

@@ -13,9 +13,16 @@ import { blockActions } from '@registratie/domain/block-actions';
@Component({
selector: 'app-aanvraag-block',
imports: [ButtonComponent, AlertComponent],
styles: [`
.actions{display:flex;align-items:center;gap:var(--rhc-space-max-md);margin-block-start:var(--rhc-space-max-sm)}
`],
styles: [
`
.actions {
display: flex;
align-items: center;
gap: var(--rhc-space-max-md);
margin-block-start: var(--rhc-space-max-sm);
}
`,
],
template: `
@if (aanvraag().status.tag === 'Concept') {
<app-alert type="warning">
@@ -23,10 +30,14 @@ import { blockActions } from '@registratie/domain/block-actions';
<p>{{ conceptText() }}</p>
<div class="actions">
@if (actions().includes('cancel')) {
<app-button variant="subtle" (click)="cancel.emit()" i18n="@@aanvraagBlock.verwijderen">Verwijderen</app-button>
<app-button variant="subtle" (click)="cancel.emit()" i18n="@@aanvraagBlock.verwijderen"
>Verwijderen</app-button
>
}
@if (actions().includes('resume')) {
<app-button (click)="resume.emit()" i18n="@@aanvraagBlock.openen">Aanvraag openen</app-button>
<app-button (click)="resume.emit()" i18n="@@aanvraagBlock.openen"
>Aanvraag openen</app-button
>
}
</div>
</app-alert>
@@ -59,5 +70,7 @@ export class AanvraagBlockComponent {
}
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' })
: '';
}

View File

@@ -38,7 +38,36 @@ export const Concept: Story = {
args: { aanvraag: { ...base, status: { tag: 'Concept', stepIndex: 1, stepCount: 3 } } },
render: (args) => ({ props: args, template: `<app-aanvraag-block [aanvraag]="aanvraag" />` }),
};
export const InBehandelingAuto: Story = { args: { aanvraag: { ...base, status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false } } } };
export const InBehandelingManual: Story = { args: { aanvraag: { ...base, type: 'registratie', status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: true } } } };
export const Goedgekeurd: Story = { args: { aanvraag: { ...base, status: { tag: 'Goedgekeurd', referentie: 'BIG-2026-456789' } } } };
export const Afgewezen: Story = { args: { aanvraag: { ...base, type: 'herregistratie', status: { tag: 'Afgewezen', referentie: 'BIG-2026-456789', reden: 'Aanvraag afgewezen: geen gewerkte uren geregistreerd.' } } } };
export const InBehandelingAuto: Story = {
args: {
aanvraag: {
...base,
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false },
},
},
};
export const InBehandelingManual: Story = {
args: {
aanvraag: {
...base,
type: 'registratie',
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: true },
},
},
};
export const Goedgekeurd: Story = {
args: { aanvraag: { ...base, status: { tag: 'Goedgekeurd', referentie: 'BIG-2026-456789' } } },
};
export const Afgewezen: Story = {
args: {
aanvraag: {
...base,
type: 'herregistratie',
status: {
tag: 'Afgewezen',
referentie: 'BIG-2026-456789',
reden: 'Aanvraag afgewezen: geen gewerkte uren geregistreerd.',
},
},
},
};

View File

@@ -15,14 +15,28 @@ import { detailRows } from '@registratie/domain/aanvraag-view';
handling is future work. The dashboard "Mijn aanvragen" rows link here. */
@Component({
selector: 'app-aanvraag-detail-page',
imports: [PageShellComponent, SkeletonComponent, AlertComponent, DataBlockComponent, DataRowComponent, ...ASYNC],
imports: [
PageShellComponent,
SkeletonComponent,
AlertComponent,
DataBlockComponent,
DataRowComponent,
...ASYNC,
],
template: `
<app-page-shell i18n-heading="@@aanvraagDetail.heading" heading="Aanvraag" backLink="/dashboard">
<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">
<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>
}
@@ -31,7 +45,9 @@ import { detailRows } from '@registratie/domain/aanvraag-view';
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>
<app-alert type="warning" i18n="@@aanvraagDetail.nietGevonden"
>Deze aanvraag is niet gevonden.</app-alert
>
}
</ng-template>
<ng-template appAsyncLoading>

View File

@@ -19,27 +19,73 @@ export type AdresErrors = Partial<Record<keyof AdresValue, string>>;
@Component({
selector: 'app-address-fields',
imports: [FormsModule, FormFieldComponent, TextInputComponent],
styles: [`
fieldset{border:0;margin:0;padding:0;min-inline-size:0}
legend{padding:0;font-weight:var(--rhc-text-font-weight-semi-bold);margin-block-end:var(--rhc-space-max-md)}
`],
styles: [
`
fieldset {
border: 0;
margin: 0;
padding: 0;
min-inline-size: 0;
}
legend {
padding: 0;
font-weight: var(--rhc-text-font-weight-semi-bold);
margin-block-end: var(--rhc-space-max-md);
}
`,
],
template: `
<fieldset>
<legend>{{ legend() }}</legend>
<app-form-field i18n-label="@@address.straat" label="Straat en huisnummer" [fieldId]="idPrefix() + '-straat'" required [error]="errors().straat">
<app-text-input [inputId]="idPrefix() + '-straat'" [invalid]="!!errors().straat"
[ngModel]="value().straat" (ngModelChange)="fieldChange.emit({ key: 'straat', value: $event })"
name="straat" [ngModelOptions]="{ standalone: true }" />
<app-form-field
i18n-label="@@address.straat"
label="Straat en huisnummer"
[fieldId]="idPrefix() + '-straat'"
required
[error]="errors().straat"
>
<app-text-input
[inputId]="idPrefix() + '-straat'"
[invalid]="!!errors().straat"
[ngModel]="value().straat"
(ngModelChange)="fieldChange.emit({ key: 'straat', value: $event })"
name="straat"
[ngModelOptions]="{ standalone: true }"
/>
</app-form-field>
<app-form-field i18n-label="@@address.postcode" label="Postcode" [fieldId]="idPrefix() + '-postcode'" required [error]="errors().postcode">
<app-text-input [inputId]="idPrefix() + '-postcode'" [invalid]="!!errors().postcode"
[ngModel]="value().postcode" (ngModelChange)="fieldChange.emit({ key: 'postcode', value: $event })"
name="postcode" i18n-placeholder="@@address.postcodePlaceholder" placeholder="1234 AB" [ngModelOptions]="{ standalone: true }" />
<app-form-field
i18n-label="@@address.postcode"
label="Postcode"
[fieldId]="idPrefix() + '-postcode'"
required
[error]="errors().postcode"
>
<app-text-input
[inputId]="idPrefix() + '-postcode'"
[invalid]="!!errors().postcode"
[ngModel]="value().postcode"
(ngModelChange)="fieldChange.emit({ key: 'postcode', value: $event })"
name="postcode"
i18n-placeholder="@@address.postcodePlaceholder"
placeholder="1234 AB"
[ngModelOptions]="{ standalone: true }"
/>
</app-form-field>
<app-form-field i18n-label="@@address.woonplaats" label="Woonplaats" [fieldId]="idPrefix() + '-woonplaats'" required [error]="errors().woonplaats">
<app-text-input [inputId]="idPrefix() + '-woonplaats'" [invalid]="!!errors().woonplaats"
[ngModel]="value().woonplaats" (ngModelChange)="fieldChange.emit({ key: 'woonplaats', value: $event })"
name="woonplaats" [ngModelOptions]="{ standalone: true }" />
<app-form-field
i18n-label="@@address.woonplaats"
label="Woonplaats"
[fieldId]="idPrefix() + '-woonplaats'"
required
[error]="errors().woonplaats"
>
<app-text-input
[inputId]="idPrefix() + '-woonplaats'"
[invalid]="!!errors().woonplaats"
[ngModel]="value().woonplaats"
(ngModelChange)="fieldChange.emit({ key: 'woonplaats', value: $event })"
name="woonplaats"
[ngModelOptions]="{ standalone: true }"
/>
</app-form-field>
</fieldset>
`,

View File

@@ -20,12 +20,18 @@ const empty = { straat: '', postcode: '', woonplaats: '' };
export const Default: Story = { args: { value: empty, errors: {} } };
export const Prefilled: Story = {
args: { value: { straat: 'Stationsplein 1', postcode: '3511 ED', woonplaats: 'Utrecht' }, errors: {} },
args: {
value: { straat: 'Stationsplein 1', postcode: '3511 ED', woonplaats: 'Utrecht' },
errors: {},
},
};
export const WithErrors: Story = {
args: {
value: { straat: '', postcode: '12', woonplaats: '' },
errors: { straat: 'Vul een straat en huisnummer in.', postcode: 'Voer een geldige postcode in, bijv. 1234 AB.' },
errors: {
straat: 'Vul een straat en huisnummer in.',
postcode: 'Voer een geldige postcode in, bijv. 1234 AB.',
},
},
};

View File

@@ -3,7 +3,11 @@ import { FormsModule } from '@angular/forms';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { HeadingComponent } from '@shared/ui/heading/heading.component';
import { AlertComponent } from '@shared/ui/alert/alert.component';
import { AddressFieldsComponent, AdresValue, AdresErrors } from '@registratie/ui/address-fields/address-fields.component';
import {
AddressFieldsComponent,
AdresValue,
AdresErrors,
} from '@registratie/ui/address-fields/address-fields.component';
import { createStore } from '@shared/application/store';
import { whenTag } from '@shared/kernel/fp';
import { State, Msg, initial, reduce } from '@registratie/domain/change-request.machine';
@@ -21,25 +25,37 @@ import { createSubmitChangeRequest } from '@registratie/application/submit-chang
template: `
@if (state().tag === 'Submitted') {
<app-alert type="ok" i18n="@@changeRequest.success">
Uw adreswijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5 werkdagen bericht.
Uw adreswijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5
werkdagen bericht.
</app-alert>
<div class="app-section">
<app-button variant="secondary" (click)="dispatch({ tag: 'Reset' })" i18n="@@changeRequest.nieuwe">Nieuwe wijziging doorgeven</app-button>
<app-button
variant="secondary"
(click)="dispatch({ tag: 'Reset' })"
i18n="@@changeRequest.nieuwe"
>Nieuwe wijziging doorgeven</app-button
>
</div>
} @else {
<app-heading [level]="2" i18n="@@changeRequest.heading">Adreswijziging doorgeven</app-heading>
<form (ngSubmit)="onSubmit()" class="form-horizontal app-section">
<div class="form-header">
<div class="form-action"><span class="meta" i18n="@@form.verplichteVelden">* verplichte velden</span></div>
<div class="form-action">
<span class="meta" i18n="@@form.verplichteVelden">* verplichte velden</span>
</div>
</div>
<app-address-fields
idPrefix="cr"
[value]="adres()"
[errors]="errors()"
(fieldChange)="dispatch({ tag: 'SetField', key: $event.key, value: $event.value })" />
(fieldChange)="dispatch({ tag: 'SetField', key: $event.key, value: $event.value })"
/>
@if (failedError()) {
<app-alert type="error"><ng-container i18n="@@changeRequest.failed">Het indienen is niet gelukt:</ng-container> {{ failedError() }}</app-alert>
<app-alert type="error"
><ng-container i18n="@@changeRequest.failed">Het indienen is niet gelukt:</ng-container>
{{ failedError() }}</app-alert
>
}
<app-button type="submit" variant="primary" [disabled]="state().tag === 'Submitting'">

View File

@@ -5,7 +5,11 @@ import { ChangeRequestFormComponent } from './change-request-form.component';
import { provideApiClient } from '@shared/infrastructure/api-client.provider';
import { Postcode } from '@registratie/domain/value-objects/postcode';
const validData = { straat: 'Lange Voorhout 9', postcode: '2514 EA' as Postcode, woonplaats: 'Den Haag' };
const validData = {
straat: 'Lange Voorhout 9',
postcode: '2514 EA' as Postcode,
woonplaats: 'Den Haag',
};
const meta: Meta<ChangeRequestFormComponent> = {
title: 'Organisms/Change Request Form',
@@ -17,16 +21,27 @@ export default meta;
type Story = StoryObj<ChangeRequestFormComponent>;
// One render per state of the machine.
export const Empty: Story = { args: { seed: { tag: 'Editing', draft: { straat: '', postcode: '', woonplaats: '' }, errors: {} } } };
export const Empty: Story = {
args: {
seed: { tag: 'Editing', draft: { straat: '', postcode: '', woonplaats: '' }, errors: {} },
},
};
export const WithErrors: Story = {
args: {
seed: {
tag: 'Editing',
draft: { straat: '', postcode: 'nope', woonplaats: '' },
errors: { straat: 'Vul straat en huisnummer in.', postcode: 'Voer een geldige postcode in, bijv. 1234 AB.' },
errors: {
straat: 'Vul straat en huisnummer in.',
postcode: 'Voer een geldige postcode in, bijv. 1234 AB.',
},
},
},
};
export const Submitting: Story = { args: { seed: { tag: 'Submitting', data: validData } } };
export const Submitted: Story = { args: { seed: { tag: 'Submitted', data: validData, referentie: 'BIG-2026-123456' } } };
export const Failed: Story = { args: { seed: { tag: 'Failed', data: validData, error: 'Netwerkfout' } } };
export const Submitted: Story = {
args: { seed: { tag: 'Submitted', data: validData, referentie: 'BIG-2026-123456' } },
};
export const Failed: Story = {
args: { seed: { tag: 'Failed', data: validData, error: 'Netwerkfout' } },
};

View File

@@ -25,91 +25,163 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
@Component({
selector: 'app-dashboard-page',
imports: [
PageShellComponent, HeadingComponent, AlertComponent, SkeletonComponent,
DataRowComponent, DataBlockComponent, TaskListComponent, ApplicationListComponent, ApplicationLinkComponent,
PageShellComponent,
HeadingComponent,
AlertComponent,
SkeletonComponent,
DataRowComponent,
DataBlockComponent,
TaskListComponent,
ApplicationListComponent,
ApplicationLinkComponent,
...ASYNC,
RegistrationSummaryComponent, RegistrationTableComponent, AanvraagBlockComponent,
RegistrationSummaryComponent,
RegistrationTableComponent,
AanvraagBlockComponent,
],
template: `
<app-page-shell i18n-heading="@@dashboard.heading" heading="Mijn overzicht" i18n-intro="@@dashboard.intro" intro="Welkom in uw persoonlijke omgeving van het BIG-register. Hier ziet u uw registratie en regelt u uw zaken.">
<div class="app-stack">
@if (aanvragen().length) {
<app-page-shell
i18n-heading="@@dashboard.heading"
heading="Mijn overzicht"
i18n-intro="@@dashboard.intro"
intro="Welkom in uw persoonlijke omgeving van het BIG-register. Hier ziet u uw registratie en regelt u uw zaken."
>
<div class="app-stack">
@if (aanvragen().length) {
<section>
@for (a of concepten(); track a.id) {
<app-aanvraag-block
animate.enter="app-item-enter"
animate.leave="app-item-leave"
[aanvraag]="a"
(resume)="resume(a)"
(cancel)="cancelAanvraag(a)"
/>
}
@if (ingediend().length) {
<app-heading [level]="2" class="app-section" i18n="@@dashboard.mijnAanvragen"
>Mijn aanvragen</app-heading
>
<app-application-list>
@for (a of ingediend(); track a.id) {
@let row = submittedRow(a);
<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>
}
</section>
}
@if (store.pendingHerregistratie()) {
<app-alert type="info" i18n="@@dashboard.pendingHerregistratie"
>Uw herregistratie-aanvraag is in behandeling.</app-alert
>
}
<app-async [data]="store.profile()">
<ng-template appAsyncLoaded let-p>
@let tasks = tasksFor($any(p).registration);
<section>
@for (a of concepten(); track a.id) {
<app-aanvraag-block animate.enter="app-item-enter" animate.leave="app-item-leave" [aanvraag]="a" (resume)="resume(a)" (cancel)="cancelAanvraag(a)" />
}
@if (ingediend().length) {
<app-heading [level]="2" class="app-section" i18n="@@dashboard.mijnAanvragen">Mijn aanvragen</app-heading>
<app-application-list>
@for (a of ingediend(); track a.id) {
@let row = submittedRow(a);
<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>
@if (tasks.length) {
<app-task-list
class="app-section"
i18n-listHeading="@@dashboard.watMoetIkRegelen"
listHeading="Wat moet ik regelen"
[tasks]="tasks"
/>
} @else {
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen"
>Wat moet ik regelen</app-heading
>
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">
U heeft op dit moment niets openstaan.
</p>
}
</section>
}
@if (store.pendingHerregistratie()) {
<app-alert type="info" i18n="@@dashboard.pendingHerregistratie">Uw herregistratie-aanvraag is in behandeling.</app-alert>
}
<section>
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie"
>Mijn registratie</app-heading
>
<div class="app-section">
<app-registration-summary [reg]="$any(p).registration" />
</div>
<app-data-block
class="app-section"
i18n-heading="@@dashboard.persoonsgegevens"
heading="Persoonsgegevens (BRP)"
>
<div
app-data-row
i18n-key="@@dashboard.straat"
key="Straat"
[value]="$any(p).person.adres.straat"
></div>
<div
app-data-row
i18n-key="@@dashboard.postcode"
key="Postcode"
[value]="$any(p).person.adres.postcode"
></div>
<div
app-data-row
i18n-key="@@dashboard.woonplaats"
key="Woonplaats"
[value]="$any(p).person.adres.woonplaats"
></div>
</app-data-block>
</section>
</ng-template>
<ng-template appAsyncLoading>
<app-skeleton height="2.5rem" [count]="6" />
</ng-template>
</app-async>
<app-async [data]="store.profile()">
<ng-template appAsyncLoaded let-p>
@let tasks = tasksFor($any(p).registration);
<section>
<app-heading [level]="2" i18n="@@dashboard.specialismen"
>Specialismen en aantekeningen</app-heading
>
<div class="app-section">
<app-async [data]="store.aantekeningen()">
<ng-template appAsyncLoaded let-r>
<app-registration-table [rows]="$any(r)" />
</ng-template>
<ng-template appAsyncLoading>
<app-skeleton height="2.5rem" [count]="3" />
</ng-template>
<ng-template appAsyncEmpty>
<p class="app-text-subtle" i18n="@@dashboard.geenSpecialismen">
U heeft nog geen specialismen of aantekeningen.
</p>
</ng-template>
</app-async>
</div>
</section>
<section>
@if (tasks.length) {
<app-task-list class="app-section" i18n-listHeading="@@dashboard.watMoetIkRegelen" listHeading="Wat moet ik regelen" [tasks]="tasks" />
} @else {
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen">Wat moet ik regelen</app-heading>
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">U heeft op dit moment niets openstaan.</p>
}
</section>
<section>
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie">Mijn registratie</app-heading>
<div class="app-section">
<app-registration-summary [reg]="$any(p).registration" />
</div>
<app-data-block class="app-section" i18n-heading="@@dashboard.persoonsgegevens" heading="Persoonsgegevens (BRP)">
<div app-data-row i18n-key="@@dashboard.straat" key="Straat" [value]="$any(p).person.adres.straat"></div>
<div app-data-row i18n-key="@@dashboard.postcode" key="Postcode" [value]="$any(p).person.adres.postcode"></div>
<div app-data-row i18n-key="@@dashboard.woonplaats" key="Woonplaats" [value]="$any(p).person.adres.woonplaats"></div>
</app-data-block>
</section>
</ng-template>
<ng-template appAsyncLoading>
<app-skeleton height="2.5rem" [count]="6" />
</ng-template>
</app-async>
<section>
<app-heading [level]="2" i18n="@@dashboard.specialismen">Specialismen en aantekeningen</app-heading>
<div class="app-section">
<app-async [data]="store.aantekeningen()">
<ng-template appAsyncLoaded let-r>
<app-registration-table [rows]="$any(r)" />
</ng-template>
<ng-template appAsyncLoading>
<app-skeleton height="2.5rem" [count]="3" />
</ng-template>
<ng-template appAsyncEmpty>
<p class="app-text-subtle" i18n="@@dashboard.geenSpecialismen">U heeft nog geen specialismen of aantekeningen.</p>
</ng-template>
</app-async>
</div>
</section>
<section>
<app-heading [level]="2" i18n="@@dashboard.watWiltUDoen">Wat wilt u doen?</app-heading>
<app-application-list class="app-section">
@for (a of acties; track a.to) {
<li app-application-link [heading]="a.titel" [subtitle]="a.tekst" [cta]="a.actie" [to]="a.to"></li>
}
</app-application-list>
</section>
</div>
<section>
<app-heading [level]="2" i18n="@@dashboard.watWiltUDoen">Wat wilt u doen?</app-heading>
<app-application-list class="app-section">
@for (a of acties; track a.to) {
<li
app-application-link
[heading]="a.titel"
[subtitle]="a.tekst"
[cta]="a.actie"
[to]="a.to"
></li>
}
</app-application-list>
</section>
</div>
</app-page-shell>
`,
})
@@ -132,7 +204,12 @@ export class DashboardPage {
protected aanvragen = computed<Aanvraag[]>(() => {
const rd = this.apps.applications();
if (rd.tag !== 'Success') return [];
const order: Record<Aanvraag['status']['tag'], number> = { Concept: 0, InBehandeling: 1, Goedgekeurd: 2, Afgewezen: 2 };
const order: Record<Aanvraag['status']['tag'], number> = {
Concept: 0,
InBehandeling: 1,
Goedgekeurd: 2,
Afgewezen: 2,
};
return rd.value.slice().sort((a, b) => order[a.status.tag] - order[b.status.tag]);
});
/** A Concept ("lopende aanvraag") renders as a melding above the list; the rest
@@ -166,11 +243,41 @@ export class DashboardPage {
componenten/aanvragen). The core portal sections live in the header nav now;
the teaching pages (concepts/brief) are only reachable from here. */
protected readonly acties = [
{ to: '/registreren', titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`, tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`, actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving` },
{ to: '/herregistratie', titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`, tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`, actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan` },
{ to: '/intake', titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`, tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`, actie: $localize`:@@dashboard.actie.intake.actie:Start intake` },
{ to: '/registratie', titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`, tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`, actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens` },
{ to: '/concepts', titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`, tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`, actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen` },
{ to: '/brief', titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`, tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`, actie: $localize`:@@dashboard.actie.brief.actie:Start brief` },
{
to: '/registreren',
titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`,
tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`,
actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving`,
},
{
to: '/herregistratie',
titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`,
tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`,
actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan`,
},
{
to: '/intake',
titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`,
tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`,
actie: $localize`:@@dashboard.actie.intake.actie:Start intake`,
},
{
to: '/registratie',
titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`,
tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`,
actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens`,
},
{
to: '/concepts',
titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`,
tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`,
actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen`,
},
{
to: '/brief',
titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`,
tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`,
actie: $localize`:@@dashboard.actie.brief.actie:Start brief`,
},
];
}

View File

@@ -9,7 +9,12 @@ import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
import { WizardShellComponent, WizardError, WizardStatus, naarStapLabel } from '@shared/layout/wizard-shell/wizard-shell.component';
import {
WizardShellComponent,
WizardError,
WizardStatus,
naarStapLabel,
} from '@shared/layout/wizard-shell/wizard-shell.component';
import { ASYNC } from '@shared/ui/async/async.component';
import { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component';
import { createStore } from '@shared/application/store';
@@ -54,138 +59,291 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
@Component({
selector: 'app-registratie-wizard',
imports: [
FormsModule, FormFieldComponent, TextInputComponent, RadioGroupComponent, ButtonComponent,
AlertComponent, SkeletonComponent, DataRowComponent, ReviewSectionComponent, ConfirmationComponent, WizardShellComponent,
AddressFieldsComponent, DocumentUploadComponent, ...ASYNC,
FormsModule,
FormFieldComponent,
TextInputComponent,
RadioGroupComponent,
ButtonComponent,
AlertComponent,
SkeletonComponent,
DataRowComponent,
ReviewSectionComponent,
ConfirmationComponent,
WizardShellComponent,
AddressFieldsComponent,
DocumentUploadComponent,
...ASYNC,
],
template: `
<app-wizard-shell
[steps]="stepLabels"
[current]="cursor()"
[stepTitle]="stepTitle()"
i18n-processName="@@regWizard.processName" processName="Inschrijven in het BIG-register"
i18n-processName="@@regWizard.processName"
processName="Inschrijven in het BIG-register"
[status]="shellStatus()"
[primaryLabel]="primaryLabel()"
[canGoBack]="cursor() > 0"
[errors]="errorList()"
[errorMessage]="errorMessage()"
i18n-submittingLabel="@@regWizard.submitting" submittingLabel="Uw registratie wordt verwerkt…"
i18n-submittingLabel="@@regWizard.submitting"
submittingLabel="Uw registratie wordt verwerkt…"
(primary)="onPrimary()"
(back)="dispatch({ tag: 'Back' })"
(cancel)="restart()"
(retry)="onRetry()"
(goToStep)="dispatch({ tag: 'GaNaarStap', cursor: $event })">
(goToStep)="dispatch({ tag: 'GaNaarStap', cursor: $event })"
>
@switch (step()) {
@case ('adres') {
@if (adresStatus() === 'laden') {
<app-skeleton height="2.5rem" [count]="4" />
} @else {
@switch (adresStatus()) {
@case ('gevonden') {
<app-alert type="info" i18n="@@regWizard.brpGevonden">Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert>
}
@case ('geen') {
<app-alert type="warning" i18n="@@regWizard.brpGeen">We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert>
}
@case ('fout') {
<app-alert type="warning" i18n="@@regWizard.brpFout">We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.</app-alert>
}
}
<app-address-fields
[value]="{ straat: draft().straat ?? '', postcode: draft().postcode ?? '', woonplaats: draft().woonplaats ?? '' }"
[errors]="{ straat: err('straat'), postcode: err('postcode'), woonplaats: err('woonplaats') }"
(fieldChange)="set($event.key, $event.value)" />
<app-form-field i18n-label="@@regWizard.correspondentieLabel" label="Hoe wilt u correspondentie ontvangen?" fieldId="correspondentie" required [error]="err('correspondentie')">
<app-radio-group name="correspondentie" [options]="kanalen" [invalid]="!!err('correspondentie')"
[ngModel]="draft().correspondentie ?? ''" (ngModelChange)="setKanaal($event)" />
@if (adresStatus() === 'laden') {
<app-skeleton height="2.5rem" [count]="4" />
} @else {
@switch (adresStatus()) {
@case ('gevonden') {
<app-alert type="info" i18n="@@regWizard.brpGevonden"
>Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert
>
}
@case ('geen') {
<app-alert type="warning" i18n="@@regWizard.brpGeen"
>We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert
>
}
@case ('fout') {
<app-alert type="warning" i18n="@@regWizard.brpFout"
>We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig
in.</app-alert
>
}
}
<app-address-fields
[value]="{
straat: draft().straat ?? '',
postcode: draft().postcode ?? '',
woonplaats: draft().woonplaats ?? '',
}"
[errors]="{
straat: err('straat'),
postcode: err('postcode'),
woonplaats: err('woonplaats'),
}"
(fieldChange)="set($event.key, $event.value)"
/>
<app-form-field
i18n-label="@@regWizard.correspondentieLabel"
label="Hoe wilt u correspondentie ontvangen?"
fieldId="correspondentie"
required
[error]="err('correspondentie')"
>
<app-radio-group
name="correspondentie"
[options]="kanalen"
[invalid]="!!err('correspondentie')"
[ngModel]="draft().correspondentie ?? ''"
(ngModelChange)="setKanaal($event)"
/>
</app-form-field>
@if (draft().correspondentie === 'email') {
<app-form-field
i18n-label="@@regWizard.emailLabel"
label="E-mailadres"
fieldId="email"
required
[error]="err('email')"
>
<app-text-input
inputId="email"
type="email"
[invalid]="!!err('email')"
[ngModel]="draft().email ?? ''"
(ngModelChange)="set('email', $event)"
name="email"
i18n-placeholder="@@regWizard.emailPlaceholder"
placeholder="naam@voorbeeld.nl"
/>
</app-form-field>
}
}
}
@case ('beroep') {
<app-async [data]="lookupRd()">
<ng-template appAsyncLoaded let-data>
<app-form-field
i18n-label="@@regWizard.diplomaLabel"
label="Kies het diploma waarmee u zich wilt registreren"
fieldId="diploma"
required
[error]="err('diploma')"
>
<app-radio-group
name="diploma"
[options]="diplomaOptions($any(data))"
[invalid]="!!err('diploma')"
[ngModel]="diplomaKeuze()"
(ngModelChange)="onDiplomaKeuze($any(data), $event)"
/>
</app-form-field>
@if (handmatigActief()) {
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing"
>Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw
beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig
beoordeeld.</app-alert
>
<app-form-field
i18n-label="@@regWizard.beroepLabel"
label="Voor welk beroep wilt u zich registreren?"
fieldId="hm-beroep"
[error]="err('diploma')"
>
<app-radio-group
name="hm-beroep"
[options]="beroepOptions($any(data))"
[invalid]="!!err('diploma')"
[ngModel]="draft().beroep ?? ''"
(ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })"
/>
</app-form-field>
@if (draft().correspondentie === 'email') {
<app-form-field i18n-label="@@regWizard.emailLabel" label="E-mailadres" fieldId="email" required [error]="err('email')">
<app-text-input inputId="email" type="email" [invalid]="!!err('email')" [ngModel]="draft().email ?? ''" (ngModelChange)="set('email', $event)" name="email" i18n-placeholder="@@regWizard.emailPlaceholder" placeholder="naam@voorbeeld.nl" />
</app-form-field>
}
} @else if (draft().beroep) {
<dl class="mb-0 app-section">
<div
app-data-row
i18n-key="@@regWizard.beroepAfgeleid"
key="Beroep (afgeleid uit diploma)"
[value]="draft().beroep ?? ''"
></div>
</dl>
}
}
@case ('beroep') {
<app-async [data]="lookupRd()">
<ng-template appAsyncLoaded let-data>
<app-form-field i18n-label="@@regWizard.diplomaLabel" label="Kies het diploma waarmee u zich wilt registreren" fieldId="diploma" required [error]="err('diploma')">
<app-radio-group name="diploma" [options]="diplomaOptions($any(data))" [invalid]="!!err('diploma')"
[ngModel]="diplomaKeuze()" (ngModelChange)="onDiplomaKeuze($any(data), $event)" />
</app-form-field>
@if (handmatigActief()) {
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing">Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig beoordeeld.</app-alert>
<app-form-field i18n-label="@@regWizard.beroepLabel" label="Voor welk beroep wilt u zich registreren?" fieldId="hm-beroep" [error]="err('diploma')">
<app-radio-group name="hm-beroep" [options]="beroepOptions($any(data))" [invalid]="!!err('diploma')"
[ngModel]="draft().beroep ?? ''" (ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })" />
</app-form-field>
} @else if (draft().beroep) {
<dl class="mb-0 app-section">
<div app-data-row i18n-key="@@regWizard.beroepAfgeleid" key="Beroep (afgeleid uit diploma)" [value]="draft().beroep ?? ''"></div>
</dl>
@for (q of actieveVragen($any(data)); track q.id) {
<app-form-field
[label]="q.vraag"
[fieldId]="'vraag-' + q.id"
[error]="vraagErr(q.id)"
>
@if (q.type === 'ja-nee') {
<app-radio-group
[name]="'vraag-' + q.id"
[options]="jaNee"
[invalid]="!!vraagErr(q.id)"
[ngModel]="antwoord(q.id)"
(ngModelChange)="
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
"
[ngModelOptions]="{ standalone: true }"
/>
} @else {
<app-text-input
[inputId]="'vraag-' + q.id"
[invalid]="!!vraagErr(q.id)"
[ngModel]="antwoord(q.id)"
(ngModelChange)="
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
"
[ngModelOptions]="{ standalone: true }"
/>
}
@for (q of actieveVragen($any(data)); track q.id) {
<app-form-field [label]="q.vraag" [fieldId]="'vraag-' + q.id" [error]="vraagErr(q.id)">
@if (q.type === 'ja-nee') {
<app-radio-group [name]="'vraag-' + q.id" [options]="jaNee" [invalid]="!!vraagErr(q.id)"
[ngModel]="antwoord(q.id)" (ngModelChange)="dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })" [ngModelOptions]="{ standalone: true }" />
} @else {
<app-text-input [inputId]="'vraag-' + q.id" [invalid]="!!vraagErr(q.id)" [ngModel]="antwoord(q.id)"
(ngModelChange)="dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })" [ngModelOptions]="{ standalone: true }" />
}
</app-form-field>
}
</ng-template>
<ng-template appAsyncLoading>
<app-skeleton height="2.5rem" [count]="3" />
</ng-template>
</app-async>
<app-document-upload
class="app-section"
[state]="upload()"
[previewUrlFor]="previewUrlFor"
(fileSelected)="uploadCtl.onFileSelected($event.categoryId, $event.files)"
(removeUpload)="uploadCtl.onRemove($event)"
(retryUpload)="uploadCtl.onRetry($event)"
(deleteUpload)="uploadCtl.onDelete($event)"
(channelChange)="uploadCtl.onChannelChange($event.categoryId, $event.channel)" />
@if (err('documenten')) {
<app-alert type="warning">{{ err('documenten') }}</app-alert>
</app-form-field>
}
</ng-template>
<ng-template appAsyncLoading>
<app-skeleton height="2.5rem" [count]="3" />
</ng-template>
</app-async>
<app-document-upload
class="app-section"
[state]="upload()"
[previewUrlFor]="previewUrlFor"
(fileSelected)="uploadCtl.onFileSelected($event.categoryId, $event.files)"
(removeUpload)="uploadCtl.onRemove($event)"
(retryUpload)="uploadCtl.onRetry($event)"
(deleteUpload)="uploadCtl.onDelete($event)"
(channelChange)="uploadCtl.onChannelChange($event.categoryId, $event.channel)"
/>
@if (err('documenten')) {
<app-alert type="warning">{{ err('documenten') }}</app-alert>
}
}
@case ('controle') {
<app-alert type="info" i18n="@@regWizard.controleer"
>Controleer uw gegevens en dien de registratie in.</app-alert
>
<app-review-section
i18n-heading="@@regWizard.sectie.adres"
heading="Adres en correspondentie"
i18n-editAriaLabel="@@regWizard.adresWijzigenAria"
editAriaLabel="Wijzigen adresgegevens"
(edit)="dispatch({ tag: 'GaNaarStap', cursor: 0 })"
>
<div
app-data-row
i18n-key="@@regWizard.summary.adres"
key="Adres"
[value]="adresSamenvatting()"
></div>
<div
app-data-row
i18n-key="@@regWizard.summary.herkomstAdres"
key="Herkomst adres"
[value]="adresHerkomstLabel()"
></div>
<div
app-data-row
i18n-key="@@regWizard.summary.correspondentie"
key="Correspondentie"
[value]="correspondentieLabel()"
></div>
@if (draft().correspondentie === 'email') {
<div
app-data-row
i18n-key="@@regWizard.summary.email"
key="E-mailadres"
[value]="draft().email ?? ''"
></div>
}
@case ('controle') {
<app-alert type="info" i18n="@@regWizard.controleer">Controleer uw gegevens en dien de registratie in.</app-alert>
<app-review-section i18n-heading="@@regWizard.sectie.adres" heading="Adres en correspondentie"
i18n-editAriaLabel="@@regWizard.adresWijzigenAria" editAriaLabel="Wijzigen adresgegevens"
(edit)="dispatch({ tag: 'GaNaarStap', cursor: 0 })">
<div app-data-row i18n-key="@@regWizard.summary.adres" key="Adres" [value]="adresSamenvatting()"></div>
<div app-data-row i18n-key="@@regWizard.summary.herkomstAdres" key="Herkomst adres" [value]="adresHerkomstLabel()"></div>
<div app-data-row i18n-key="@@regWizard.summary.correspondentie" key="Correspondentie" [value]="correspondentieLabel()"></div>
@if (draft().correspondentie === 'email') {
<div app-data-row i18n-key="@@regWizard.summary.email" key="E-mailadres" [value]="draft().email ?? ''"></div>
}
</app-review-section>
<app-review-section class="app-section" i18n-heading="@@regWizard.sectie.beroep" heading="Beroep en diploma"
i18n-editAriaLabel="@@regWizard.diplomaWijzigenAria" editAriaLabel="Wijzigen beroep en diploma"
(edit)="dispatch({ tag: 'GaNaarStap', cursor: 1 })">
<div app-data-row i18n-key="@@regWizard.summary.beroep" key="Beroep" [value]="draft().beroep ?? ''"></div>
<div app-data-row i18n-key="@@regWizard.summary.herkomstDiploma" key="Herkomst diploma" [value]="diplomaHerkomstLabel()"></div>
@for (item of samenvattingVragen(); track item.vraag) {
<div app-data-row [key]="item.vraag" [value]="item.antwoord"></div>
}
</app-review-section>
</app-review-section>
<app-review-section
class="app-section"
i18n-heading="@@regWizard.sectie.beroep"
heading="Beroep en diploma"
i18n-editAriaLabel="@@regWizard.diplomaWijzigenAria"
editAriaLabel="Wijzigen beroep en diploma"
(edit)="dispatch({ tag: 'GaNaarStap', cursor: 1 })"
>
<div
app-data-row
i18n-key="@@regWizard.summary.beroep"
key="Beroep"
[value]="draft().beroep ?? ''"
></div>
<div
app-data-row
i18n-key="@@regWizard.summary.herkomstDiploma"
key="Herkomst diploma"
[value]="diplomaHerkomstLabel()"
></div>
@for (item of samenvattingVragen(); track item.vraag) {
<div app-data-row [key]="item.vraag" [value]="item.antwoord"></div>
}
</app-review-section>
}
}
<div wizardSuccess>
<app-confirmation i18n-title="@@regWizard.success.title" title="Uw registratie is ontvangen">
<p class="app-section" i18n="@@regWizard.success.referentie">Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.</p>
<app-confirmation
i18n-title="@@regWizard.success.title"
title="Uw registratie is ontvangen"
>
<p class="app-section" i18n="@@regWizard.success.referentie">
Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.
</p>
<div class="app-section">
<app-button variant="secondary" (click)="restart()" i18n="@@regWizard.nieuweRegistratie">Nieuwe registratie starten</app-button>
<app-button variant="secondary" (click)="restart()" i18n="@@regWizard.nieuweRegistratie"
>Nieuwe registratie starten</app-button
>
</div>
</app-confirmation>
</div>
@@ -206,8 +364,16 @@ export class RegistratieWizardComponent {
seed = input<RegistratieState>(initial);
readonly kanalen = KANALEN;
readonly stepLabels = [$localize`:@@regWizard.step.adres:Adres`, $localize`:@@regWizard.step.beroep:Beroep`, $localize`:@@regWizard.step.controle:Controle`]; // short labels for the stepper
private stepTitles = [$localize`:@@regWizard.title.adres:Adres en correspondentievoorkeur`, $localize`:@@regWizard.title.beroep:Beroep op basis van uw diploma`, $localize`:@@regWizard.title.controle:Controleren en indienen`];
readonly stepLabels = [
$localize`:@@regWizard.step.adres:Adres`,
$localize`:@@regWizard.step.beroep:Beroep`,
$localize`:@@regWizard.step.controle:Controle`,
]; // short labels for the stepper
private stepTitles = [
$localize`:@@regWizard.title.adres:Adres en correspondentievoorkeur`,
$localize`:@@regWizard.title.beroep:Beroep op basis van uw diploma`,
$localize`:@@regWizard.title.controle:Controleren en indienen`,
];
readonly state = this.store.model;
readonly dispatch = this.store.dispatch;
@@ -234,14 +400,18 @@ export class RegistratieWizardComponent {
snapshot: () => {
const s = this.state();
if (s.tag !== 'Invullen' || !hasProgress(s)) return null;
const documentIds = deliveryRefs(s.upload).filter((r) => r.channel === 'digital' && r.documentId).map((r) => r.documentId!);
const documentIds = deliveryRefs(s.upload)
.filter((r) => r.channel === 'digital' && r.documentId)
.map((r) => r.documentId!);
return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds };
},
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as RegistratieState }),
enabled: () => this.seed() === initial,
});
protected step = computed<StepId>(() => STEPS[Math.min(this.cursor(), STEPS.length - 1)]);
protected stepTitle = computed(() => this.stepTitles[Math.min(this.cursor(), this.stepTitles.length - 1)]);
protected stepTitle = computed(
() => this.stepTitles[Math.min(this.cursor(), this.stepTitles.length - 1)],
);
protected referentie = computed(() => whenTag(this.state(), 'Ingediend')?.referentie ?? '');
protected failedError = computed(() => whenTag(this.state(), 'Mislukt')?.error ?? '');
@@ -251,13 +421,21 @@ export class RegistratieWizardComponent {
const next = this.cursor() + 1;
return naarStapLabel(next + 1, this.stepLabels[next]);
});
protected errorMessage = computed(() => $localize`:@@regWizard.indienenMislukt:Het indienen is niet gelukt:` + ` ${this.failedError()}`);
protected errorMessage = computed(
() =>
$localize`:@@regWizard.indienenMislukt:Het indienen is niet gelukt:` +
` ${this.failedError()}`,
);
protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) {
case 'Invullen': return 'editing';
case 'Indienen': return 'submitting';
case 'Ingediend': return 'submitted';
case 'Mislukt': return 'failed';
case 'Invullen':
return 'editing';
case 'Indienen':
return 'submitting';
case 'Ingediend':
return 'submitted';
case 'Mislukt':
return 'failed';
}
});
/** Current step's errors (incl. per-question), flattened for the error summary. */
@@ -274,12 +452,32 @@ export class RegistratieWizardComponent {
});
protected adresSamenvatting = computed(() => {
const d = this.draft();
return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')].filter(Boolean).join(', ');
return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')]
.filter(Boolean)
.join(', ');
});
// Readable labels for the controle summary (instead of raw enum values).
protected adresHerkomstLabel = computed(() => ({ brp: $localize`:@@regWizard.herkomst.adresBrp:Automatisch uit de BRP`, handmatig: $localize`:@@regWizard.herkomst.adresHandmatig:Handmatig ingevoerd` }[this.draft().adresHerkomst ?? 'handmatig']));
protected correspondentieLabel = computed(() => ({ email: $localize`:@@regWizard.corr.email:Per e-mail`, post: $localize`:@@regWizard.corr.post:Per post` }[this.draft().correspondentie ?? 'post']));
protected diplomaHerkomstLabel = computed(() => ({ duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`, handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)` }[this.draft().diplomaHerkomst ?? 'handmatig']));
protected adresHerkomstLabel = computed(
() =>
({
brp: $localize`:@@regWizard.herkomst.adresBrp:Automatisch uit de BRP`,
handmatig: $localize`:@@regWizard.herkomst.adresHandmatig:Handmatig ingevoerd`,
})[this.draft().adresHerkomst ?? 'handmatig'],
);
protected correspondentieLabel = computed(
() =>
({
email: $localize`:@@regWizard.corr.email:Per e-mail`,
post: $localize`:@@regWizard.corr.post:Per post`,
})[this.draft().correspondentie ?? 'post'],
);
protected diplomaHerkomstLabel = computed(
() =>
({
duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`,
handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)`,
})[this.draft().diplomaHerkomst ?? 'handmatig'],
);
/** BRP lookup outcome (laden/gevonden/geen/fout) and the parsed DUO lookup, both
served by the application facade — the wizard renders, it does not fetch/parse. */
@@ -295,23 +493,35 @@ export class RegistratieWizardComponent {
readonly jaNee = JA_NEE;
protected err = (k: DraftField | 'correspondentie' | 'diploma' | 'documenten') => this.invullen()?.errors[k] ?? '';
protected err = (k: DraftField | 'correspondentie' | 'diploma' | 'documenten') =>
this.invullen()?.errors[k] ?? '';
protected vraagErr = (id: string) => this.invullen()?.errors.antwoorden?.[id] ?? '';
protected antwoord = (id: string) => this.draft().antwoorden[id] ?? ''; // runtime guard: missing key → undefined
protected set = (key: DraftField, value: string) => this.dispatch({ tag: 'SetField', key, value });
protected setKanaal = (value: string) => this.dispatch({ tag: 'SetCorrespondentie', value: value as Correspondentie });
protected set = (key: DraftField, value: string) =>
this.dispatch({ tag: 'SetField', key, value });
protected setKanaal = (value: string) =>
this.dispatch({ tag: 'SetCorrespondentie', value: value as Correspondentie });
/** True while the user is entering a diploma manually (not in the DUO list). */
protected handmatigActief = computed(() => this.draft().diplomaHerkomst === 'handmatig');
/** The radio selection: a diploma id, or the"not listed" sentinel in manual mode. */
protected diplomaKeuze = computed(() => (this.handmatigActief() ? HANDMATIG : this.draft().diplomaId ?? ''));
protected diplomaKeuze = computed(() =>
this.handmatigActief() ? HANDMATIG : (this.draft().diplomaId ?? ''),
);
protected diplomaOptions = (data: DuoLookupDto) => [
...data.diplomas.map((d) => ({ value: d.id, label: `${d.naam}${d.instelling} (${d.jaar})` })),
{ value: HANDMATIG, label: $localize`:@@regWizard.diplomaNietBij:Mijn diploma staat er niet bij` },
...data.diplomas.map((d) => ({
value: d.id,
label: `${d.naam}${d.instelling} (${d.jaar})`,
})),
{
value: HANDMATIG,
label: $localize`:@@regWizard.diplomaNietBij:Mijn diploma staat er niet bij`,
},
];
protected beroepOptions = (data: DuoLookupDto) => data.handmatig.beroepen.map((b) => ({ value: b, label: b }));
protected beroepOptions = (data: DuoLookupDto) =>
data.handmatig.beroepen.map((b) => ({ value: b, label: b }));
/** The policy questions that apply to the current choice (server-decided). */
protected actieveVragen = (data: DuoLookupDto): PolicyQuestionDto[] => {
@@ -324,24 +534,41 @@ export class RegistratieWizardComponent {
const data = this.duoData();
const d = this.draft();
if (!data) return [] as { vraag: string; antwoord: string }[];
const alle = [...data.diplomas.flatMap((x) => x.policyQuestions), ...data.handmatig.policyQuestions];
return (d.vraagIds ?? []).map((id) => ({ vraag: alle.find((q) => q.id === id)?.vraag ?? id, antwoord: d.antwoorden[id] ?? '' }));
const alle = [
...data.diplomas.flatMap((x) => x.policyQuestions),
...data.handmatig.policyQuestions,
];
return (d.vraagIds ?? []).map((id) => ({
vraag: alle.find((q) => q.id === id)?.vraag ?? id,
antwoord: d.antwoorden[id] ?? '',
}));
});
protected onDiplomaKeuze(data: DuoLookupDto, id: string) {
if (id === HANDMATIG) {
this.dispatch({ tag: 'KiesHandmatig', vraagIds: data.handmatig.policyQuestions.map((q) => q.id) });
this.dispatch({
tag: 'KiesHandmatig',
vraagIds: data.handmatig.policyQuestions.map((q) => q.id),
});
return;
}
const d = data.diplomas.find((x) => x.id === id);
if (d) this.dispatch({ tag: 'KiesDiploma', diplomaId: d.id, beroep: d.beroep, vraagIds: d.policyQuestions.map((q) => q.id) });
if (d)
this.dispatch({
tag: 'KiesDiploma',
diplomaId: d.id,
beroep: d.beroep,
vraagIds: d.policyQuestions.map((q) => q.id),
});
}
constructor() {
// An explicit seed (stories/tests) wins; otherwise resume from the backend draft
// (`?aanvraag=<id>`), or start fresh. Persistence is the draftSync controller's job.
const seeded = this.seed();
queueMicrotask(() => (seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume()));
queueMicrotask(() =>
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
);
// Prefill the address from the BRP lookup as it arrives. Track only the facade's
// parsed prefill signal; untrack the dispatch (it reads the state signal, which
// would otherwise make this effect loop on its own write). Don't clobber
@@ -352,7 +579,12 @@ export class RegistratieWizardComponent {
untracked(() => {
const s = this.state();
if (s.tag !== 'Invullen' || s.draft.straat) return;
this.dispatch({ tag: 'PrefillAdres', straat: a.straat, postcode: a.postcode, woonplaats: a.woonplaats });
this.dispatch({
tag: 'PrefillAdres',
straat: a.straat,
postcode: a.postcode,
woonplaats: a.woonplaats,
});
});
});
// A11y: focus management (step heading on step change, error summary on a
@@ -384,7 +616,10 @@ export class RegistratieWizardComponent {
private async runIfIndienen() {
const s = this.state();
if (s.tag !== 'Indienen') return;
const r = await this.draftSync.submit({ diplomaHerkomst: s.data.diplomaHerkomst, documents: s.data.documents });
const r = await this.draftSync.submit({
diplomaHerkomst: s.data.diplomaHerkomst,
documents: s.data.documents,
});
if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value.referentie ?? '' });
else this.dispatch({ tag: 'SubmitFailed', error: r.error });
}

View File

@@ -3,14 +3,36 @@ import { applicationConfig } from '@storybook/angular';
import { provideHttpClient } from '@angular/common/http';
import { provideApiClient } from '@shared/infrastructure/api-client.provider';
import { RegistratieWizardComponent } from './registratie-wizard.component';
import { Draft, RegistratieState, ValidRegistratie } from '@registratie/domain/registratie-wizard.machine';
import {
Draft,
RegistratieState,
ValidRegistratie,
} from '@registratie/domain/registratie-wizard.machine';
import { initialUpload } from '@shared/upload/upload.machine';
import { Postcode } from '@registratie/domain/value-objects/postcode';
const adres: Partial<Draft> = { straat: 'Lange Voorhout 9', postcode: '2514 EA', woonplaats: 'Den Haag', adresHerkomst: 'brp', correspondentie: 'post' };
const filled: Partial<Draft> = { ...adres, diplomaId: 'd1', beroep: 'Arts', diplomaHerkomst: 'duo', vraagIds: [] };
const adres: Partial<Draft> = {
straat: 'Lange Voorhout 9',
postcode: '2514 EA',
woonplaats: 'Den Haag',
adresHerkomst: 'brp',
correspondentie: 'post',
};
const filled: Partial<Draft> = {
...adres,
diplomaId: 'd1',
beroep: 'Arts',
diplomaHerkomst: 'duo',
vraagIds: [],
};
const invullen = (draft: Partial<Draft>, cursor = 0): RegistratieState => ({ tag: 'Invullen', draft: { antwoorden: {}, ...draft }, cursor, errors: {}, upload: initialUpload });
const invullen = (draft: Partial<Draft>, cursor = 0): RegistratieState => ({
tag: 'Invullen',
draft: { antwoorden: {}, ...draft },
cursor,
errors: {},
upload: initialUpload,
});
const validData: ValidRegistratie = {
adres: { straat: 'Lange Voorhout 9', postcode: '2514 EA' as Postcode, woonplaats: 'Den Haag' },
@@ -34,10 +56,39 @@ type Story = StoryObj<RegistratieWizardComponent>;
export const Adres: Story = { args: { seed: invullen(adres, 0) } };
export const Beroep: Story = { args: { seed: invullen(filled, 1) } };
/** English-language diploma → the Dutch-proficiency policy question appears. */
export const BeroepEngelstalig: Story = { args: { seed: invullen({ ...adres, diplomaId: 'd2', beroep: 'Arts', diplomaHerkomst: 'duo', vraagIds: ['nl-taalvaardigheid'] }, 1) } };
export const BeroepEngelstalig: Story = {
args: {
seed: invullen(
{
...adres,
diplomaId: 'd2',
beroep: 'Arts',
diplomaHerkomst: 'duo',
vraagIds: ['nl-taalvaardigheid'],
},
1,
),
},
};
/** Diploma not in DUO → declare beroep + the maximal policy-question set. */
export const BeroepHandmatig: Story = { args: { seed: invullen({ ...adres, diplomaId: 'handmatig', diplomaHerkomst: 'handmatig', vraagIds: ['nl-taalvaardigheid', 'diploma-erkend', 'toelichting'] }, 1) } };
export const BeroepHandmatig: Story = {
args: {
seed: invullen(
{
...adres,
diplomaId: 'handmatig',
diplomaHerkomst: 'handmatig',
vraagIds: ['nl-taalvaardigheid', 'diploma-erkend', 'toelichting'],
},
1,
),
},
};
export const Controle: Story = { args: { seed: invullen(filled, 2) } };
export const Indienen: Story = { args: { seed: { tag: 'Indienen', data: validData } } };
export const Ingediend: Story = { args: { seed: { tag: 'Ingediend', data: validData, referentie: 'BIG-2026-123456' } } };
export const Mislukt: Story = { args: { seed: { tag: 'Mislukt', data: validData, error: 'Netwerkfout' } } };
export const Ingediend: Story = {
args: { seed: { tag: 'Ingediend', data: validData, referentie: 'BIG-2026-123456' } },
};
export const Mislukt: Story = {
args: { seed: { tag: 'Mislukt', data: validData, error: 'Netwerkfout' } },
};

View File

@@ -12,11 +12,11 @@ import { RegistratieWizardComponent } from '@registratie/ui/registratie-wizard/r
<app-page-shell
i18n-heading="@@registratie.heading"
heading="Inschrijven in het BIG-register"
backLink="/dashboard">
backLink="/dashboard"
>
<app-alert type="info" i18n="@@registratie.intro">
In drie stappen schrijft u zich in: uw adres en correspondentievoorkeur, het
diploma waarmee u zich registreert, en een controle. Uw gegevens blijven bewaard
als u de pagina herlaadt.
In drie stappen schrijft u zich in: uw adres en correspondentievoorkeur, het diploma waarmee
u zich registreert, en een controle. Uw gegevens blijven bewaard als u de pagina herlaadt.
</app-alert>
<div class="app-section">
<app-registratie-wizard />

View File

@@ -9,11 +9,18 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
@Component({
selector: 'app-registration-detail-page',
imports: [
PageShellComponent, SkeletonComponent, ...ASYNC,
RegistrationSummaryComponent, ChangeRequestFormComponent,
PageShellComponent,
SkeletonComponent,
...ASYNC,
RegistrationSummaryComponent,
ChangeRequestFormComponent,
],
template: `
<app-page-shell i18n-heading="@@registratieDetail.heading" heading="Mijn gegevens" backLink="/dashboard">
<app-page-shell
i18n-heading="@@registratieDetail.heading"
heading="Mijn gegevens"
backLink="/dashboard"
>
<app-async [data]="store.profile()">
<ng-template appAsyncLoaded let-p>
<app-registration-summary [reg]="$any(p).registration" />

View File

@@ -13,25 +13,60 @@ import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
imports: [DatePipe, DataRowComponent, StatusBadgeComponent, DataBlockComponent],
template: `
<app-data-block i18n-ariaLabel="@@summary.ariaLabel" ariaLabel="Registratiegegevens">
<div app-data-row i18n-key="@@summary.bigNummer" key="BIG-nummer" [value]="reg().bigNummer"></div>
<div
app-data-row
i18n-key="@@summary.bigNummer"
key="BIG-nummer"
[value]="reg().bigNummer"
></div>
<div app-data-row i18n-key="@@summary.naam" key="Naam" [value]="reg().naam"></div>
<div app-data-row i18n-key="@@summary.beroep" key="Beroep" [value]="reg().beroep"></div>
<div app-data-row i18n-key="@@summary.status" key="Status">
<app-status-badge [label]="label()" [color]="color()" />
</div>
<div app-data-row i18n-key="@@summary.registratiedatum" key="Registratiedatum" [value]="reg().registratiedatum | date:'longDate'"></div>
<div
app-data-row
i18n-key="@@summary.registratiedatum"
key="Registratiedatum"
[value]="reg().registratiedatum | date: 'longDate'"
></div>
<!-- Each status variant renders only the row its own data supports. -->
@switch (reg().status.tag) {
@case ('Geregistreerd') {
<div app-data-row i18n-key="@@summary.uiterste" key="Uiterste herregistratie" [value]="$any(reg().status).herregistratieDatum | date:'longDate'"></div>
<div
app-data-row
i18n-key="@@summary.uiterste"
key="Uiterste herregistratie"
[value]="$any(reg().status).herregistratieDatum | date: 'longDate'"
></div>
}
@case ('Geschorst') {
<div app-data-row i18n-key="@@summary.geschorstTot" key="Geschorst tot" [value]="$any(reg().status).geschorstTot | date:'longDate'"></div>
<div app-data-row i18n-key="@@summary.reden" key="Reden" [value]="$any(reg().status).reden"></div>
<div
app-data-row
i18n-key="@@summary.geschorstTot"
key="Geschorst tot"
[value]="$any(reg().status).geschorstTot | date: 'longDate'"
></div>
<div
app-data-row
i18n-key="@@summary.reden"
key="Reden"
[value]="$any(reg().status).reden"
></div>
}
@case ('Doorgehaald') {
<div app-data-row i18n-key="@@summary.doorgehaaldOp" key="Doorgehaald op" [value]="$any(reg().status).doorgehaaldOp | date:'longDate'"></div>
<div app-data-row i18n-key="@@summary.reden" key="Reden" [value]="$any(reg().status).reden"></div>
<div
app-data-row
i18n-key="@@summary.doorgehaaldOp"
key="Doorgehaald op"
[value]="$any(reg().status).doorgehaaldOp | date: 'longDate'"
></div>
<div
app-data-row
i18n-key="@@summary.reden"
key="Reden"
[value]="$any(reg().status).reden"
></div>
}
}
</app-data-block>

View File

@@ -23,8 +23,18 @@ export const Geregistreerd: Story = {
args: { reg: { ...base, status: { tag: 'Geregistreerd', herregistratieDatum: '2027-09-01' } } },
};
export const Geschorst: Story = {
args: { reg: { ...base, status: { tag: 'Geschorst', geschorstTot: '2026-12-31', reden: 'Lopend tuchtonderzoek' } } },
args: {
reg: {
...base,
status: { tag: 'Geschorst', geschorstTot: '2026-12-31', reden: 'Lopend tuchtonderzoek' },
},
},
};
export const Doorgehaald: Story = {
args: { reg: { ...base, status: { tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'Op eigen verzoek' } } },
args: {
reg: {
...base,
status: { tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'Op eigen verzoek' },
},
},
};

View File

@@ -21,7 +21,7 @@ import { Aantekening } from '@registratie/domain/registration';
<tr>
<td>{{ row.type }}</td>
<td>{{ row.omschrijving }}</td>
<td>{{ row.datum | date:'mediumDate' }}</td>
<td>{{ row.datum | date: 'mediumDate' }}</td>
</tr>
}
</tbody>

View File

@@ -12,7 +12,11 @@ export const Default: Story = {
args: {
rows: [
{ type: 'Specialisme', omschrijving: 'Huisartsgeneeskunde', datum: '2016-04-12' },
{ type: 'Aantekening', omschrijving: 'Erkend opleider huisartsgeneeskunde', datum: '2019-01-08' },
{
type: 'Aantekening',
omschrijving: 'Erkend opleider huisartsgeneeskunde',
datum: '2019-01-08',
},
],
},
};