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>
This commit is contained in:
eho
2026-06-26 09:38:11 +02:00
co-authored by Claude Opus 4.8
parent 6086729563
commit 164d20a10d
4 changed files with 36 additions and 16 deletions
@@ -4,12 +4,14 @@ import { Uren, parseUren } from '@registratie/domain/value-objects/uren';
/** What the user is typing (raw, possibly invalid). */
export interface Draft {
uren: string;
jaren: string;
punten: string;
}
/** What we have AFTER parsing — branded/typed, guaranteed valid. */
export interface Valid {
uren: Uren;
jaren: number;
punten: number;
}
@@ -25,26 +27,32 @@ export type WizardState =
| { tag: 'Submitted'; data: Valid }
| { tag: 'Failed'; data: Valid; error: string };
export const initial: WizardState = { tag: 'Editing', step: 1, draft: { uren: '', punten: '' }, errors: {} };
export const initial: WizardState = { tag: 'Editing', step: 1, draft: { uren: '', jaren: '', punten: '' }, errors: {} };
/** Parse every field; on success hand back a Valid, else the per-field errors. */
function validate(draft: Draft): Result<Partial<Record<keyof Draft, string>>, Valid> {
const uren = parseUren(draft.uren);
const jaren = parseUren(draft.jaren);
const punten = parseUren(draft.punten);
const errors: Partial<Record<keyof Draft, string>> = {};
if (!uren.ok) errors.uren = uren.error;
if (!jaren.ok) errors.jaren = jaren.error;
if (!punten.ok) errors.punten = punten.error;
if (uren.ok && punten.ok) return { ok: true, value: { uren: uren.value, punten: punten.value } };
if (uren.ok && jaren.ok && punten.ok) {
return { ok: true, value: { uren: uren.value, jaren: jaren.value, punten: punten.value } };
}
return { ok: false, error: errors };
}
/** Step 1 → 2: only advance if the uren field parses. Illegal elsewhere = no-op. */
/** Step 1 → 2: advance only when BOTH step-1 fields parse. Illegal elsewhere = no-op. */
export function next(s: WizardState): WizardState {
if (s.tag !== 'Editing' || s.step !== 1) return s;
const uren = parseUren(s.draft.uren);
return uren.ok
? { ...s, step: 2, errors: {} }
: { ...s, errors: { uren: uren.error } };
const jaren = parseUren(s.draft.jaren);
const errors: Partial<Record<keyof Draft, string>> = {};
if (!uren.ok) errors.uren = uren.error;
if (!jaren.ok) errors.jaren = jaren.error;
return Object.keys(errors).length === 0 ? { ...s, step: 2, errors: {} } : { ...s, errors };
}
export function back(s: WizardState): WizardState {