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 { AlertComponent } from '@shared/ui/alert/alert.component'; import { WizardShellComponent, WizardError, WizardStatus } from '@shared/layout/wizard-shell/wizard-shell.component'; import { createStore } from '@shared/application/store'; import { whenTag } from '@shared/kernel/fp'; import { BigProfileStore } from '@registratie/application/big-profile.store'; import { WizardState, WizardMsg, Draft, initial, reduce, hasProgress } from '@herregistratie/domain/herregistratie.machine'; import { createDraftSync } from '@registratie/application/draft-sync'; import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component'; import { createUploadController } from '@shared/upload/upload-controller'; import { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload.machine'; /** 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, AlertComponent, WizardShellComponent, DocumentUploadComponent], template: ` @switch (step()) { @case (1) { } @case (2) { } @case (3) { @if (errDocumenten()) { {{ errDocumenten() }} } } }
Uw aanvraag tot herregistratie is ontvangen.
`, }) export class HerregistratieWizardComponent { private profile = inject(BigProfileStore); private store = createStore(initial, reduce); /** Optional seed so Storybook / the showcase can mount any state directly. */ seed = input(initial); readonly state = this.store.model; // public so the showcase can highlight the live state protected dispatch = this.store.dispatch; // Backend draft-sync (new persistence for this wizard): create a Concept on first // progress, debounced-sync the snapshot, resume by `?aanvraag=`. private draftSync = createDraftSync({ type: 'herregistratie', snapshot: () => { const s = this.state(); if (s.tag !== 'Editing' || !hasProgress(s)) return null; const documentIds = deliveryRefs(s.upload).filter((r) => r.channel === 'digital' && r.documentId).map((r) => r.documentId!); return { draft: s, stepIndex: s.step - 1, stepCount: this.stepLabels.length, documentIds }; }, onResume: (draft) => this.dispatch({ tag: 'Seed', state: (draft as WizardState | null) ?? initial }), enabled: () => this.seed() === initial, }); // Stepper labels + per-step heading titles (presentational only). readonly stepLabels = [$localize`:@@herregWizard.step.werkervaring:Werkervaring`, $localize`:@@herregWizard.step.nascholing:Nascholing`, $localize`:@@herregWizard.step.documenten:Documenten`]; private stepTitles = [$localize`:@@herregWizard.title.werkervaring:Werkervaring (afgelopen 5 jaar)`, $localize`:@@herregWizard.title.nascholing:Nascholing`, $localize`:@@herregWizard.title.documenten:Documenten aanleveren`]; private editing = computed(() => whenTag(this.state(), 'Editing')); protected step = computed(() => this.editing()?.step ?? 1); protected draft = computed(() => this.editing()?.draft ?? { uren: '', jaren: '', punten: '' }); protected upload = computed(() => this.editing()?.upload ?? initialUpload); protected errUren = computed(() => this.editing()?.errors.uren ?? ''); protected errJaren = computed(() => this.editing()?.errors.jaren ?? ''); protected errPunten = computed(() => this.editing()?.errors.punten ?? ''); protected errDocumenten = computed(() => this.editing()?.errors.documenten ?? ''); protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? ''); protected uploadCtl = createUploadController({ wizardId: 'herregistratie', getUpload: () => this.upload(), dispatch: (msg) => this.dispatch({ tag: 'Upload', msg }), }); // --- Presentational wiring for the shared wizard shell --------------------- protected stepTitle = computed(() => this.stepTitles[this.step() - 1]); protected primaryLabel = computed(() => (this.step() < 3 ? $localize`:@@wizard.volgende:Volgende` : $localize`:@@herregWizard.indienen:Herregistratie aanvragen`)); protected errorMessage = computed(() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`); protected shellStatus = computed(() => { switch (this.state().tag) { case 'Editing': return 'editing'; case 'Submitting': return 'submitting'; case 'Submitted': return 'submitted'; case 'Failed': return 'failed'; } }); /** Current step's field errors, flattened for the shell's error summary. */ protected errorList = computed(() => { const e = this.editing()?.errors ?? {}; return (Object.keys(e) as (keyof typeof e)[]) .filter((k) => e[k]) .map((k) => ({ id: k, message: e[k]! })); }); constructor() { // An explicit seed (stories/tests) wins; otherwise resume the backend draft // (`?aanvraag=`) 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())); } onPrimary() { const s = this.state(); if (s.tag !== 'Editing') return; this.dispatch(s.step < 3 ? { tag: 'Next' } : { tag: 'Submit' }); this.runIfSubmitting(); } onRetry() { this.dispatch({ tag: 'Retry' }); this.runIfSubmitting(); } /** Reset the wizard to a fresh, empty start. */ restart() { this.draftSync.reset(); this.dispatch({ tag: 'Seed', state: initial }); } /** The effect: when we entered Submitting, submit through the aanvraag lifecycle, flip the optimistic cross-page flag, then dispatch the result (commit/rollback). */ private async runIfSubmitting() { const s = this.state(); if (s.tag !== 'Submitting') return; this.profile.beginHerregistratie(); const r = await this.draftSync.submit({ uren: s.data.uren, documents: s.data.documents }); if (r.ok) { this.dispatch({ tag: 'SubmitConfirmed' }); this.profile.confirmHerregistratie(); } else { this.dispatch({ tag: 'SubmitFailed', error: r.error }); this.profile.rollbackHerregistratie(); } } }