style: format frontend, docs and skills with prettier; add .prettierignore
One-time prettier --write so the new format:check CI gate starts green. .prettierignore excludes generated (api-client.ts, documentation.json), vendored (public/cibg-huisstijl), and backend (dotnet format owns it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,33 +2,57 @@ import { describe, it, expect } from 'vitest';
|
||||
import { submittedRow, detailRows, purposeLabel, statusLabel, TYPE_LABELS } from './aanvraag-view';
|
||||
import { Aanvraag } from './aanvraag';
|
||||
|
||||
const base = { id: '1', type: 'herregistratie' as const, documentIds: [], createdAt: '', updatedAt: '', submittedAt: '2024-05-12' };
|
||||
const base = {
|
||||
id: '1',
|
||||
type: 'herregistratie' as const,
|
||||
documentIds: [],
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
submittedAt: '2024-05-12',
|
||||
};
|
||||
|
||||
describe('submittedRow', () => {
|
||||
it('heading is the type, subtitle is the purpose', () => {
|
||||
const row = submittedRow({ ...base, status: { tag: 'InBehandeling', referentie: 'R1', manual: false } } as Aanvraag);
|
||||
const row = submittedRow({
|
||||
...base,
|
||||
status: { tag: 'InBehandeling', referentie: 'R1', manual: false },
|
||||
} as Aanvraag);
|
||||
expect(row.heading).toBe(TYPE_LABELS.herregistratie);
|
||||
expect(row.subtitle).toBe(purposeLabel('herregistratie'));
|
||||
});
|
||||
|
||||
it('status line carries the status label, reference and submit date', () => {
|
||||
const row = submittedRow({ ...base, status: { tag: 'InBehandeling', referentie: 'R1', manual: false } } as Aanvraag);
|
||||
expect(row.status).toContain(statusLabel({ tag: 'InBehandeling', referentie: 'R1', manual: false }));
|
||||
const row = submittedRow({
|
||||
...base,
|
||||
status: { tag: 'InBehandeling', referentie: 'R1', manual: false },
|
||||
} as Aanvraag);
|
||||
expect(row.status).toContain(
|
||||
statusLabel({ tag: 'InBehandeling', referentie: 'R1', manual: false }),
|
||||
);
|
||||
expect(row.status).toContain('R1');
|
||||
expect(row.status).toContain('12 mei 2024');
|
||||
});
|
||||
|
||||
it('manual review adds a note; rejection adds its reason', () => {
|
||||
const manual = submittedRow({ ...base, status: { tag: 'InBehandeling', referentie: 'R1', manual: true } } as Aanvraag);
|
||||
const manual = submittedRow({
|
||||
...base,
|
||||
status: { tag: 'InBehandeling', referentie: 'R1', manual: true },
|
||||
} as Aanvraag);
|
||||
expect(manual.status).toContain('handmatig');
|
||||
const rejected = submittedRow({ ...base, status: { tag: 'Afgewezen', referentie: 'R2', reden: 'Onvoldoende uren' } } as Aanvraag);
|
||||
const rejected = submittedRow({
|
||||
...base,
|
||||
status: { tag: 'Afgewezen', referentie: 'R2', reden: 'Onvoldoende uren' },
|
||||
} as Aanvraag);
|
||||
expect(rejected.status).toContain('Onvoldoende uren');
|
||||
});
|
||||
});
|
||||
|
||||
describe('detailRows', () => {
|
||||
it('lists soort/waarvoor/status/referentie/ingediend, plus reason when rejected', () => {
|
||||
const rows = detailRows({ ...base, status: { tag: 'Afgewezen', referentie: 'R2', reden: 'Onvoldoende uren' } } as Aanvraag);
|
||||
const rows = detailRows({
|
||||
...base,
|
||||
status: { tag: 'Afgewezen', referentie: 'R2', reden: 'Onvoldoende uren' },
|
||||
} as Aanvraag);
|
||||
const values = rows.map((r) => r.value);
|
||||
expect(values).toContain(TYPE_LABELS.herregistratie);
|
||||
expect(values).toContain('R2');
|
||||
@@ -37,7 +61,11 @@ describe('detailRows', () => {
|
||||
});
|
||||
|
||||
it('reference falls back to em dash for a Concept', () => {
|
||||
const rows = detailRows({ ...base, submittedAt: undefined, status: { tag: 'Concept', stepIndex: 0, stepCount: 3 } } as Aanvraag);
|
||||
const rows = detailRows({
|
||||
...base,
|
||||
submittedAt: undefined,
|
||||
status: { tag: 'Concept', stepIndex: 0, stepCount: 3 },
|
||||
} as Aanvraag);
|
||||
const ref = rows.find((r) => r.value === '—');
|
||||
expect(ref).toBeTruthy();
|
||||
expect(rows.length).toBe(5);
|
||||
|
||||
@@ -13,19 +13,26 @@ export const TYPE_LABELS: Record<AanvraagType, string> = {
|
||||
/** What the aanvraag is for (shown under the title). */
|
||||
export function purposeLabel(type: AanvraagType): string {
|
||||
switch (type) {
|
||||
case 'registratie': return $localize`:@@aanvraag.purpose.registratie:Inschrijving in het BIG-register`;
|
||||
case 'herregistratie': return $localize`:@@aanvraag.purpose.herregistratie:Verlenging van uw BIG-registratie`;
|
||||
case 'intake': return $localize`:@@aanvraag.purpose.intake:Intake-vragenlijst voor uw herregistratie`;
|
||||
case 'registratie':
|
||||
return $localize`:@@aanvraag.purpose.registratie:Inschrijving in het BIG-register`;
|
||||
case 'herregistratie':
|
||||
return $localize`:@@aanvraag.purpose.herregistratie:Verlenging van uw BIG-registratie`;
|
||||
case 'intake':
|
||||
return $localize`:@@aanvraag.purpose.intake:Intake-vragenlijst voor uw herregistratie`;
|
||||
}
|
||||
}
|
||||
|
||||
/** The status as a plain label (what state the aanvraag is in). */
|
||||
export function statusLabel(status: AanvraagStatus): string {
|
||||
switch (status.tag) {
|
||||
case 'Concept': return $localize`:@@aanvraag.status.concept:Concept (nog niet ingediend)`;
|
||||
case 'InBehandeling': return $localize`:@@aanvraag.status.inBehandeling:In behandeling`;
|
||||
case 'Goedgekeurd': return $localize`:@@aanvraag.status.goedgekeurd:Goedgekeurd`;
|
||||
case 'Afgewezen': return $localize`:@@aanvraag.status.afgewezen:Afgewezen`;
|
||||
case 'Concept':
|
||||
return $localize`:@@aanvraag.status.concept:Concept (nog niet ingediend)`;
|
||||
case 'InBehandeling':
|
||||
return $localize`:@@aanvraag.status.inBehandeling:In behandeling`;
|
||||
case 'Goedgekeurd':
|
||||
return $localize`:@@aanvraag.status.goedgekeurd:Goedgekeurd`;
|
||||
case 'Afgewezen':
|
||||
return $localize`:@@aanvraag.status.afgewezen:Afgewezen`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +50,9 @@ export interface AanvraagRow {
|
||||
}
|
||||
|
||||
function formatNL(iso?: string): string {
|
||||
return iso ? new Date(iso).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' }) : '';
|
||||
return iso
|
||||
? new Date(iso).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' })
|
||||
: '';
|
||||
}
|
||||
|
||||
/** Fields for a submitted aanvraag's row in the dashboard "aanvragen" list (Concept
|
||||
@@ -53,10 +62,18 @@ export function submittedRow(a: Aanvraag): AanvraagRow {
|
||||
const parts = [statusLabel(s)];
|
||||
const ref = referentie(s);
|
||||
if (ref) parts.push($localize`:@@aanvraag.row.ref:Referentie ${ref}:ref:`);
|
||||
if (a.submittedAt) parts.push($localize`:@@aanvraag.row.ingediend:ingediend op ${formatNL(a.submittedAt)}:datum:`);
|
||||
if (s.tag === 'InBehandeling' && s.manual) parts.push($localize`:@@aanvraagBlock.manual:Uw aanvraag wordt handmatig beoordeeld in de backoffice.`);
|
||||
if (a.submittedAt)
|
||||
parts.push($localize`:@@aanvraag.row.ingediend:ingediend op ${formatNL(a.submittedAt)}:datum:`);
|
||||
if (s.tag === 'InBehandeling' && s.manual)
|
||||
parts.push(
|
||||
$localize`:@@aanvraagBlock.manual:Uw aanvraag wordt handmatig beoordeeld in de backoffice.`,
|
||||
);
|
||||
if (s.tag === 'Afgewezen') parts.push(s.reden);
|
||||
return { heading: TYPE_LABELS[a.type], subtitle: purposeLabel(a.type), status: parts.join(' · ') };
|
||||
return {
|
||||
heading: TYPE_LABELS[a.type],
|
||||
subtitle: purposeLabel(a.type),
|
||||
status: parts.join(' · '),
|
||||
};
|
||||
}
|
||||
|
||||
/** Key/value rows for the case-detail page (CIBG Datablock). */
|
||||
@@ -65,11 +82,20 @@ export function detailRows(a: Aanvraag): { key: string; value: string }[] {
|
||||
{ key: $localize`:@@aanvraag.detail.soort:Soort aanvraag`, value: TYPE_LABELS[a.type] },
|
||||
{ key: $localize`:@@aanvraag.detail.waarvoor:Waarvoor`, value: purposeLabel(a.type) },
|
||||
{ key: $localize`:@@aanvraag.detail.status:Status`, value: statusLabel(a.status) },
|
||||
{ key: $localize`:@@aanvraag.detail.referentie:Referentie`, value: referentie(a.status) || '—' },
|
||||
{ key: $localize`:@@aanvraag.detail.ingediend:Ingediend op`, value: a.submittedAt ? formatNL(a.submittedAt) : '—' },
|
||||
{
|
||||
key: $localize`:@@aanvraag.detail.referentie:Referentie`,
|
||||
value: referentie(a.status) || '—',
|
||||
},
|
||||
{
|
||||
key: $localize`:@@aanvraag.detail.ingediend:Ingediend op`,
|
||||
value: a.submittedAt ? formatNL(a.submittedAt) : '—',
|
||||
},
|
||||
];
|
||||
if (a.status.tag === 'Afgewezen') {
|
||||
rows.push({ key: $localize`:@@aanvraag.detail.reden:Reden van afwijzing`, value: a.status.reden });
|
||||
rows.push({
|
||||
key: $localize`:@@aanvraag.detail.reden:Reden van afwijzing`,
|
||||
value: a.status.reden,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@ import { blockActions } from './block-actions';
|
||||
|
||||
describe('blockActions', () => {
|
||||
it('a Concept can be resumed or cancelled', () => {
|
||||
expect(blockActions({ tag: 'Concept', stepIndex: 1, stepCount: 3 })).toEqual(['resume', 'cancel']);
|
||||
expect(blockActions({ tag: 'Concept', stepIndex: 1, stepCount: 3 })).toEqual([
|
||||
'resume',
|
||||
'cancel',
|
||||
]);
|
||||
});
|
||||
|
||||
it('an in-behandeling aanvraag only exposes its documents', () => {
|
||||
expect(blockActions({ tag: 'InBehandeling', referentie: 'BIG-1', manual: true })).toEqual(['viewDocuments']);
|
||||
expect(blockActions({ tag: 'InBehandeling', referentie: 'BIG-1', manual: true })).toEqual([
|
||||
'viewDocuments',
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolved aanvragen have no actions', () => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { State, reduce, initial } from './change-request.machine';
|
||||
|
||||
const editingWith = (draft: Partial<{ straat: string; postcode: string; woonplaats: string }>): State => ({
|
||||
const editingWith = (
|
||||
draft: Partial<{ straat: string; postcode: string; woonplaats: string }>,
|
||||
): State => ({
|
||||
tag: 'Editing',
|
||||
draft: { straat: '', postcode: '', woonplaats: '', ...draft },
|
||||
errors: {},
|
||||
@@ -23,13 +25,17 @@ describe('change-request reduce', () => {
|
||||
});
|
||||
|
||||
it('Submit with a valid draft moves to Submitting with parsed (normalised) data', () => {
|
||||
const s = reduce(editingWith({ straat: 'Lange Voorhout 9', postcode: '2514ea' }), { tag: 'Submit' });
|
||||
const s = reduce(editingWith({ straat: 'Lange Voorhout 9', postcode: '2514ea' }), {
|
||||
tag: 'Submit',
|
||||
});
|
||||
expect(s.tag).toBe('Submitting');
|
||||
expect((s as Extract<State, { tag: 'Submitting' }>).data.postcode).toBe('2514 EA');
|
||||
});
|
||||
|
||||
it('confirms and fails only from Submitting; Retry re-submits a failure', () => {
|
||||
const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { tag: 'Submit' });
|
||||
const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), {
|
||||
tag: 'Submit',
|
||||
});
|
||||
const ok = reduce(submitting, { tag: 'SubmitConfirmed', referentie: 'BIG-2026-1' });
|
||||
expect(ok).toMatchObject({ tag: 'Submitted', referentie: 'BIG-2026-1' });
|
||||
|
||||
@@ -39,7 +45,9 @@ describe('change-request reduce', () => {
|
||||
});
|
||||
|
||||
it('Reset returns to the initial editing state', () => {
|
||||
const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { tag: 'Submit' });
|
||||
const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), {
|
||||
tag: 'Submit',
|
||||
});
|
||||
expect(reduce(submitting, { tag: 'Reset' })).toEqual(initial);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,10 @@ function validate(draft: Draft): Result<Errors, Valid> {
|
||||
if (!straat) errors.straat = $localize`:@@validation.straat:Vul straat en huisnummer in.`;
|
||||
if (!postcode.ok) errors.postcode = postcode.error;
|
||||
if (straat && postcode.ok) {
|
||||
return { ok: true, value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() } };
|
||||
return {
|
||||
ok: true,
|
||||
value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() },
|
||||
};
|
||||
}
|
||||
return { ok: false, error: errors };
|
||||
}
|
||||
@@ -69,7 +72,9 @@ export function reduce(s: State, m: Msg): State {
|
||||
case 'Retry':
|
||||
return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;
|
||||
case 'SubmitConfirmed':
|
||||
return s.tag === 'Submitting' ? { tag: 'Submitted', data: s.data, referentie: m.referentie } : s;
|
||||
return s.tag === 'Submitting'
|
||||
? { tag: 'Submitted', data: s.data, referentie: m.referentie }
|
||||
: s;
|
||||
case 'SubmitFailed':
|
||||
return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;
|
||||
case 'Reset':
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { hasProgress, initial, RegistratieState } from './registratie-wizard.machine';
|
||||
|
||||
const invullen = (over: Partial<Extract<RegistratieState, { tag: 'Invullen' }>>) =>
|
||||
({ ...(initial as Extract<RegistratieState, { tag: 'Invullen' }>), ...over });
|
||||
const invullen = (over: Partial<Extract<RegistratieState, { tag: 'Invullen' }>>) => ({
|
||||
...(initial as Extract<RegistratieState, { tag: 'Invullen' }>),
|
||||
...over,
|
||||
});
|
||||
|
||||
describe('hasProgress', () => {
|
||||
it('is false for a fresh wizard', () => {
|
||||
@@ -10,13 +12,23 @@ describe('hasProgress', () => {
|
||||
});
|
||||
|
||||
it('ignores an auto-prefilled BRP address at step 0', () => {
|
||||
const s = invullen({ draft: { straat: 'Lange Voorhout 9', postcode: '2514 EA', woonplaats: 'Den Haag', adresHerkomst: 'brp', antwoorden: {} } });
|
||||
const s = invullen({
|
||||
draft: {
|
||||
straat: 'Lange Voorhout 9',
|
||||
postcode: '2514 EA',
|
||||
woonplaats: 'Den Haag',
|
||||
adresHerkomst: 'brp',
|
||||
antwoorden: {},
|
||||
},
|
||||
});
|
||||
expect(hasProgress(s)).toBe(false);
|
||||
});
|
||||
|
||||
it('is true once the user advances, picks correspondence/diploma, or is past step 0', () => {
|
||||
expect(hasProgress(invullen({ cursor: 1 }))).toBe(true);
|
||||
expect(hasProgress(invullen({ draft: { correspondentie: 'post', antwoorden: {} } }))).toBe(true);
|
||||
expect(hasProgress(invullen({ draft: { correspondentie: 'post', antwoorden: {} } }))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(hasProgress(invullen({ draft: { diplomaId: 'd1', antwoorden: {} } }))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,8 +29,19 @@ const invullen = (draft: Partial<Draft>, cursor = 0): RegistratieState => ({
|
||||
upload: initialUpload,
|
||||
});
|
||||
|
||||
const validAdres = { straat: 'Lange Voorhout 9', postcode: '2514 EA', woonplaats: 'Den Haag', correspondentie: 'post' as const, adresHerkomst: 'brp' as const };
|
||||
const validDraft: Partial<Draft> = { ...validAdres, diplomaId: 'd1', beroep: 'Arts', diplomaHerkomst: 'duo' };
|
||||
const validAdres = {
|
||||
straat: 'Lange Voorhout 9',
|
||||
postcode: '2514 EA',
|
||||
woonplaats: 'Den Haag',
|
||||
correspondentie: 'post' as const,
|
||||
adresHerkomst: 'brp' as const,
|
||||
};
|
||||
const validDraft: Partial<Draft> = {
|
||||
...validAdres,
|
||||
diplomaId: 'd1',
|
||||
beroep: 'Arts',
|
||||
diplomaHerkomst: 'duo',
|
||||
};
|
||||
|
||||
describe('STEPS (fixed)', () => {
|
||||
it('always has the same three steps', () => {
|
||||
@@ -106,7 +117,18 @@ describe('adres origin (BRP vs handmatig)', () => {
|
||||
});
|
||||
|
||||
it('a manually entered address still submits (only manual diploma is gated)', () => {
|
||||
const s = submit(invullen({ straat: 'Kerkstraat 1', postcode: '1234 AB', woonplaats: 'Utrecht', correspondentie: 'post', adresHerkomst: 'handmatig', diplomaId: 'd1', beroep: 'Arts', diplomaHerkomst: 'duo' }));
|
||||
const s = submit(
|
||||
invullen({
|
||||
straat: 'Kerkstraat 1',
|
||||
postcode: '1234 AB',
|
||||
woonplaats: 'Utrecht',
|
||||
correspondentie: 'post',
|
||||
adresHerkomst: 'handmatig',
|
||||
diplomaId: 'd1',
|
||||
beroep: 'Arts',
|
||||
diplomaHerkomst: 'duo',
|
||||
}),
|
||||
);
|
||||
expect(s.tag).toBe('Indienen');
|
||||
expect((s as any).data.adresHerkomst).toBe('handmatig');
|
||||
});
|
||||
@@ -185,7 +207,12 @@ describe('submit', () => {
|
||||
describe('reduce (message-driven happy path)', () => {
|
||||
it('drives the full flow via messages', () => {
|
||||
let s: RegistratieState = initial;
|
||||
s = reduce(s, { tag: 'PrefillAdres', straat: 'Lange Voorhout 9', postcode: '2514 EA', woonplaats: 'Den Haag' });
|
||||
s = reduce(s, {
|
||||
tag: 'PrefillAdres',
|
||||
straat: 'Lange Voorhout 9',
|
||||
postcode: '2514 EA',
|
||||
woonplaats: 'Den Haag',
|
||||
});
|
||||
s = reduce(s, { tag: 'SetCorrespondentie', value: 'post' });
|
||||
s = reduce(s, { tag: 'Next' });
|
||||
expect(currentStep(s as any)).toBe('beroep');
|
||||
@@ -199,7 +226,10 @@ describe('reduce (message-driven happy path)', () => {
|
||||
});
|
||||
|
||||
it('SubmitFailed then Retry returns to Indienen with the same data', () => {
|
||||
let s = reduce(reduce(invullen(validDraft), { tag: 'Submit' }), { tag: 'SubmitFailed', error: 'boom' });
|
||||
let s = reduce(reduce(invullen(validDraft), { tag: 'Submit' }), {
|
||||
tag: 'SubmitFailed',
|
||||
error: 'boom',
|
||||
});
|
||||
expect(s.tag).toBe('Mislukt');
|
||||
s = reduce(s, { tag: 'Retry' });
|
||||
expect(s.tag).toBe('Indienen');
|
||||
@@ -208,27 +238,51 @@ describe('reduce (message-driven happy path)', () => {
|
||||
});
|
||||
|
||||
describe('inline document upload (beroep step)', () => {
|
||||
const cat = { categoryId: 'diploma', label: 'Diploma', description: '', required: true, acceptedTypes: [], maxSizeMb: 10, multiple: false, allowPostDelivery: true };
|
||||
const cat = {
|
||||
categoryId: 'diploma',
|
||||
label: 'Diploma',
|
||||
description: '',
|
||||
required: true,
|
||||
acceptedTypes: [],
|
||||
maxSizeMb: 10,
|
||||
multiple: false,
|
||||
allowPostDelivery: true,
|
||||
};
|
||||
|
||||
it('routes Upload messages through the upload reducer', () => {
|
||||
const s = reduce(invullen(validDraft), { tag: 'Upload', msg: { type: 'CategoriesLoaded', categories: [cat] } });
|
||||
const s = reduce(invullen(validDraft), {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'CategoriesLoaded', categories: [cat] },
|
||||
});
|
||||
expect((s as any).upload.categories).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('blocks the beroep step until a required category is satisfied', () => {
|
||||
let s = reduce(invullen(validDraft, 1), { tag: 'Upload', msg: { type: 'CategoriesLoaded', categories: [cat] } });
|
||||
let s = reduce(invullen(validDraft, 1), {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'CategoriesLoaded', categories: [cat] },
|
||||
});
|
||||
s = reduce(s, { tag: 'Next' }); // beroep → controle blocked
|
||||
expect(currentStep(s as any)).toBe('beroep');
|
||||
expect((s as any).errors.documenten).toBeTruthy();
|
||||
// choosing post delivery satisfies the requirement
|
||||
s = reduce(s, { tag: 'Upload', msg: { type: 'DeliveryChannelChanged', categoryId: 'diploma', channel: 'post' } });
|
||||
s = reduce(s, {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'DeliveryChannelChanged', categoryId: 'diploma', channel: 'post' },
|
||||
});
|
||||
s = reduce(s, { tag: 'Next' });
|
||||
expect(currentStep(s as any)).toBe('controle');
|
||||
});
|
||||
|
||||
it('includes delivery refs in the submitted data', () => {
|
||||
let s = reduce(invullen(validDraft), { tag: 'Upload', msg: { type: 'CategoriesLoaded', categories: [cat] } });
|
||||
s = reduce(s, { tag: 'Upload', msg: { type: 'DeliveryChannelChanged', categoryId: 'diploma', channel: 'post' } });
|
||||
let s = reduce(invullen(validDraft), {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'CategoriesLoaded', categories: [cat] },
|
||||
});
|
||||
s = reduce(s, {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'DeliveryChannelChanged', categoryId: 'diploma', channel: 'post' },
|
||||
});
|
||||
const done = submit(s as any);
|
||||
expect(done.tag).toBe('Indienen');
|
||||
expect((done as any).data.documents).toEqual([{ categoryId: 'diploma', channel: 'post' }]);
|
||||
|
||||
@@ -84,7 +84,13 @@ export type RegistratieState =
|
||||
| { tag: 'Mislukt'; data: ValidRegistratie; error: string };
|
||||
|
||||
const emptyDraft: Draft = { antwoorden: {} };
|
||||
export const initial: RegistratieState = { tag: 'Invullen', draft: emptyDraft, cursor: 0, errors: {}, upload: initialUpload };
|
||||
export const initial: RegistratieState = {
|
||||
tag: 'Invullen',
|
||||
draft: emptyDraft,
|
||||
cursor: 0,
|
||||
errors: {},
|
||||
upload: initialUpload,
|
||||
};
|
||||
|
||||
/** Which step the cursor currently points at (clamped to the fixed list). */
|
||||
export function currentStep(s: Extract<RegistratieState, { tag: 'Invullen' }>): StepId {
|
||||
@@ -112,11 +118,14 @@ function validateStep(step: StepId, d: Draft, upload: UploadState): Result<Error
|
||||
const errors: Errors = {};
|
||||
switch (step) {
|
||||
case 'adres': {
|
||||
if (!d.straat || d.straat.trim() === '') errors.straat = $localize`:@@validation.straat2:Vul een straat en huisnummer in.`;
|
||||
if (!d.straat || d.straat.trim() === '')
|
||||
errors.straat = $localize`:@@validation.straat2:Vul een straat en huisnummer in.`;
|
||||
const pc = parsePostcode(d.postcode ?? '');
|
||||
if (!pc.ok) errors.postcode = pc.error;
|
||||
if (!d.woonplaats || d.woonplaats.trim() === '') errors.woonplaats = $localize`:@@validation.woonplaats:Vul een woonplaats in.`;
|
||||
if (!d.correspondentie) errors.correspondentie = $localize`:@@validation.maakKeuze:Maak een keuze.`;
|
||||
if (!d.woonplaats || d.woonplaats.trim() === '')
|
||||
errors.woonplaats = $localize`:@@validation.woonplaats:Vul een woonplaats in.`;
|
||||
if (!d.correspondentie)
|
||||
errors.correspondentie = $localize`:@@validation.maakKeuze:Maak een keuze.`;
|
||||
// E-mail is only required when 'email' is the chosen channel.
|
||||
if (d.correspondentie === 'email') {
|
||||
const e = parseEmail(d.email ?? '');
|
||||
@@ -135,7 +144,8 @@ function validateStep(step: StepId, d: Draft, upload: UploadState): Result<Error
|
||||
// they're answered.
|
||||
const open: Record<string, string> = {};
|
||||
for (const id of d.vraagIds ?? []) {
|
||||
if (!(d.antwoorden[id] ?? '').trim()) open[id] = $localize`:@@validation.beantwoordVraag:Beantwoord deze vraag.`;
|
||||
if (!(d.antwoorden[id] ?? '').trim())
|
||||
open[id] = $localize`:@@validation.beantwoordVraag:Beantwoord deze vraag.`;
|
||||
}
|
||||
if (Object.keys(open).length > 0) errors.antwoorden = open;
|
||||
// Required documents for this wizard attach to the beroep step (inline upload).
|
||||
@@ -186,7 +196,8 @@ export function setField(s: RegistratieState, key: DraftField, value: string): R
|
||||
if (s.tag !== 'Invullen') return s;
|
||||
const draft: Draft = { ...s.draft, [key]: value };
|
||||
// Editing an address field means the user owns it now — not the BRP copy.
|
||||
if (key === 'straat' || key === 'postcode' || key === 'woonplaats') draft.adresHerkomst = 'handmatig';
|
||||
if (key === 'straat' || key === 'postcode' || key === 'woonplaats')
|
||||
draft.adresHerkomst = 'handmatig';
|
||||
return { ...s, draft };
|
||||
}
|
||||
|
||||
@@ -196,16 +207,30 @@ export function setCorrespondentie(s: RegistratieState, value: Correspondentie):
|
||||
}
|
||||
|
||||
/** Prefill the address from a BRP lookup and flag its origin (PRD §7). */
|
||||
export function prefillAdres(s: RegistratieState, straat: string, postcode: string, woonplaats: string): RegistratieState {
|
||||
export function prefillAdres(
|
||||
s: RegistratieState,
|
||||
straat: string,
|
||||
postcode: string,
|
||||
woonplaats: string,
|
||||
): RegistratieState {
|
||||
if (s.tag !== 'Invullen') return s;
|
||||
return { ...s, draft: { ...s.draft, straat, postcode, woonplaats, adresHerkomst: 'brp' } };
|
||||
}
|
||||
|
||||
/** Pick a DUO diploma; the beroep is derived from it and the applicable policy
|
||||
questions (`vraagIds`) come with it (both server-computed, passed in). */
|
||||
export function kiesDiploma(s: RegistratieState, diplomaId: string, beroep: string, vraagIds: string[]): RegistratieState {
|
||||
export function kiesDiploma(
|
||||
s: RegistratieState,
|
||||
diplomaId: string,
|
||||
beroep: string,
|
||||
vraagIds: string[],
|
||||
): RegistratieState {
|
||||
if (s.tag !== 'Invullen') return s;
|
||||
return { ...s, draft: { ...s.draft, diplomaId, beroep, vraagIds, diplomaHerkomst: 'duo' }, errors: {} };
|
||||
return {
|
||||
...s,
|
||||
draft: { ...s.draft, diplomaId, beroep, vraagIds, diplomaHerkomst: 'duo' },
|
||||
errors: {},
|
||||
};
|
||||
}
|
||||
|
||||
/** Switch to manual diploma entry: the diploma isn't in DUO, so the MAXIMAL
|
||||
@@ -213,7 +238,17 @@ export function kiesDiploma(s: RegistratieState, diplomaId: string, beroep: stri
|
||||
beroep is declared separately (declareerBeroep). */
|
||||
export function kiesHandmatig(s: RegistratieState, vraagIds: string[]): RegistratieState {
|
||||
if (s.tag !== 'Invullen') return s;
|
||||
return { ...s, draft: { ...s.draft, diplomaId: 'handmatig', beroep: undefined, vraagIds, diplomaHerkomst: 'handmatig' }, errors: {} };
|
||||
return {
|
||||
...s,
|
||||
draft: {
|
||||
...s.draft,
|
||||
diplomaId: 'handmatig',
|
||||
beroep: undefined,
|
||||
vraagIds,
|
||||
diplomaHerkomst: 'handmatig',
|
||||
},
|
||||
errors: {},
|
||||
};
|
||||
}
|
||||
|
||||
/** Declare the beroep for a manually-entered diploma (chosen from a fixed list). */
|
||||
@@ -260,7 +295,9 @@ export function upload(s: RegistratieState, msg: UploadMsg): RegistratieState {
|
||||
|
||||
export function resolve(s: RegistratieState, r: Result<string, string>): RegistratieState {
|
||||
if (s.tag !== 'Indienen') return s;
|
||||
return r.ok ? { tag: 'Ingediend', data: s.data, referentie: r.value } : { tag: 'Mislukt', data: s.data, error: r.error };
|
||||
return r.ok
|
||||
? { tag: 'Ingediend', data: s.data, referentie: r.value }
|
||||
: { tag: 'Mislukt', data: s.data, error: r.error };
|
||||
}
|
||||
|
||||
export type RegistratieMsg =
|
||||
@@ -308,7 +345,9 @@ export function reduce(s: RegistratieState, m: RegistratieMsg): RegistratieState
|
||||
case 'Retry':
|
||||
return s.tag === 'Mislukt' ? { tag: 'Indienen', data: s.data } : s;
|
||||
case 'SubmitConfirmed':
|
||||
return s.tag === 'Indienen' ? { tag: 'Ingediend', data: s.data, referentie: m.referentie } : s;
|
||||
return s.tag === 'Indienen'
|
||||
? { tag: 'Ingediend', data: s.data, referentie: m.referentie }
|
||||
: s;
|
||||
case 'SubmitFailed':
|
||||
return s.tag === 'Indienen' ? { tag: 'Mislukt', data: s.data, error: m.error } : s;
|
||||
case 'Upload':
|
||||
|
||||
@@ -3,8 +3,12 @@ import { Registration } from './registration';
|
||||
import { isHerregistratieEligible, statusColor } from './registration.policy';
|
||||
|
||||
const reg = (status: Registration['status']): Registration => ({
|
||||
bigNummer: '19012345601', naam: 'Test', beroep: 'Arts',
|
||||
registratiedatum: '2012-09-01', geboortedatum: '1985-03-14', status,
|
||||
bigNummer: '19012345601',
|
||||
naam: 'Test',
|
||||
beroep: 'Arts',
|
||||
registratiedatum: '2012-09-01',
|
||||
geboortedatum: '1985-03-14',
|
||||
status,
|
||||
});
|
||||
|
||||
describe('registration.policy', () => {
|
||||
@@ -15,8 +19,18 @@ describe('registration.policy', () => {
|
||||
});
|
||||
|
||||
it('struck-off / suspended registrations are never eligible', () => {
|
||||
expect(isHerregistratieEligible(reg({ tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'x' }), new Date('2027-01-01'))).toBe(false);
|
||||
expect(isHerregistratieEligible(reg({ tag: 'Geschorst', geschorstTot: '2026-12-31', reden: 'x' }), new Date('2027-01-01'))).toBe(false);
|
||||
expect(
|
||||
isHerregistratieEligible(
|
||||
reg({ tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'x' }),
|
||||
new Date('2027-01-01'),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isHerregistratieEligible(
|
||||
reg({ tag: 'Geschorst', geschorstTot: '2026-12-31', reden: 'x' }),
|
||||
new Date('2027-01-01'),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('statusColor is total over the union', () => {
|
||||
|
||||
@@ -38,7 +38,11 @@ export function herregistratieDeadline(reg: Registration): Date | null {
|
||||
SERVER-OWNED RULE: this now runs on the backend (BFF), which ships the result
|
||||
as `decisions.eligibleForHerregistratie` in the dashboard view. Kept here as
|
||||
the reference implementation + unit test; the frontend no longer calls it. */
|
||||
export function isHerregistratieEligible(reg: Registration, today: Date, windowMonths = 12): boolean {
|
||||
export function isHerregistratieEligible(
|
||||
reg: Registration,
|
||||
today: Date,
|
||||
windowMonths = 12,
|
||||
): boolean {
|
||||
const deadline = herregistratieDeadline(reg);
|
||||
if (!deadline) return false;
|
||||
const windowStart = new Date(deadline);
|
||||
|
||||
@@ -24,7 +24,10 @@ describe('tasksFromProfile', () => {
|
||||
});
|
||||
|
||||
it('surfaces a notice for a suspended registration (independent of eligibility)', () => {
|
||||
const reg: Registration = { ...base, status: { tag: 'Geschorst', geschorstTot: '2027-01-01', reden: 'Onderzoek' } };
|
||||
const reg: Registration = {
|
||||
...base,
|
||||
status: { tag: 'Geschorst', geschorstTot: '2027-01-01', reden: 'Onderzoek' },
|
||||
};
|
||||
const tasks = tasksFromProfile(reg, false);
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].title).toContain('geschorst');
|
||||
@@ -32,7 +35,10 @@ describe('tasksFromProfile', () => {
|
||||
});
|
||||
|
||||
it('surfaces a notice for a struck-off registration', () => {
|
||||
const reg: Registration = { ...base, status: { tag: 'Doorgehaald', doorgehaaldOp: '2025-01-01', reden: 'Op eigen verzoek' } };
|
||||
const reg: Registration = {
|
||||
...base,
|
||||
status: { tag: 'Doorgehaald', doorgehaaldOp: '2025-01-01', reden: 'Op eigen verzoek' },
|
||||
};
|
||||
const tasks = tasksFromProfile(reg, false);
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].title).toContain('doorgehaald');
|
||||
|
||||
@@ -22,7 +22,10 @@ function formatNL(d: Date): string {
|
||||
* it does not recompute the rule (ADR-0001). The deadline is still formatted
|
||||
* client-side for the task copy (presentation, not a rule).
|
||||
*/
|
||||
export function tasksFromProfile(reg: Registration, eligibleForHerregistratie: boolean): PortalTask[] {
|
||||
export function tasksFromProfile(
|
||||
reg: Registration,
|
||||
eligibleForHerregistratie: boolean,
|
||||
): PortalTask[] {
|
||||
const tasks: PortalTask[] = [];
|
||||
|
||||
if (eligibleForHerregistratie) {
|
||||
|
||||
@@ -5,5 +5,7 @@ export type BigNummer = Brand<string, 'BigNummer'>;
|
||||
|
||||
export function parseBigNummer(raw: string): Result<string, BigNummer> {
|
||||
const t = raw.trim();
|
||||
return /^\d{11}$/.test(t) ? ok(t as BigNummer) : err($localize`:@@validation.bigNummer:Een BIG-nummer bestaat uit 11 cijfers.`);
|
||||
return /^\d{11}$/.test(t)
|
||||
? ok(t as BigNummer)
|
||||
: err($localize`:@@validation.bigNummer:Een BIG-nummer bestaat uit 11 cijfers.`);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ export function parseEmail(raw: string): Result<string, Email> {
|
||||
// Deliberately lax: a single @ with non-empty, dot-bearing parts. Good enough
|
||||
// for instant feedback; the server re-validates.
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)) {
|
||||
return err($localize`:@@validation.email:Voer een geldig e-mailadres in, bijv. naam@voorbeeld.nl.`);
|
||||
return err(
|
||||
$localize`:@@validation.email:Voer een geldig e-mailadres in, bijv. naam@voorbeeld.nl.`,
|
||||
);
|
||||
}
|
||||
return ok(t as Email);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@ import { parseUren } from './uren';
|
||||
|
||||
describe('parseUren', () => {
|
||||
it('accepts non-negative whole numbers, including 0', () => {
|
||||
for (const [raw, n] of [['0', 0], [' 40 ', 40], ['1000', 1000]] as const) {
|
||||
for (const [raw, n] of [
|
||||
['0', 0],
|
||||
[' 40 ', 40],
|
||||
['1000', 1000],
|
||||
] as const) {
|
||||
const r = parseUren(raw);
|
||||
expect(r.ok).toBe(true);
|
||||
if (r.ok) expect(r.value).toBe(n);
|
||||
|
||||
Reference in New Issue
Block a user