diff --git a/src/app/herregistratie/domain/herregistratie.machine.spec.ts b/src/app/herregistratie/domain/herregistratie.machine.spec.ts index cea8ca3..4ad8632 100644 --- a/src/app/herregistratie/domain/herregistratie.machine.spec.ts +++ b/src/app/herregistratie/domain/herregistratie.machine.spec.ts @@ -2,8 +2,8 @@ import { describe, it, expect } from 'vitest'; import { ok, err } from '@shared/kernel/fp'; import { initial, next, back, submit, resolve, reduce, WizardState } from './herregistratie.machine'; -const editing1 = (uren: string, punten = ''): WizardState => ({ tag: 'Editing', step: 1, draft: { uren, punten }, errors: {} }); -const editing2 = (uren: string, punten: string): WizardState => ({ tag: 'Editing', step: 2, draft: { uren, punten }, errors: {} }); +const editing1 = (uren: string, jaren = '5', punten = ''): WizardState => ({ tag: 'Editing', step: 1, draft: { uren, jaren, punten }, errors: {} }); +const editing2 = (uren: string, punten: string, jaren = '5'): WizardState => ({ tag: 'Editing', step: 2, draft: { uren, jaren, punten }, errors: {} }); describe('wizard.machine', () => { it('next advances only when step 1 parses', () => { @@ -16,7 +16,13 @@ describe('wizard.machine', () => { expect(submit(editing2('4160', 'x')).tag).toBe('Editing'); // invalid punten -> no Submitting const good = submit(editing2('4160', '200')); expect(good.tag).toBe('Submitting'); - expect((good as any).data).toEqual({ uren: 4160, punten: 200 }); + expect((good as any).data).toEqual({ uren: 4160, jaren: 5, punten: 200 }); + }); + + it('next requires BOTH step-1 fields (uren and jaren)', () => { + expect((next(editing1('4160', '')) as any).errors.jaren).toBeTruthy(); // jaren empty -> stays + expect((next(editing1('4160', '')) as any).step).toBe(1); + expect((next(editing1('4160', '5')) as any).step).toBe(2); // both valid -> advance }); it('back / resolve are no-ops from illegal states', () => { @@ -35,6 +41,7 @@ describe('reduce (message-driven)', () => { it('drives the full happy path via messages', () => { let s: WizardState = initial; s = reduce(s, { tag: 'SetField', key: 'uren', value: '4160' }); + s = reduce(s, { tag: 'SetField', key: 'jaren', value: '5' }); s = reduce(s, { tag: 'Next' }); expect(s.tag === 'Editing' && s.step).toBe(2); s = reduce(s, { tag: 'SetField', key: 'punten', value: '200' }); @@ -49,7 +56,7 @@ describe('reduce (message-driven)', () => { expect(s.tag).toBe('Failed'); s = reduce(s, { tag: 'Retry' }); expect(s.tag).toBe('Submitting'); - expect((s as any).data).toEqual({ uren: 4160, punten: 200 }); + expect((s as any).data).toEqual({ uren: 4160, jaren: 5, punten: 200 }); }); it('Seed mounts an arbitrary state', () => { diff --git a/src/app/herregistratie/domain/herregistratie.machine.ts b/src/app/herregistratie/domain/herregistratie.machine.ts index b1badfd..157efb3 100644 --- a/src/app/herregistratie/domain/herregistratie.machine.ts +++ b/src/app/herregistratie/domain/herregistratie.machine.ts @@ -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>, Valid> { const uren = parseUren(draft.uren); + const jaren = parseUren(draft.jaren); const punten = parseUren(draft.punten); const errors: Partial> = {}; 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> = {}; + 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 { diff --git a/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts b/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts index 2dbab38..39dcfef 100644 --- a/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts +++ b/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts @@ -29,6 +29,10 @@ import { submitHerregistratie } from '@herregistratie/application/submit-herregi + + + Volgende } @else { @@ -64,13 +68,14 @@ export class HerregistratieWizardComponent { /** Optional seed so Storybook / the showcase can mount any state directly. */ seed = input(initial); - protected state = this.store.model; + 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) : null)); protected step = computed(() => this.editing()?.step ?? 1); - protected draft = computed(() => this.editing()?.draft ?? { uren: '', punten: '' }); + protected draft = computed(() => 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).error : '')); diff --git a/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.stories.ts b/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.stories.ts index c283857..fda71ca 100644 --- a/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.stories.ts +++ b/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.stories.ts @@ -5,7 +5,7 @@ import { HerregistratieWizardComponent } from './herregistratie-wizard.component import { WizardState } from '@herregistratie/domain/herregistratie.machine'; import { Uren } from '@registratie/domain/value-objects/uren'; -const validData = { uren: 4160 as Uren, punten: 200 }; +const validData = { uren: 4160 as Uren, jaren: 5, punten: 200 }; const meta: Meta = { title: 'Herregistratie/Wizard', @@ -18,11 +18,11 @@ export default meta; type Story = StoryObj; // Each story seeds one state of the machine — one render per union variant. -export const Step1: Story = { args: { seed: { tag: 'Editing', step: 1, draft: { uren: '', punten: '' }, errors: {} } } }; +export const Step1: Story = { args: { seed: { tag: 'Editing', step: 1, draft: { uren: '', jaren: '', punten: '' }, errors: {} } } }; export const Step1Error: Story = { - args: { seed: { tag: 'Editing', step: 1, draft: { uren: 'abc', punten: '' }, errors: { uren: 'Vul een geheel aantal uren in (0 of meer).' } } satisfies WizardState }, + args: { seed: { tag: 'Editing', step: 1, draft: { uren: 'abc', jaren: '', punten: '' }, errors: { uren: 'Vul een geheel aantal in (0 of meer).', jaren: 'Vul een geheel aantal in (0 of meer).' } } satisfies WizardState }, }; -export const Step2: Story = { args: { seed: { tag: 'Editing', step: 2, draft: { uren: '4160', punten: '' }, errors: {} } } }; +export const Step2: Story = { args: { seed: { tag: 'Editing', step: 2, draft: { uren: '4160', jaren: '5', punten: '' }, errors: {} } } }; export const Submitting: Story = { args: { seed: { tag: 'Submitting', data: validData } } }; export const Submitted: Story = { args: { seed: { tag: 'Submitted', data: validData } } }; export const Failed: Story = { args: { seed: { tag: 'Failed', data: validData, error: 'Netwerkfout' } } };