diff --git a/src/app/core/parse.ts b/src/app/core/parse.ts new file mode 100644 index 0000000..179d298 --- /dev/null +++ b/src/app/core/parse.ts @@ -0,0 +1,29 @@ +import { Brand, Result, ok, err } from './fp'; + +/** + * "Parse, don't validate." A validated value gets its own branded type, and the + * smart constructor is the ONLY way to mint one. So once you hold a Postcode you + * know it's well-formed — validity is carried in the type, not re-checked + * everywhere or tracked in a parallel error flag. + */ +export type Postcode = Brand; +export type Uren = Brand; + +export function parsePostcode(raw: string): Result { + const t = raw.trim().toUpperCase(); + if (!/^[1-9]\d{3}\s?[A-Z]{2}$/.test(t)) { + return err('Voer een geldige postcode in, bijv. 1234 AB.'); + } + // Normalise to "1234 AB" — the parser also cleans up. + return ok(t.replace(/^(\d{4})\s?([A-Z]{2})$/, '$1 $2') as Postcode); +} + +export function parseUren(raw: string): Result { + const t = raw.trim(); + const n = Number(t); + // Number('') is 0 — guard the empty string explicitly. + if (t === '' || !Number.isInteger(n) || n < 0) { + return err('Vul een geheel aantal in (0 of meer).'); + } + return ok(n as Uren); +} diff --git a/src/app/organisms/change-request-form/change-request-form.component.ts b/src/app/organisms/change-request-form/change-request-form.component.ts index 47aa799..fb923de 100644 --- a/src/app/organisms/change-request-form/change-request-form.component.ts +++ b/src/app/organisms/change-request-form/change-request-form.component.ts @@ -4,9 +4,19 @@ import { FormFieldComponent } from '../../molecules/form-field/form-field.compon import { TextInputComponent } from '../../atoms/text-input/text-input.component'; import { ButtonComponent } from '../../atoms/button/button.component'; import { HeadingComponent } from '../../atoms/heading/heading.component'; +import { Postcode, parsePostcode } from '../../core/parse'; + +/** A submitted change request carries a *parsed* postcode (branded Postcode), + not a raw string — downstream code can't receive an unvalidated one. */ +export interface ChangeRequest { + street: string; + zip: Postcode; + city: string; +} /** Organism: change-request (adreswijziging) form. Reuses the same form-field - molecule + text-input/button atoms as the login form. */ + molecule + text-input/button atoms as the login form. Field errors come + straight from the parser's Result — no parallel "is it valid" flag to drift. */ @Component({ selector: 'app-change-request-form', imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent, HeadingComponent], @@ -16,8 +26,8 @@ import { HeadingComponent } from '../../atoms/heading/heading.component'; - - + + @@ -33,11 +43,17 @@ export class ChangeRequestFormComponent { zip = ''; city = ''; streetError = signal(''); - submitted = output(); + zipError = signal(''); + submitted = output(); onSubmit() { - if (!this.street.trim()) { this.streetError.set('Vul straat en huisnummer in.'); return; } - this.streetError.set(''); - this.submitted.emit(); + const street = this.street.trim(); + this.streetError.set(street ? '' : 'Vul straat en huisnummer in.'); + + const postcode = parsePostcode(this.zip); + this.zipError.set(postcode.ok ? '' : postcode.error); + + if (!street || !postcode.ok) return; + this.submitted.emit({ street, zip: postcode.value, city: this.city.trim() }); } }