refactor: WizardStatus to a payload-carrying WizardPhase (RD-10)

The wizard shell took two inputs to say one thing: a flat WizardStatus
string and a separate errorMessage input. Each wizard needed three
computeds (failedError, errorMessage, shellStatus) to take the state
apart and put it back together for the shell.

WizardPhase replaces both inputs with one discriminated union. Its
Failed variant carries the message directly, so no data travels through
a second channel. Each wizard now maps its own tags onto WizardPhase in
one computed, composing the localized failure prefix at the same spot
errorMessage did before. The three machines and their own vocabulary
(Editing/Answering/Invullen, Indienen/Ingediend/Mislukt) are unchanged;
only the shell's input contract changes.

The shell reads the Failed message via the existing whenTag helper,
because @switch cannot narrow a union in an Angular template.

Both $localize ids (wizard.indienenMislukt, regWizard.indienenMislukt)
keep byte-identical source text, so no locale file changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-09-04 17:45:05 +02:00
co-authored by Claude Sonnet 5
parent 84cbf3f7d8
commit 11664d2efa
7 changed files with 261 additions and 60 deletions
@@ -6,7 +6,7 @@ import { AlertComponent } from '@shared/ui/alert/alert.component';
import {
WizardShellComponent,
WizardError,
WizardStatus,
WizardPhase,
naarStapLabel,
} from '@shared/layout/wizard-shell/wizard-shell.component';
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
@@ -50,11 +50,10 @@ import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.
[stepTitle]="stepTitle()"
i18n-processName="@@herregWizard.processName"
processName="Herregistratie aanvragen"
[status]="shellStatus()"
[phase]="phase()"
[primaryLabel]="primaryLabel()"
[canGoBack]="step() > 1"
[errors]="errorList()"
[errorMessage]="errorMessage()"
(primary)="dispatch({ tag: 'Primary' })"
(back)="dispatch({ tag: 'Back' })"
(cancel)="restart()"
@@ -214,7 +213,6 @@ export class HerregistratieWizardComponent {
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(),
@@ -234,19 +232,22 @@ export class HerregistratieWizardComponent {
protected goToStep(index: number) {
this.dispatch({ tag: 'GaNaarStap', step: (index + 1) as 1 | 2 | 3 });
}
protected errorMessage = computed(
() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`,
);
protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) {
/** Maps this machine's own tags onto the shell's `WizardPhase` vocabulary,
composing the localized failure prefix so the `Failed` message arrives intact. */
protected phase = computed<WizardPhase>(() => {
const s = this.state();
switch (s.tag) {
case 'Editing':
return 'editing';
return { tag: 'Editing' };
case 'Submitting':
return 'submitting';
return { tag: 'Submitting' };
case 'Submitted':
return 'submitted';
return { tag: 'Submitted' };
case 'Failed':
return 'failed';
return {
tag: 'Failed',
message: $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${s.error}`,
};
}
});
/** Current step's field errors, flattened for the shell's error summary. */
@@ -12,7 +12,7 @@ import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.comp
import {
WizardShellComponent,
WizardError,
WizardStatus,
WizardPhase,
naarStapLabel,
} from '@shared/layout/wizard-shell/wizard-shell.component';
import { createStore } from '@shared/application/store';
@@ -59,11 +59,10 @@ import { IntakePolicyStore } from '@herregistratie/application/intake-policy.sto
[stepTitle]="stepTitle()"
i18n-processName="@@intake.processName"
processName="Herregistratie-intake"
[status]="shellStatus()"
[phase]="phase()"
[primaryLabel]="primaryLabel()"
[canGoBack]="cursor() > 0"
[errors]="errorList()"
[errorMessage]="errorMessage()"
(primary)="dispatch({ tag: 'Primary' })"
(back)="dispatch({ tag: 'Back' })"
(cancel)="restart()"
@@ -323,7 +322,6 @@ export class IntakeWizardComponent {
);
/** Whether the inline scholing question is shown (and required) in the 'werk' step. */
protected scholingZichtbaar = computed(() => lageUren(this.answers(), this.scholingThreshold()));
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');
// --- Presentational wiring for the shared wizard shell ---------------------
readonly stepLabels = [
@@ -342,19 +340,22 @@ export class IntakeWizardComponent {
const next = this.cursor() + 1;
return naarStapLabel(next + 1, this.stepLabels[next]);
});
protected errorMessage = computed(
() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`,
);
protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) {
/** Maps this machine's own tags onto the shell's `WizardPhase` vocabulary,
composing the localized failure prefix so the `Failed` message arrives intact. */
protected phase = computed<WizardPhase>(() => {
const s = this.state();
switch (s.tag) {
case 'Answering':
return 'editing';
return { tag: 'Editing' };
case 'Submitting':
return 'submitting';
return { tag: 'Submitting' };
case 'Submitted':
return 'submitted';
return { tag: 'Submitted' };
case 'Failed':
return 'failed';
return {
tag: 'Failed',
message: $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${s.error}`,
};
}
});
/** Current step's field errors, flattened for the shell's error summary. The