Files
atomic-design-poc/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts
Edwin van den Houdt 164d20a10d Add second question (jaren werkzaam) to herregistratie wizard step 1
Step 1 was a single field, making the wizard feel thin. Add "Aantal jaren
werkzaam" beside "Gewerkte uren" on the same step (no new step): Draft/Valid
gain `jaren`, `next` validates both step-1 fields before advancing, and
`validate` parses it for the submitted payload. Verified live: an empty jaren
blocks advancing with an inline error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:38:11 +02:00

114 lines
5.6 KiB
TypeScript

import { Component, computed, inject, input } 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 { ButtonComponent } from '@shared/ui/button/button.component';
import { AlertComponent } from '@shared/ui/alert/alert.component';
import { SpinnerComponent } from '@shared/ui/spinner/spinner.component';
import { createStore } from '@shared/application/store';
import { BigProfileStore } from '@registratie/application/big-profile.store';
import { WizardState, WizardMsg, Draft, initial, reduce } from '@herregistratie/domain/herregistratie.machine';
import { submitHerregistratie } from '@herregistratie/application/submit-herregistratie';
/** Organism: multi-step herregistratie wizard. ALL state lives in one signal
driven by the pure `reduce` function (see herregistratie.machine.ts) via an
Elm-style store. The UI just sends messages and folds over the state's tag —
no booleans like `submitting`/`submitted` that could contradict each other.
Submitting also flips an optimistic flag on the shared BigProfileStore, so
the dashboard shows "in behandeling" immediately. */
@Component({
selector: 'app-herregistratie-wizard',
imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent, AlertComponent, SpinnerComponent],
template: `
@switch (state().tag) {
@case ('Editing') {
<p class="rhc-paragraph" style="color:var(--rhc-color-grijs-700)">Stap {{ step() }} van 2</p>
<form (ngSubmit)="onPrimary()" style="max-width:28rem">
@if (step() === 1) {
<app-form-field label="Gewerkte uren (afgelopen 5 jaar)" fieldId="uren" [error]="errUren()">
<app-text-input inputId="uren" [ngModel]="draft().uren" (ngModelChange)="dispatch({ tag: 'SetField', key: 'uren', value: $event })"
name="uren" [invalid]="!!errUren()" placeholder="bijv. 4160" />
</app-form-field>
<app-form-field label="Aantal jaren werkzaam" fieldId="jaren" [error]="errJaren()">
<app-text-input inputId="jaren" [ngModel]="draft().jaren" (ngModelChange)="dispatch({ tag: 'SetField', key: 'jaren', value: $event })"
name="jaren" [invalid]="!!errJaren()" placeholder="bijv. 5" />
</app-form-field>
<app-button type="submit" variant="primary">Volgende</app-button>
} @else {
<app-form-field label="Behaalde nascholingspunten" fieldId="punten" [error]="errPunten()">
<app-text-input inputId="punten" [ngModel]="draft().punten" (ngModelChange)="dispatch({ tag: 'SetField', key: 'punten', value: $event })"
name="punten" [invalid]="!!errPunten()" placeholder="bijv. 200" />
</app-form-field>
<div style="display:flex;gap:0.5rem">
<app-button type="button" variant="secondary" (click)="dispatch({ tag: 'Back' })">Vorige</app-button>
<app-button type="submit" variant="primary">Herregistratie aanvragen</app-button>
</div>
}
</form>
}
@case ('Submitting') {
<app-spinner /> <span>Aanvraag wordt verwerkt…</span>
}
@case ('Submitted') {
<app-alert type="ok">Uw aanvraag tot herregistratie is ontvangen.</app-alert>
}
@case ('Failed') {
<app-alert type="error">Indienen mislukt: {{ failedError() }}</app-alert>
<div style="margin-top:1rem">
<app-button variant="secondary" (click)="onRetry()">Opnieuw proberen</app-button>
</div>
}
}
`,
})
export class HerregistratieWizardComponent {
private profile = inject(BigProfileStore);
private store = createStore<WizardState, WizardMsg>(initial, reduce);
/** Optional seed so Storybook / the showcase can mount any state directly. */
seed = input<WizardState>(initial);
readonly state = this.store.model; // public so the showcase can highlight the live state
protected dispatch = this.store.dispatch;
private editing = computed(() => (this.state().tag === 'Editing' ? (this.state() as Extract<WizardState, { tag: 'Editing' }>) : null));
protected step = computed(() => this.editing()?.step ?? 1);
protected draft = computed<Draft>(() => this.editing()?.draft ?? { uren: '', jaren: '', punten: '' });
protected errUren = computed(() => this.editing()?.errors.uren ?? '');
protected errJaren = computed(() => this.editing()?.errors.jaren ?? '');
protected errPunten = computed(() => this.editing()?.errors.punten ?? '');
protected failedError = computed(() => (this.state().tag === 'Failed' ? (this.state() as Extract<WizardState, { tag: 'Failed' }>).error : ''));
constructor() {
queueMicrotask(() => this.dispatch({ tag: 'Seed', state: this.seed() }));
}
onPrimary() {
const s = this.state();
if (s.tag !== 'Editing') return;
this.dispatch(s.step === 1 ? { tag: 'Next' } : { tag: 'Submit' });
this.runIfSubmitting();
}
onRetry() {
this.dispatch({ tag: 'Retry' });
this.runIfSubmitting();
}
/** The effect: when we entered Submitting, call the backend command, flip the
optimistic cross-page flag, then dispatch the result (and commit/rollback). */
private async runIfSubmitting() {
const s = this.state();
if (s.tag !== 'Submitting') return;
this.profile.beginHerregistratie();
const r = await submitHerregistratie(s.data);
if (r.ok) {
this.dispatch({ tag: 'SubmitConfirmed' });
this.profile.confirmHerregistratie();
} else {
this.dispatch({ tag: 'SubmitFailed', error: r.error });
this.profile.rollbackHerregistratie();
}
}
}