Add registratie wizard, BFF dashboard-view, contracts/value-objects, and architecture docs
Checkpoint of in-progress work: the registration wizard (address prefill, DUO diploma lookup, policy questions), decision-DTO contracts, parse-don't- validate value objects, infrastructure adapters, plus CLAUDE.md and the architecture/ADR docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
import { Component, ElementRef, computed, effect, inject, input, untracked, viewChild } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||
import { RadioGroupComponent } from '@shared/ui/radio-group/radio-group.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SpinnerComponent } from '@shared/ui/spinner/spinner.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { StepperComponent } from '@shared/ui/stepper/stepper.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { createStore } from '@shared/application/store';
|
||||
import { RemoteData, fromResource } from '@shared/application/remote-data';
|
||||
import { BrpAdapter, parseBrpAddress } from '@registratie/infrastructure/brp.adapter';
|
||||
import { DuoAdapter, parseDuoLookup } from '@registratie/infrastructure/duo.adapter';
|
||||
import { DuoLookupDto, PolicyQuestionDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||
import {
|
||||
RegistratieState,
|
||||
RegistratieMsg,
|
||||
Draft,
|
||||
DraftField,
|
||||
Correspondentie,
|
||||
StepId,
|
||||
initial,
|
||||
reduce,
|
||||
STEPS,
|
||||
} from '@registratie/domain/registratie-wizard.machine';
|
||||
import { submitRegistratie } from '@registratie/application/submit-registratie';
|
||||
|
||||
const STORAGE_KEY = 'registratie-v1'; // ponytail: bump the suffix if the persisted shape changes; no migration.
|
||||
const KANALEN = [{ value: 'email', label: 'E-mail' }, { value: 'post', label: 'Post' }];
|
||||
const JA_NEE = [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }];
|
||||
const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
|
||||
|
||||
/** Organism: the BIG-registration wizard. All state lives in one signal driven by
|
||||
the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the
|
||||
draft via an effect; the DUO diploma list renders through <app-async>; choosing
|
||||
a diploma reveals its server-derived beroep. The draft is persisted to
|
||||
localStorage so a reload keeps the user's progress. Built entirely from existing
|
||||
atoms/molecules. */
|
||||
@Component({
|
||||
selector: 'app-registratie-wizard',
|
||||
imports: [
|
||||
FormsModule, FormFieldComponent, TextInputComponent, RadioGroupComponent, ButtonComponent,
|
||||
AlertComponent, SpinnerComponent, SkeletonComponent, DataRowComponent, StepperComponent, ...ASYNC,
|
||||
],
|
||||
template: `
|
||||
@switch (state().tag) {
|
||||
@case ('Invullen') {
|
||||
<app-stepper class="app-section" [steps]="stepLabels" [current]="cursor()" />
|
||||
<h2 #stepHeading tabindex="-1" class="rhc-heading nl-heading--level-2 app-section">{{ stepTitle() }}</h2>
|
||||
<form (ngSubmit)="onPrimary()" class="app-form">
|
||||
@switch (step()) {
|
||||
@case ('adres') {
|
||||
@if (adresStatus() === 'laden') {
|
||||
<app-skeleton height="2.5rem" [count]="4" />
|
||||
} @else {
|
||||
@switch (adresStatus()) {
|
||||
@case ('gevonden') {
|
||||
<app-alert type="info">Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert>
|
||||
}
|
||||
@case ('geen') {
|
||||
<app-alert type="warning">We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert>
|
||||
}
|
||||
@case ('fout') {
|
||||
<app-alert type="warning">We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.</app-alert>
|
||||
}
|
||||
}
|
||||
<app-form-field label="Straat en huisnummer" fieldId="straat" [error]="err('straat')">
|
||||
<app-text-input inputId="straat" [invalid]="!!err('straat')" [ngModel]="draft().straat ?? ''" (ngModelChange)="set('straat', $event)" name="straat" />
|
||||
</app-form-field>
|
||||
<app-form-field label="Postcode" fieldId="postcode" [error]="err('postcode')">
|
||||
<app-text-input inputId="postcode" [invalid]="!!err('postcode')" [ngModel]="draft().postcode ?? ''" (ngModelChange)="set('postcode', $event)" name="postcode" placeholder="1234 AB" />
|
||||
</app-form-field>
|
||||
<app-form-field label="Woonplaats" fieldId="woonplaats" [error]="err('woonplaats')">
|
||||
<app-text-input inputId="woonplaats" [invalid]="!!err('woonplaats')" [ngModel]="draft().woonplaats ?? ''" (ngModelChange)="set('woonplaats', $event)" name="woonplaats" />
|
||||
</app-form-field>
|
||||
<app-form-field label="Hoe wilt u correspondentie ontvangen?" fieldId="correspondentie" [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 label="E-mailadres" fieldId="email" [error]="err('email')">
|
||||
<app-text-input inputId="email" type="email" [invalid]="!!err('email')" [ngModel]="draft().email ?? ''" (ngModelChange)="set('email', $event)" name="email" placeholder="naam@voorbeeld.nl" />
|
||||
</app-form-field>
|
||||
}
|
||||
}
|
||||
}
|
||||
@case ('beroep') {
|
||||
<app-async [data]="lookupRd()">
|
||||
<ng-template appAsyncLoaded let-data>
|
||||
<app-form-field label="Kies het diploma waarmee u zich wilt registreren" fieldId="diploma" [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">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 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="rhc-data-summary rhc-data-summary--row app-section">
|
||||
<app-data-row key="Beroep (afgeleid uit diploma)" [value]="draft().beroep ?? ''" />
|
||||
</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]="draft().antwoorden[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]="draft().antwoorden[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>
|
||||
}
|
||||
@case ('controle') {
|
||||
<app-alert type="info">Controleer uw gegevens en dien de registratie in.</app-alert>
|
||||
<dl class="rhc-data-summary rhc-data-summary--row app-section">
|
||||
<app-data-row key="Adres" [value]="adresSamenvatting()" />
|
||||
<app-data-row key="Herkomst adres" [value]="adresHerkomstLabel()" />
|
||||
<app-data-row key="Correspondentie" [value]="correspondentieLabel()" />
|
||||
@if (draft().correspondentie === 'email') {
|
||||
<app-data-row key="E-mailadres" [value]="draft().email ?? ''" />
|
||||
}
|
||||
<app-data-row key="Beroep" [value]="draft().beroep ?? ''" />
|
||||
<app-data-row key="Herkomst diploma" [value]="diplomaHerkomstLabel()" />
|
||||
@for (item of samenvattingVragen(); track item.vraag) {
|
||||
<app-data-row [key]="item.vraag" [value]="item.antwoord" />
|
||||
}
|
||||
</dl>
|
||||
<div class="app-button-row app-section">
|
||||
<app-button type="button" variant="subtle" (click)="dispatch({ tag: 'GaNaarStap', cursor: 0 })">Adres wijzigen</app-button>
|
||||
<app-button type="button" variant="subtle" (click)="dispatch({ tag: 'GaNaarStap', cursor: 1 })">Diploma wijzigen</app-button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="app-button-row app-button-row--spaced">
|
||||
@if (cursor() > 0) {
|
||||
<app-button type="button" variant="secondary" (click)="dispatch({ tag: 'Back' })">Vorige</app-button>
|
||||
}
|
||||
<app-button type="submit" variant="primary">{{ step() === 'controle' ? 'Registratie indienen' : 'Volgende' }}</app-button>
|
||||
<app-button type="button" variant="subtle" (click)="restart()">Annuleren</app-button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
@case ('Indienen') {
|
||||
<app-spinner /> <span>Uw registratie wordt verwerkt…</span>
|
||||
}
|
||||
@case ('Ingediend') {
|
||||
<app-alert type="ok">Uw registratie is ontvangen. Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.</app-alert>
|
||||
<div class="app-section">
|
||||
<app-button variant="secondary" (click)="restart()">Nieuwe registratie starten</app-button>
|
||||
</div>
|
||||
}
|
||||
@case ('Mislukt') {
|
||||
<app-alert type="error">Het indienen is niet gelukt: {{ failedError() }}</app-alert>
|
||||
<div class="app-section">
|
||||
<app-button variant="secondary" (click)="onRetry()">Opnieuw proberen</app-button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class RegistratieWizardComponent {
|
||||
private brp = inject(BrpAdapter);
|
||||
private duo = inject(DuoAdapter);
|
||||
private store = createStore<RegistratieState, RegistratieMsg>(initial, reduce);
|
||||
|
||||
protected adresRes = this.brp.adresResource();
|
||||
private diplomasRes = this.duo.diplomasResource();
|
||||
|
||||
/** Optional seed so Storybook / tests can mount any state directly. */
|
||||
seed = input<RegistratieState>(initial);
|
||||
|
||||
readonly kanalen = KANALEN;
|
||||
readonly stepLabels = ['Adres', 'Beroep', 'Controle']; // short labels for the stepper
|
||||
private stepTitles = ['Adres en correspondentievoorkeur', 'Beroep op basis van uw diploma', 'Controleren en indienen'];
|
||||
readonly state = this.store.model;
|
||||
readonly dispatch = this.store.dispatch;
|
||||
|
||||
/** Focus target: the step heading receives focus on step change (a11y). */
|
||||
private stepHeading = viewChild<ElementRef<HTMLElement>>('stepHeading');
|
||||
|
||||
private invullen = computed(() => (this.state().tag === 'Invullen' ? (this.state() as Extract<RegistratieState, { tag: 'Invullen' }>) : null));
|
||||
protected cursor = computed(() => this.invullen()?.cursor ?? 0);
|
||||
protected draft = computed<Draft>(() => this.invullen()?.draft ?? { antwoorden: {} });
|
||||
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 referentie = computed(() => (this.state().tag === 'Ingediend' ? (this.state() as Extract<RegistratieState, { tag: 'Ingediend' }>).referentie : ''));
|
||||
protected failedError = computed(() => (this.state().tag === 'Mislukt' ? (this.state() as Extract<RegistratieState, { tag: 'Mislukt' }>).error : ''));
|
||||
protected adresSamenvatting = computed(() => {
|
||||
const d = this.draft();
|
||||
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: 'Automatisch uit de BRP', handmatig: 'Handmatig ingevoerd' }[this.draft().adresHerkomst ?? 'handmatig']));
|
||||
protected correspondentieLabel = computed(() => ({ email: 'Per e-mail', post: 'Per post' }[this.draft().correspondentie ?? 'post']));
|
||||
protected diplomaHerkomstLabel = computed(() => ({ duo: 'Geverifieerd via DUO', handmatig: 'Handmatig ingevoerd (wordt beoordeeld)' }[this.draft().diplomaHerkomst ?? 'handmatig']));
|
||||
|
||||
/** BRP lookup outcome. A failure or "geen adres" never blocks the wizard — both
|
||||
fall back to manual entry (PRD §7). 'geen' covers both no-address-found and a
|
||||
malformed response; 'fout' is an unreachable BRP. */
|
||||
protected adresStatus = computed<'laden' | 'gevonden' | 'geen' | 'fout'>(() => {
|
||||
const st = this.adresRes.status();
|
||||
if (st === 'loading' || st === 'reloading') return 'laden';
|
||||
if (st === 'error') return 'fout';
|
||||
const json = this.adresRes.value();
|
||||
const parsed = json !== undefined ? parseBrpAddress(json) : null;
|
||||
return parsed && parsed.ok && parsed.value.gevonden ? 'gevonden' : 'geen';
|
||||
});
|
||||
|
||||
/** The DUO lookup (diplomas + manual fallback), validated at the trust boundary. */
|
||||
protected lookupRd = computed<RemoteData<Error | undefined, DuoLookupDto>>(() => {
|
||||
const rd = fromResource(this.diplomasRes);
|
||||
if (rd.tag !== 'Success') return rd;
|
||||
const parsed = parseDuoLookup(rd.value);
|
||||
return parsed.ok ? { tag: 'Success', value: parsed.value } : { tag: 'Failure', error: new Error(parsed.error) };
|
||||
});
|
||||
|
||||
/** Parsed lookup as a plain value (or null) — used outside the beroep step (the
|
||||
controle summary) where the <app-async> template variable isn't in scope. */
|
||||
private duoData = computed<DuoLookupDto | null>(() => {
|
||||
const rd = this.lookupRd();
|
||||
return rd.tag === 'Success' ? rd.value : null;
|
||||
});
|
||||
|
||||
readonly jaNee = JA_NEE;
|
||||
|
||||
protected err = (k: DraftField | 'correspondentie' | 'diploma') => this.invullen()?.errors[k] ?? '';
|
||||
protected vraagErr = (id: string) => this.invullen()?.errors.antwoorden?.[id] ?? '';
|
||||
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 diplomaOptions = (data: DuoLookupDto) => [
|
||||
...data.diplomas.map((d) => ({ value: d.id, label: `${d.naam} — ${d.instelling} (${d.jaar})` })),
|
||||
{ value: HANDMATIG, label: 'Mijn diploma staat er niet bij' },
|
||||
];
|
||||
|
||||
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[] => {
|
||||
if (this.handmatigActief()) return data.handmatig.policyQuestions;
|
||||
return data.diplomas.find((d) => d.id === this.draft().diplomaId)?.policyQuestions ?? [];
|
||||
};
|
||||
|
||||
/** Answered policy questions for the controle summary (question text + answer). */
|
||||
protected samenvattingVragen = computed(() => {
|
||||
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] ?? '' }));
|
||||
});
|
||||
|
||||
protected onDiplomaKeuze(data: DuoLookupDto, id: string) {
|
||||
if (id === HANDMATIG) {
|
||||
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) });
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// An explicit seed (stories) wins; otherwise resume from localStorage.
|
||||
const seeded = this.seed();
|
||||
queueMicrotask(() => this.dispatch({ tag: 'Seed', state: seeded !== initial ? seeded : (this.restore() ?? initial) }));
|
||||
// Persist only while filling in; clear once the flow is done.
|
||||
effect(() => {
|
||||
const s = this.state();
|
||||
if (s.tag === 'Invullen') localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
|
||||
else localStorage.removeItem(STORAGE_KEY);
|
||||
});
|
||||
// Prefill the address from the BRP lookup as it arrives. Track only the resource
|
||||
// value; untrack the dispatch (it reads the state signal, which would otherwise
|
||||
// make this effect loop on its own write). Don't clobber edits/restored data.
|
||||
effect(() => {
|
||||
const json = this.adresRes.value();
|
||||
if (!json) return;
|
||||
const parsed = parseBrpAddress(json);
|
||||
untracked(() => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Invullen' || s.draft.straat) return;
|
||||
// ponytail: slice 2 handles gevonden === false -> manual entry stays handmatig.
|
||||
if (parsed.ok && parsed.value.gevonden && parsed.value.adres) {
|
||||
const a = parsed.value.adres;
|
||||
this.dispatch({ tag: 'PrefillAdres', straat: a.straat, postcode: a.postcode, woonplaats: a.woonplaats });
|
||||
}
|
||||
});
|
||||
});
|
||||
// A11y: move focus to the step heading when the STEP changes (depends only on
|
||||
// cursor(), which is value-stable across keystrokes — so typing never steals
|
||||
// focus). Skip the first run so we don't grab focus on initial load.
|
||||
let firstStep = true;
|
||||
effect(() => {
|
||||
this.cursor();
|
||||
if (firstStep) { firstStep = false; return; }
|
||||
untracked(() => {
|
||||
if (this.state().tag !== 'Invullen') return;
|
||||
queueMicrotask(() => this.stepHeading()?.nativeElement.focus());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private restore(): RegistratieState | null {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw) as RegistratieState;
|
||||
} catch {
|
||||
return null; // ponytail: corrupt entry -> start fresh, no migration.
|
||||
}
|
||||
}
|
||||
|
||||
onPrimary() {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Invullen') return;
|
||||
this.dispatch(this.step() === 'controle' ? { tag: 'Submit' } : { tag: 'Next' });
|
||||
this.runIfIndienen();
|
||||
}
|
||||
|
||||
onRetry() {
|
||||
this.dispatch({ tag: 'Retry' });
|
||||
this.runIfIndienen();
|
||||
}
|
||||
|
||||
/** Reset the wizard to a fresh start. Reload the BRP lookup so the address
|
||||
re-prefills, keeping the form and the "vooraf ingevuld" note consistent. */
|
||||
restart() {
|
||||
this.dispatch({ tag: 'Seed', state: initial });
|
||||
this.adresRes.reload();
|
||||
}
|
||||
|
||||
/** The effect: when we enter Indienen, call the backend, then dispatch the
|
||||
outcome (success carries the confirmation reference). */
|
||||
private async runIfIndienen() {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Indienen') return;
|
||||
const r = await submitRegistratie(s.data);
|
||||
if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value });
|
||||
else this.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { applicationConfig } from '@storybook/angular';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { RegistratieWizardComponent } from './registratie-wizard.component';
|
||||
import { Draft, RegistratieState, ValidRegistratie } from '@registratie/domain/registratie-wizard.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 invullen = (draft: Partial<Draft>, cursor = 0): RegistratieState => ({ tag: 'Invullen', draft: { antwoorden: {}, ...draft }, cursor, errors: {} });
|
||||
|
||||
const validData: ValidRegistratie = {
|
||||
adres: { straat: 'Lange Voorhout 9', postcode: '2514 EA' as Postcode, woonplaats: 'Den Haag' },
|
||||
adresHerkomst: 'brp',
|
||||
correspondentie: 'post',
|
||||
diplomaId: 'd1',
|
||||
diplomaHerkomst: 'duo',
|
||||
beroep: 'Arts',
|
||||
antwoorden: {},
|
||||
};
|
||||
|
||||
const meta: Meta<RegistratieWizardComponent> = {
|
||||
title: 'Registratie/RegistratieWizard',
|
||||
component: RegistratieWizardComponent,
|
||||
decorators: [applicationConfig({ providers: [provideHttpClient()] })],
|
||||
};
|
||||
export default meta;
|
||||
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) } };
|
||||
/** 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 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' } } };
|
||||
Reference in New Issue
Block a user