import { Result, assertNever } from '@shared/kernel/fp'; /** The three actions the beoordeling screen offers a behandelaar — mirrors the backend's `Besluit` enum member names 1:1 (the wire convention: a string, not a raw enum — see `RecordBesluitRequest`). */ const BESLUIT_TAGS = ['Goedkeuren', 'Afwijzen', 'MeerInfoOpvragen'] as const; export type BesluitTag = (typeof BESLUIT_TAGS)[number]; function isBesluitTag(v: string): v is BesluitTag { return (BESLUIT_TAGS as readonly string[]).includes(v); } /** What the user picked (raw, possibly empty while nothing is selected yet). */ export interface Draft { besluit: string; toelichting: string; } /** After parsing — besluit is the narrow tag; toelichting is present only when given (required for Afwijzen/MeerInfoOpvragen, optional for Goedkeuren — enforced by validate). */ export interface Valid { besluit: BesluitTag; toelichting?: string; } export type Errors = Partial>; /** The decision form as one tagged union — same idiom as every other form in this house (form-machine skill), single-step. draft/errors exist only while Editing. */ export type BesluitState = | { tag: 'Editing'; draft: Draft; errors: Errors } | { tag: 'Submitting'; data: Valid } | { tag: 'Submitted'; data: Valid } | { tag: 'Failed'; data: Valid; error: string }; export const initial: BesluitState = { tag: 'Editing', draft: { besluit: '', toelichting: '' }, errors: {}, }; function validate(draft: Draft): Result { if (!isBesluitTag(draft.besluit)) { return { ok: false, error: { besluit: $localize`:@@besluit.error.verplicht:Kies een besluit.` }, }; } const toelichting = draft.toelichting.trim(); if (draft.besluit !== 'Goedkeuren' && toelichting === '') { return { ok: false, error: { toelichting: $localize`:@@besluit.error.toelichtingVerplicht:Geef een toelichting.`, }, }; } return { ok: true, value: { besluit: draft.besluit, toelichting: toelichting || undefined } }; } export type BesluitMsg = | { tag: 'SetField'; key: keyof Draft; value: string } | { tag: 'Submit' } | { tag: 'Retry' } | { tag: 'SubmitConfirmed' } | { tag: 'SubmitFailed'; error: string } | { tag: 'Reset' } | { tag: 'Seed'; state: BesluitState }; // mount a specific state (stories/tests) export function reduce(s: BesluitState, m: BesluitMsg): BesluitState { switch (m.tag) { case 'SetField': return s.tag === 'Editing' ? { ...s, draft: { ...s.draft, [m.key]: m.value } } : s; case 'Submit': { if (s.tag !== 'Editing') return s; const r = validate(s.draft); return r.ok ? { tag: 'Submitting', data: r.value } : { ...s, errors: r.error }; } case 'Retry': return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s; case 'SubmitConfirmed': return s.tag === 'Submitting' ? { tag: 'Submitted', data: s.data } : s; case 'SubmitFailed': return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s; case 'Reset': return initial; case 'Seed': return m.state; default: return assertNever(m); } }