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:
@@ -1,7 +1,10 @@
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { RemoteData } from '@shared/application/remote-data';
|
||||
import { Aanvraag } from '@registratie/domain/aanvraag';
|
||||
import { ApplicationsAdapter, parseApplications } from '@registratie/infrastructure/applications.adapter';
|
||||
import {
|
||||
ApplicationsAdapter,
|
||||
parseApplications,
|
||||
} from '@registratie/infrastructure/applications.adapter';
|
||||
|
||||
type Err = Error | undefined;
|
||||
|
||||
@@ -31,7 +34,11 @@ export class ApplicationsStore {
|
||||
if (this.state().tag !== 'Success') this.state.set({ tag: 'Loading' });
|
||||
try {
|
||||
const parsed = parseApplications(await this.adapter.list());
|
||||
this.state.set(parsed.ok ? { tag: 'Success', value: parsed.value } : { tag: 'Failure', error: new Error(parsed.error) });
|
||||
this.state.set(
|
||||
parsed.ok
|
||||
? { tag: 'Success', value: parsed.value }
|
||||
: { tag: 'Failure', error: new Error(parsed.error) },
|
||||
);
|
||||
} catch (e) {
|
||||
this.state.set({ tag: 'Failure', error: e as Error });
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ import { Aantekening } from '../domain/registration';
|
||||
import { BigProfile } from '../domain/big-profile';
|
||||
import { HerregistratieDecisions } from '../contracts/dashboard-view.dto';
|
||||
import { BigRegisterAdapter } from '../infrastructure/big-register.adapter';
|
||||
import { DashboardView, DashboardViewAdapter, parseDashboardView } from '../infrastructure/dashboard-view.adapter';
|
||||
import {
|
||||
DashboardView,
|
||||
DashboardViewAdapter,
|
||||
parseDashboardView,
|
||||
} from '../infrastructure/dashboard-view.adapter';
|
||||
|
||||
type Err = Error | undefined;
|
||||
|
||||
@@ -32,14 +36,20 @@ export class BigProfileStore {
|
||||
const rd = fromResource(this.viewRes);
|
||||
if (rd.tag !== 'Success') return rd;
|
||||
const parsed = parseDashboardView(rd.value);
|
||||
return parsed.ok ? { tag: 'Success', value: parsed.value } : { tag: 'Failure', error: new Error(parsed.error) };
|
||||
return parsed.ok
|
||||
? { tag: 'Success', value: parsed.value }
|
||||
: { tag: 'Failure', error: new Error(parsed.error) };
|
||||
});
|
||||
|
||||
/** Registration + person, from the single aggregated call. */
|
||||
readonly profile = computed<RemoteData<Err, BigProfile>>(() => map(this.view(), (v) => v.profile));
|
||||
readonly profile = computed<RemoteData<Err, BigProfile>>(() =>
|
||||
map(this.view(), (v) => v.profile),
|
||||
);
|
||||
|
||||
/** Server-computed decisions (e.g. herregistratie eligibility) — rendered, not recomputed. */
|
||||
readonly decisions = computed<RemoteData<Err, HerregistratieDecisions>>(() => map(this.view(), (v) => v.decisions));
|
||||
readonly decisions = computed<RemoteData<Err, HerregistratieDecisions>>(() =>
|
||||
map(this.view(), (v) => v.decisions),
|
||||
);
|
||||
|
||||
/** Specialisms/notes stay a separate stream (they have their own empty state). */
|
||||
readonly aantekeningen = computed<RemoteData<Err, Aantekening[]>>(() => {
|
||||
|
||||
@@ -2,9 +2,15 @@ import { DestroyRef, effect, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Result } from '@shared/kernel/fp';
|
||||
import { runSubmit, SUBMIT_FAILED } from '@shared/application/submit';
|
||||
import type { SubmitApplicationRequest, SubmitApplicationResponse } from '@shared/infrastructure/api-client';
|
||||
import type {
|
||||
SubmitApplicationRequest,
|
||||
SubmitApplicationResponse,
|
||||
} from '@shared/infrastructure/api-client';
|
||||
import { AanvraagType } from '@registratie/domain/aanvraag';
|
||||
import { ApplicationsAdapter, parseApplications } from '@registratie/infrastructure/applications.adapter';
|
||||
import {
|
||||
ApplicationsAdapter,
|
||||
parseApplications,
|
||||
} from '@registratie/infrastructure/applications.adapter';
|
||||
|
||||
/** What a wizard persists per step: the opaque machine snapshot + progress + docs. */
|
||||
export interface DraftSnapshot {
|
||||
@@ -59,7 +65,12 @@ export function createDraftSync(deps: DraftSyncDeps) {
|
||||
ensuring ??= adapter.create(deps.type).then((newId) => {
|
||||
id = newId;
|
||||
// Stamp the id into the URL (no navigation) so a reload resumes this Concept.
|
||||
void router!.navigate([], { relativeTo: route!, queryParams: { aanvraag: newId }, queryParamsHandling: 'merge', replaceUrl: true });
|
||||
void router!.navigate([], {
|
||||
relativeTo: route!,
|
||||
queryParams: { aanvraag: newId },
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
return newId;
|
||||
});
|
||||
return ensuring;
|
||||
@@ -78,7 +89,12 @@ export function createDraftSync(deps: DraftSyncDeps) {
|
||||
const snap = deps.snapshot();
|
||||
if (!snap) return;
|
||||
const theId = await ensureId();
|
||||
await adapter.syncDraft(theId, { draft: snap.draft, stepIndex: snap.stepIndex, stepCount: snap.stepCount, documentIds: snap.documentIds });
|
||||
await adapter.syncDraft(theId, {
|
||||
draft: snap.draft,
|
||||
stepIndex: snap.stepIndex,
|
||||
stepCount: snap.stepCount,
|
||||
documentIds: snap.documentIds,
|
||||
});
|
||||
};
|
||||
|
||||
// One effect watches the snapshot; each change resets a debounce timer. The timer's
|
||||
@@ -117,7 +133,9 @@ export function createDraftSync(deps: DraftSyncDeps) {
|
||||
const findConcept = async (): Promise<string | undefined> => {
|
||||
try {
|
||||
const parsed = parseApplications(await adapter.list());
|
||||
return parsed.ok ? parsed.value.find((a) => a.type === deps.type && a.status.tag === 'Concept')?.id : undefined;
|
||||
return parsed.ok
|
||||
? parsed.value.find((a) => a.type === deps.type && a.status.tag === 'Concept')?.id
|
||||
: undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
@@ -143,7 +161,12 @@ export function createDraftSync(deps: DraftSyncDeps) {
|
||||
if (existing) {
|
||||
await load(existing);
|
||||
// Stamp the id into the URL so a reload resumes the same Concept.
|
||||
void router!.navigate([], { relativeTo: route!, queryParams: { aanvraag: existing }, queryParamsHandling: 'merge', replaceUrl: true });
|
||||
void router!.navigate([], {
|
||||
relativeTo: route!,
|
||||
queryParams: { aanvraag: existing },
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
applyResume(null);
|
||||
@@ -169,7 +192,13 @@ export function createDraftSync(deps: DraftSyncDeps) {
|
||||
id = undefined;
|
||||
ensuring = undefined;
|
||||
}
|
||||
if (active()) void router!.navigate([], { relativeTo: route!, queryParams: { aanvraag: null }, queryParamsHandling: 'merge', replaceUrl: true });
|
||||
if (active())
|
||||
void router!.navigate([], {
|
||||
relativeTo: route!,
|
||||
queryParams: { aanvraag: null },
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,19 +36,23 @@ export class RegistratieLookupStore {
|
||||
|
||||
/** The address to prefill the draft with, once BRP resolves with a found address;
|
||||
null otherwise (loading, error, no address, malformed). */
|
||||
readonly prefillAdres = computed<{ straat: string; postcode: string; woonplaats: string } | null>(() => {
|
||||
const json = this.adresRes.value();
|
||||
if (json === undefined) return null;
|
||||
const parsed = parseBrpAddress(json);
|
||||
return parsed.ok && parsed.value.gevonden && parsed.value.adres ? parsed.value.adres : null;
|
||||
});
|
||||
readonly prefillAdres = computed<{ straat: string; postcode: string; woonplaats: string } | null>(
|
||||
() => {
|
||||
const json = this.adresRes.value();
|
||||
if (json === undefined) return null;
|
||||
const parsed = parseBrpAddress(json);
|
||||
return parsed.ok && parsed.value.gevonden && parsed.value.adres ? parsed.value.adres : null;
|
||||
},
|
||||
);
|
||||
|
||||
/** The DUO lookup (diplomas + manual fallback), validated at the trust boundary. */
|
||||
readonly duoLookup = computed<RemoteData<Err, DuoLookupDto>>(() => {
|
||||
const rd = fromResource(this.diplomasRes);
|
||||
if (rd.tag !== 'Success') return rd;
|
||||
const parsed = parseDuoLookup(rd.value);
|
||||
return parsed.ok ? { tag: 'Success', value: parsed.value } : { tag: 'Failure', error: new Error(parsed.error) };
|
||||
return parsed.ok
|
||||
? { tag: 'Success', value: parsed.value }
|
||||
: { tag: 'Failure', error: new Error(parsed.error) };
|
||||
});
|
||||
|
||||
/** Reload the BRP lookup (e.g. when the wizard restarts) so the address re-prefills. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseAanvraagStatus, parseApplicationSummary, parseApplications, parseApplicationDetail } from './applications.adapter';
|
||||
import {
|
||||
parseAanvraagStatus,
|
||||
parseApplicationSummary,
|
||||
parseApplications,
|
||||
parseApplicationDetail,
|
||||
} from './applications.adapter';
|
||||
|
||||
const concept = { id: 'a1', type: 'registratie', status: { tag: 'Concept', stepIndex: 1, stepCount: 4 }, documentIds: [], createdAt: '2026-07-01T10:00:00Z', updatedAt: '2026-07-01T10:05:00Z' };
|
||||
const concept = {
|
||||
id: 'a1',
|
||||
type: 'registratie',
|
||||
status: { tag: 'Concept', stepIndex: 1, stepCount: 4 },
|
||||
documentIds: [],
|
||||
createdAt: '2026-07-01T10:00:00Z',
|
||||
updatedAt: '2026-07-01T10:05:00Z',
|
||||
};
|
||||
|
||||
describe('parseAanvraagStatus', () => {
|
||||
it('parses each tag with its required fields', () => {
|
||||
expect(parseAanvraagStatus({ tag: 'Concept', stepIndex: 2, stepCount: 4 })).toEqual({ ok: true, value: { tag: 'Concept', stepIndex: 2, stepCount: 4 } });
|
||||
expect(parseAanvraagStatus({ tag: 'InBehandeling', referentie: 'BIG-1', manual: true }).ok).toBe(true);
|
||||
expect(parseAanvraagStatus({ tag: 'Concept', stepIndex: 2, stepCount: 4 })).toEqual({
|
||||
ok: true,
|
||||
value: { tag: 'Concept', stepIndex: 2, stepCount: 4 },
|
||||
});
|
||||
expect(
|
||||
parseAanvraagStatus({ tag: 'InBehandeling', referentie: 'BIG-1', manual: true }).ok,
|
||||
).toBe(true);
|
||||
expect(parseAanvraagStatus({ tag: 'Goedgekeurd', referentie: 'BIG-1' }).ok).toBe(true);
|
||||
expect(parseAanvraagStatus({ tag: 'Afgewezen', referentie: 'BIG-1', reden: 'geen uren' }).ok).toBe(true);
|
||||
expect(
|
||||
parseAanvraagStatus({ tag: 'Afgewezen', referentie: 'BIG-1', reden: 'geen uren' }).ok,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a missing status, unknown tag, and wrong-typed fields', () => {
|
||||
|
||||
@@ -9,7 +9,12 @@ import {
|
||||
SubmitApplicationRequest,
|
||||
SubmitApplicationResponse,
|
||||
} from '@shared/infrastructure/api-client';
|
||||
import { Aanvraag, AanvraagDetail, AanvraagStatus, AanvraagType } from '@registratie/domain/aanvraag';
|
||||
import {
|
||||
Aanvraag,
|
||||
AanvraagDetail,
|
||||
AanvraagStatus,
|
||||
AanvraagType,
|
||||
} from '@registratie/domain/aanvraag';
|
||||
|
||||
/**
|
||||
* Infrastructure adapter for the backend-owned Aanvraag aggregate — the only place
|
||||
@@ -54,20 +59,25 @@ export class ApplicationsAdapter {
|
||||
const AANVRAAG_TYPES: readonly string[] = ['registratie', 'herregistratie', 'intake'];
|
||||
|
||||
/** Trust-boundary parse of the status union — the tag drives which fields must exist. */
|
||||
export function parseAanvraagStatus(s: AanvraagStatusDto | undefined): Result<string, AanvraagStatus> {
|
||||
export function parseAanvraagStatus(
|
||||
s: AanvraagStatusDto | undefined,
|
||||
): Result<string, AanvraagStatus> {
|
||||
if (!s || typeof s.tag !== 'string') return err('aanvraag: missing status');
|
||||
switch (s.tag) {
|
||||
case 'Concept':
|
||||
if (typeof s.stepIndex !== 'number' || typeof s.stepCount !== 'number') return err('aanvraag: bad Concept status');
|
||||
if (typeof s.stepIndex !== 'number' || typeof s.stepCount !== 'number')
|
||||
return err('aanvraag: bad Concept status');
|
||||
return ok({ tag: 'Concept', stepIndex: s.stepIndex, stepCount: s.stepCount });
|
||||
case 'InBehandeling':
|
||||
if (typeof s.referentie !== 'string' || typeof s.manual !== 'boolean') return err('aanvraag: bad InBehandeling status');
|
||||
if (typeof s.referentie !== 'string' || typeof s.manual !== 'boolean')
|
||||
return err('aanvraag: bad InBehandeling status');
|
||||
return ok({ tag: 'InBehandeling', referentie: s.referentie, manual: s.manual });
|
||||
case 'Goedgekeurd':
|
||||
if (typeof s.referentie !== 'string') return err('aanvraag: bad Goedgekeurd status');
|
||||
return ok({ tag: 'Goedgekeurd', referentie: s.referentie });
|
||||
case 'Afgewezen':
|
||||
if (typeof s.referentie !== 'string' || typeof s.reden !== 'string') return err('aanvraag: bad Afgewezen status');
|
||||
if (typeof s.referentie !== 'string' || typeof s.reden !== 'string')
|
||||
return err('aanvraag: bad Afgewezen status');
|
||||
return ok({ tag: 'Afgewezen', referentie: s.referentie, reden: s.reden });
|
||||
default:
|
||||
return err(`aanvraag: unknown status tag ${s.tag}`);
|
||||
@@ -76,8 +86,10 @@ export function parseAanvraagStatus(s: AanvraagStatusDto | undefined): Result<st
|
||||
|
||||
function parseCommon(dto: ApplicationSummaryDto): Result<string, Aanvraag> {
|
||||
if (typeof dto.id !== 'string') return err('aanvraag: missing id');
|
||||
if (typeof dto.type !== 'string' || !AANVRAAG_TYPES.includes(dto.type)) return err(`aanvraag: bad type ${dto.type}`);
|
||||
if (typeof dto.createdAt !== 'string' || typeof dto.updatedAt !== 'string') return err('aanvraag: missing timestamps');
|
||||
if (typeof dto.type !== 'string' || !AANVRAAG_TYPES.includes(dto.type))
|
||||
return err(`aanvraag: bad type ${dto.type}`);
|
||||
if (typeof dto.createdAt !== 'string' || typeof dto.updatedAt !== 'string')
|
||||
return err('aanvraag: missing timestamps');
|
||||
const status = parseAanvraagStatus(dto.status);
|
||||
if (!status.ok) return status;
|
||||
return ok({
|
||||
|
||||
@@ -22,5 +22,9 @@ export class BigRegisterAdapter {
|
||||
|
||||
/** Map the wire DTO (all fields optional) onto our domain type. */
|
||||
function toAantekening(n: AantekeningDto): Aantekening {
|
||||
return { type: n.type as AantekeningType, omschrijving: n.omschrijving ?? '', datum: n.datum ?? '' };
|
||||
return {
|
||||
type: n.type as AantekeningType,
|
||||
omschrijving: n.omschrijving ?? '',
|
||||
datum: n.datum ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { parseBrpAddress } from './brp.adapter';
|
||||
|
||||
describe('parseBrpAddress (trust boundary)', () => {
|
||||
it('accepts a found address', () => {
|
||||
const r = parseBrpAddress({ gevonden: true, adres: { straat: 'Lange Voorhout 9', postcode: '2514 EA', woonplaats: 'Den Haag' } });
|
||||
const r = parseBrpAddress({
|
||||
gevonden: true,
|
||||
adres: { straat: 'Lange Voorhout 9', postcode: '2514 EA', woonplaats: 'Den Haag' },
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
if (r.ok) expect(r.value.adres?.postcode).toBe('2514 EA');
|
||||
});
|
||||
|
||||
@@ -27,7 +27,12 @@ export function parseBrpAddress(json: unknown): Result<string, BrpAddressDto> {
|
||||
if (typeof dto.gevonden !== 'boolean') return err('brp-address: missing/invalid gevonden');
|
||||
if (dto.gevonden) {
|
||||
const a = dto.adres;
|
||||
if (!a || typeof a.straat !== 'string' || typeof a.postcode !== 'string' || typeof a.woonplaats !== 'string') {
|
||||
if (
|
||||
!a ||
|
||||
typeof a.straat !== 'string' ||
|
||||
typeof a.postcode !== 'string' ||
|
||||
typeof a.woonplaats !== 'string'
|
||||
) {
|
||||
return err('brp-address: missing/invalid adres');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ const valid = {
|
||||
geboortedatum: '1985-03-14',
|
||||
status: { tag: 'Geregistreerd', herregistratieDatum: '2027-03-01' },
|
||||
},
|
||||
person: { naam: 'Dr. A. de Vries', geboortedatum: '1985-03-14', adres: { straat: 'X 1', postcode: '2514 EA', woonplaats: 'Den Haag' } },
|
||||
person: {
|
||||
naam: 'Dr. A. de Vries',
|
||||
geboortedatum: '1985-03-14',
|
||||
adres: { straat: 'X 1', postcode: '2514 EA', woonplaats: 'Den Haag' },
|
||||
},
|
||||
decisions: { eligibleForHerregistratie: true, herregistratieReason: 'within window' },
|
||||
};
|
||||
|
||||
@@ -27,6 +31,8 @@ describe('parseDashboardView (trust boundary)', () => {
|
||||
it('rejects malformed responses instead of trusting them', () => {
|
||||
expect(parseDashboardView(null).ok).toBe(false);
|
||||
expect(parseDashboardView({ ...valid, registration: undefined }).ok).toBe(false);
|
||||
expect(parseDashboardView({ ...valid, decisions: { eligibleForHerregistratie: 'yes' } }).ok).toBe(false);
|
||||
expect(
|
||||
parseDashboardView({ ...valid, decisions: { eligibleForHerregistratie: 'yes' } }).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Injectable, inject, resource } from '@angular/core';
|
||||
import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import { DashboardViewDto, HerregistratieDecisions } from '@registratie/contracts/dashboard-view.dto';
|
||||
import {
|
||||
DashboardViewDto,
|
||||
HerregistratieDecisions,
|
||||
} from '@registratie/contracts/dashboard-view.dto';
|
||||
import { Registration } from '@registratie/domain/registration';
|
||||
import { Person } from '@registratie/domain/person';
|
||||
import { BigProfile } from '@registratie/domain/big-profile';
|
||||
@@ -49,7 +52,12 @@ export function parseDashboardView(json: unknown): Result<string, DashboardView>
|
||||
const dto = json as Partial<DashboardViewDto>;
|
||||
|
||||
const reg = dto.registration;
|
||||
if (!reg || typeof reg.bigNummer !== 'string' || !reg.status || typeof reg.status.tag !== 'string') {
|
||||
if (
|
||||
!reg ||
|
||||
typeof reg.bigNummer !== 'string' ||
|
||||
!reg.status ||
|
||||
typeof reg.status.tag !== 'string'
|
||||
) {
|
||||
return err('dashboard-view: missing/invalid registration');
|
||||
}
|
||||
const person = dto.person;
|
||||
@@ -72,10 +80,17 @@ export function parseDashboardView(json: unknown): Result<string, DashboardView>
|
||||
geboortedatum: reg.geboortedatum,
|
||||
status: reg.status,
|
||||
};
|
||||
const persoon: Person = { naam: person.naam, geboortedatum: person.geboortedatum, adres: person.adres };
|
||||
const persoon: Person = {
|
||||
naam: person.naam,
|
||||
geboortedatum: person.geboortedatum,
|
||||
adres: person.adres,
|
||||
};
|
||||
|
||||
return ok({
|
||||
profile: { registration, person: persoon },
|
||||
decisions: { eligibleForHerregistratie: d.eligibleForHerregistratie, herregistratieReason: d.herregistratieReason },
|
||||
decisions: {
|
||||
eligibleForHerregistratie: d.eligibleForHerregistratie,
|
||||
herregistratieReason: d.herregistratieReason,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,8 +3,22 @@ import { parseDuoLookup } from './duo.adapter';
|
||||
|
||||
const valid = {
|
||||
diplomas: [
|
||||
{ id: 'd1', naam: 'Geneeskunde', instelling: 'Universiteit Leiden', jaar: 2011, beroep: 'Arts', policyQuestions: [] },
|
||||
{ id: 'd2', naam: 'Medicine', instelling: 'University of Edinburgh', jaar: 2013, beroep: 'Arts', policyQuestions: [{ id: 'nl-taal', vraag: 'Toon taalvaardigheid', type: 'ja-nee' }] },
|
||||
{
|
||||
id: 'd1',
|
||||
naam: 'Geneeskunde',
|
||||
instelling: 'Universiteit Leiden',
|
||||
jaar: 2011,
|
||||
beroep: 'Arts',
|
||||
policyQuestions: [],
|
||||
},
|
||||
{
|
||||
id: 'd2',
|
||||
naam: 'Medicine',
|
||||
instelling: 'University of Edinburgh',
|
||||
jaar: 2013,
|
||||
beroep: 'Arts',
|
||||
policyQuestions: [{ id: 'nl-taal', vraag: 'Toon taalvaardigheid', type: 'ja-nee' }],
|
||||
},
|
||||
],
|
||||
handmatig: {
|
||||
beroepen: ['Arts', 'Verpleegkundige'],
|
||||
@@ -34,6 +48,14 @@ describe('parseDuoLookup (trust boundary)', () => {
|
||||
expect(parseDuoLookup({}).ok).toBe(false); // no diplomas
|
||||
expect(parseDuoLookup({ diplomas: [] }).ok).toBe(false); // no handmatig
|
||||
expect(parseDuoLookup({ diplomas: [{ id: 'd1' }], handmatig: valid.handmatig }).ok).toBe(false); // bad diploma
|
||||
expect(parseDuoLookup({ diplomas: [], handmatig: { beroepen: ['Arts'], policyQuestions: [{ id: 'x', vraag: 'y', type: 'bogus' }] } }).ok).toBe(false); // bad question type
|
||||
expect(
|
||||
parseDuoLookup({
|
||||
diplomas: [],
|
||||
handmatig: {
|
||||
beroepen: ['Arts'],
|
||||
policyQuestions: [{ id: 'x', vraag: 'y', type: 'bogus' }],
|
||||
},
|
||||
}).ok,
|
||||
).toBe(false); // bad question type
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Injectable, inject, resource } from '@angular/core';
|
||||
import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import { DuoLookupDto, DuoDiplomaDto, PolicyQuestionDto, ManualDiplomaPolicyDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||
import {
|
||||
DuoLookupDto,
|
||||
DuoDiplomaDto,
|
||||
PolicyQuestionDto,
|
||||
ManualDiplomaPolicyDto,
|
||||
} from '@registratie/contracts/duo-diplomas.dto';
|
||||
import { ApiClient } from '@shared/infrastructure/api-client';
|
||||
|
||||
/**
|
||||
@@ -25,7 +30,12 @@ function parseQuestions(json: unknown): PolicyQuestionDto[] | null {
|
||||
for (const q of json) {
|
||||
if (typeof q !== 'object' || q === null) return null;
|
||||
const p = q as Partial<PolicyQuestionDto>;
|
||||
if (typeof p.id !== 'string' || typeof p.vraag !== 'string' || (p.type !== 'ja-nee' && p.type !== 'tekst')) return null;
|
||||
if (
|
||||
typeof p.id !== 'string' ||
|
||||
typeof p.vraag !== 'string' ||
|
||||
(p.type !== 'ja-nee' && p.type !== 'tekst')
|
||||
)
|
||||
return null;
|
||||
out.push({ id: p.id, vraag: p.vraag, type: p.type });
|
||||
}
|
||||
return out;
|
||||
@@ -43,10 +53,22 @@ export function parseDuoLookup(json: unknown): Result<string, DuoLookupDto> {
|
||||
if (typeof item !== 'object' || item === null) return err('duo-lookup: invalid diploma');
|
||||
const d = item as Partial<DuoDiplomaDto>;
|
||||
const vragen = parseQuestions(d.policyQuestions);
|
||||
if (typeof d.id !== 'string' || typeof d.naam !== 'string' || typeof d.beroep !== 'string' || vragen === null) {
|
||||
if (
|
||||
typeof d.id !== 'string' ||
|
||||
typeof d.naam !== 'string' ||
|
||||
typeof d.beroep !== 'string' ||
|
||||
vragen === null
|
||||
) {
|
||||
return err('duo-lookup: missing/invalid diploma fields');
|
||||
}
|
||||
diplomas.push({ id: d.id, naam: d.naam, instelling: d.instelling ?? '', jaar: typeof d.jaar === 'number' ? d.jaar : 0, beroep: d.beroep, policyQuestions: vragen });
|
||||
diplomas.push({
|
||||
id: d.id,
|
||||
naam: d.naam,
|
||||
instelling: d.instelling ?? '',
|
||||
jaar: typeof d.jaar === 'number' ? d.jaar : 0,
|
||||
beroep: d.beroep,
|
||||
policyQuestions: vragen,
|
||||
});
|
||||
}
|
||||
|
||||
const hm = dto.handmatig;
|
||||
|
||||
@@ -13,9 +13,16 @@ import { blockActions } from '@registratie/domain/block-actions';
|
||||
@Component({
|
||||
selector: 'app-aanvraag-block',
|
||||
imports: [ButtonComponent, AlertComponent],
|
||||
styles: [`
|
||||
.actions{display:flex;align-items:center;gap:var(--rhc-space-max-md);margin-block-start:var(--rhc-space-max-sm)}
|
||||
`],
|
||||
styles: [
|
||||
`
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--rhc-space-max-md);
|
||||
margin-block-start: var(--rhc-space-max-sm);
|
||||
}
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
@if (aanvraag().status.tag === 'Concept') {
|
||||
<app-alert type="warning">
|
||||
@@ -23,10 +30,14 @@ import { blockActions } from '@registratie/domain/block-actions';
|
||||
<p>{{ conceptText() }}</p>
|
||||
<div class="actions">
|
||||
@if (actions().includes('cancel')) {
|
||||
<app-button variant="subtle" (click)="cancel.emit()" i18n="@@aanvraagBlock.verwijderen">Verwijderen</app-button>
|
||||
<app-button variant="subtle" (click)="cancel.emit()" i18n="@@aanvraagBlock.verwijderen"
|
||||
>Verwijderen</app-button
|
||||
>
|
||||
}
|
||||
@if (actions().includes('resume')) {
|
||||
<app-button (click)="resume.emit()" i18n="@@aanvraagBlock.openen">Aanvraag openen</app-button>
|
||||
<app-button (click)="resume.emit()" i18n="@@aanvraagBlock.openen"
|
||||
>Aanvraag openen</app-button
|
||||
>
|
||||
}
|
||||
</div>
|
||||
</app-alert>
|
||||
@@ -59,5 +70,7 @@ export class AanvraagBlockComponent {
|
||||
}
|
||||
|
||||
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' })
|
||||
: '';
|
||||
}
|
||||
|
||||
@@ -38,7 +38,36 @@ export const Concept: Story = {
|
||||
args: { aanvraag: { ...base, status: { tag: 'Concept', stepIndex: 1, stepCount: 3 } } },
|
||||
render: (args) => ({ props: args, template: `<app-aanvraag-block [aanvraag]="aanvraag" />` }),
|
||||
};
|
||||
export const InBehandelingAuto: Story = { args: { aanvraag: { ...base, status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false } } } };
|
||||
export const InBehandelingManual: Story = { args: { aanvraag: { ...base, type: 'registratie', status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: true } } } };
|
||||
export const Goedgekeurd: Story = { args: { aanvraag: { ...base, status: { tag: 'Goedgekeurd', referentie: 'BIG-2026-456789' } } } };
|
||||
export const Afgewezen: Story = { args: { aanvraag: { ...base, type: 'herregistratie', status: { tag: 'Afgewezen', referentie: 'BIG-2026-456789', reden: 'Aanvraag afgewezen: geen gewerkte uren geregistreerd.' } } } };
|
||||
export const InBehandelingAuto: Story = {
|
||||
args: {
|
||||
aanvraag: {
|
||||
...base,
|
||||
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
export const InBehandelingManual: Story = {
|
||||
args: {
|
||||
aanvraag: {
|
||||
...base,
|
||||
type: 'registratie',
|
||||
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
export const Goedgekeurd: Story = {
|
||||
args: { aanvraag: { ...base, status: { tag: 'Goedgekeurd', referentie: 'BIG-2026-456789' } } },
|
||||
};
|
||||
export const Afgewezen: Story = {
|
||||
args: {
|
||||
aanvraag: {
|
||||
...base,
|
||||
type: 'herregistratie',
|
||||
status: {
|
||||
tag: 'Afgewezen',
|
||||
referentie: 'BIG-2026-456789',
|
||||
reden: 'Aanvraag afgewezen: geen gewerkte uren geregistreerd.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,14 +15,28 @@ import { detailRows } from '@registratie/domain/aanvraag-view';
|
||||
handling is future work. The dashboard "Mijn aanvragen" rows link here. */
|
||||
@Component({
|
||||
selector: 'app-aanvraag-detail-page',
|
||||
imports: [PageShellComponent, SkeletonComponent, AlertComponent, DataBlockComponent, DataRowComponent, ...ASYNC],
|
||||
imports: [
|
||||
PageShellComponent,
|
||||
SkeletonComponent,
|
||||
AlertComponent,
|
||||
DataBlockComponent,
|
||||
DataRowComponent,
|
||||
...ASYNC,
|
||||
],
|
||||
template: `
|
||||
<app-page-shell i18n-heading="@@aanvraagDetail.heading" heading="Aanvraag" backLink="/dashboard">
|
||||
<app-page-shell
|
||||
i18n-heading="@@aanvraagDetail.heading"
|
||||
heading="Aanvraag"
|
||||
backLink="/dashboard"
|
||||
>
|
||||
<app-async [data]="store.applications()">
|
||||
<ng-template appAsyncLoaded let-list>
|
||||
@let a = find($any(list));
|
||||
@if (a) {
|
||||
<app-data-block i18n-ariaLabel="@@aanvraagDetail.ariaLabel" ariaLabel="Aanvraaggegevens">
|
||||
<app-data-block
|
||||
i18n-ariaLabel="@@aanvraagDetail.ariaLabel"
|
||||
ariaLabel="Aanvraaggegevens"
|
||||
>
|
||||
@for (row of rows(a); track row.key) {
|
||||
<div app-data-row [key]="row.key" [value]="row.value"></div>
|
||||
}
|
||||
@@ -31,7 +45,9 @@ import { detailRows } from '@registratie/domain/aanvraag-view';
|
||||
De volledige afhandeling van deze aanvraag is nog niet beschikbaar in deze POC.
|
||||
</app-alert>
|
||||
} @else {
|
||||
<app-alert type="warning" i18n="@@aanvraagDetail.nietGevonden">Deze aanvraag is niet gevonden.</app-alert>
|
||||
<app-alert type="warning" i18n="@@aanvraagDetail.nietGevonden"
|
||||
>Deze aanvraag is niet gevonden.</app-alert
|
||||
>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
|
||||
@@ -19,27 +19,73 @@ export type AdresErrors = Partial<Record<keyof AdresValue, string>>;
|
||||
@Component({
|
||||
selector: 'app-address-fields',
|
||||
imports: [FormsModule, FormFieldComponent, TextInputComponent],
|
||||
styles: [`
|
||||
fieldset{border:0;margin:0;padding:0;min-inline-size:0}
|
||||
legend{padding:0;font-weight:var(--rhc-text-font-weight-semi-bold);margin-block-end:var(--rhc-space-max-md)}
|
||||
`],
|
||||
styles: [
|
||||
`
|
||||
fieldset {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
legend {
|
||||
padding: 0;
|
||||
font-weight: var(--rhc-text-font-weight-semi-bold);
|
||||
margin-block-end: var(--rhc-space-max-md);
|
||||
}
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<fieldset>
|
||||
<legend>{{ legend() }}</legend>
|
||||
<app-form-field i18n-label="@@address.straat" label="Straat en huisnummer" [fieldId]="idPrefix() + '-straat'" required [error]="errors().straat">
|
||||
<app-text-input [inputId]="idPrefix() + '-straat'" [invalid]="!!errors().straat"
|
||||
[ngModel]="value().straat" (ngModelChange)="fieldChange.emit({ key: 'straat', value: $event })"
|
||||
name="straat" [ngModelOptions]="{ standalone: true }" />
|
||||
<app-form-field
|
||||
i18n-label="@@address.straat"
|
||||
label="Straat en huisnummer"
|
||||
[fieldId]="idPrefix() + '-straat'"
|
||||
required
|
||||
[error]="errors().straat"
|
||||
>
|
||||
<app-text-input
|
||||
[inputId]="idPrefix() + '-straat'"
|
||||
[invalid]="!!errors().straat"
|
||||
[ngModel]="value().straat"
|
||||
(ngModelChange)="fieldChange.emit({ key: 'straat', value: $event })"
|
||||
name="straat"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</app-form-field>
|
||||
<app-form-field i18n-label="@@address.postcode" label="Postcode" [fieldId]="idPrefix() + '-postcode'" required [error]="errors().postcode">
|
||||
<app-text-input [inputId]="idPrefix() + '-postcode'" [invalid]="!!errors().postcode"
|
||||
[ngModel]="value().postcode" (ngModelChange)="fieldChange.emit({ key: 'postcode', value: $event })"
|
||||
name="postcode" i18n-placeholder="@@address.postcodePlaceholder" placeholder="1234 AB" [ngModelOptions]="{ standalone: true }" />
|
||||
<app-form-field
|
||||
i18n-label="@@address.postcode"
|
||||
label="Postcode"
|
||||
[fieldId]="idPrefix() + '-postcode'"
|
||||
required
|
||||
[error]="errors().postcode"
|
||||
>
|
||||
<app-text-input
|
||||
[inputId]="idPrefix() + '-postcode'"
|
||||
[invalid]="!!errors().postcode"
|
||||
[ngModel]="value().postcode"
|
||||
(ngModelChange)="fieldChange.emit({ key: 'postcode', value: $event })"
|
||||
name="postcode"
|
||||
i18n-placeholder="@@address.postcodePlaceholder"
|
||||
placeholder="1234 AB"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</app-form-field>
|
||||
<app-form-field i18n-label="@@address.woonplaats" label="Woonplaats" [fieldId]="idPrefix() + '-woonplaats'" required [error]="errors().woonplaats">
|
||||
<app-text-input [inputId]="idPrefix() + '-woonplaats'" [invalid]="!!errors().woonplaats"
|
||||
[ngModel]="value().woonplaats" (ngModelChange)="fieldChange.emit({ key: 'woonplaats', value: $event })"
|
||||
name="woonplaats" [ngModelOptions]="{ standalone: true }" />
|
||||
<app-form-field
|
||||
i18n-label="@@address.woonplaats"
|
||||
label="Woonplaats"
|
||||
[fieldId]="idPrefix() + '-woonplaats'"
|
||||
required
|
||||
[error]="errors().woonplaats"
|
||||
>
|
||||
<app-text-input
|
||||
[inputId]="idPrefix() + '-woonplaats'"
|
||||
[invalid]="!!errors().woonplaats"
|
||||
[ngModel]="value().woonplaats"
|
||||
(ngModelChange)="fieldChange.emit({ key: 'woonplaats', value: $event })"
|
||||
name="woonplaats"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</app-form-field>
|
||||
</fieldset>
|
||||
`,
|
||||
|
||||
@@ -20,12 +20,18 @@ const empty = { straat: '', postcode: '', woonplaats: '' };
|
||||
export const Default: Story = { args: { value: empty, errors: {} } };
|
||||
|
||||
export const Prefilled: Story = {
|
||||
args: { value: { straat: 'Stationsplein 1', postcode: '3511 ED', woonplaats: 'Utrecht' }, errors: {} },
|
||||
args: {
|
||||
value: { straat: 'Stationsplein 1', postcode: '3511 ED', woonplaats: 'Utrecht' },
|
||||
errors: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithErrors: Story = {
|
||||
args: {
|
||||
value: { straat: '', postcode: '12', woonplaats: '' },
|
||||
errors: { straat: 'Vul een straat en huisnummer in.', postcode: 'Voer een geldige postcode in, bijv. 1234 AB.' },
|
||||
errors: {
|
||||
straat: 'Vul een straat en huisnummer in.',
|
||||
postcode: 'Voer een geldige postcode in, bijv. 1234 AB.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,11 @@ import { FormsModule } from '@angular/forms';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { AddressFieldsComponent, AdresValue, AdresErrors } from '@registratie/ui/address-fields/address-fields.component';
|
||||
import {
|
||||
AddressFieldsComponent,
|
||||
AdresValue,
|
||||
AdresErrors,
|
||||
} from '@registratie/ui/address-fields/address-fields.component';
|
||||
import { createStore } from '@shared/application/store';
|
||||
import { whenTag } from '@shared/kernel/fp';
|
||||
import { State, Msg, initial, reduce } from '@registratie/domain/change-request.machine';
|
||||
@@ -21,25 +25,37 @@ import { createSubmitChangeRequest } from '@registratie/application/submit-chang
|
||||
template: `
|
||||
@if (state().tag === 'Submitted') {
|
||||
<app-alert type="ok" i18n="@@changeRequest.success">
|
||||
Uw adreswijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5 werkdagen bericht.
|
||||
Uw adreswijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5
|
||||
werkdagen bericht.
|
||||
</app-alert>
|
||||
<div class="app-section">
|
||||
<app-button variant="secondary" (click)="dispatch({ tag: 'Reset' })" i18n="@@changeRequest.nieuwe">Nieuwe wijziging doorgeven</app-button>
|
||||
<app-button
|
||||
variant="secondary"
|
||||
(click)="dispatch({ tag: 'Reset' })"
|
||||
i18n="@@changeRequest.nieuwe"
|
||||
>Nieuwe wijziging doorgeven</app-button
|
||||
>
|
||||
</div>
|
||||
} @else {
|
||||
<app-heading [level]="2" i18n="@@changeRequest.heading">Adreswijziging doorgeven</app-heading>
|
||||
<form (ngSubmit)="onSubmit()" class="form-horizontal app-section">
|
||||
<div class="form-header">
|
||||
<div class="form-action"><span class="meta" i18n="@@form.verplichteVelden">* verplichte velden</span></div>
|
||||
<div class="form-action">
|
||||
<span class="meta" i18n="@@form.verplichteVelden">* verplichte velden</span>
|
||||
</div>
|
||||
</div>
|
||||
<app-address-fields
|
||||
idPrefix="cr"
|
||||
[value]="adres()"
|
||||
[errors]="errors()"
|
||||
(fieldChange)="dispatch({ tag: 'SetField', key: $event.key, value: $event.value })" />
|
||||
(fieldChange)="dispatch({ tag: 'SetField', key: $event.key, value: $event.value })"
|
||||
/>
|
||||
|
||||
@if (failedError()) {
|
||||
<app-alert type="error"><ng-container i18n="@@changeRequest.failed">Het indienen is niet gelukt:</ng-container> {{ failedError() }}</app-alert>
|
||||
<app-alert type="error"
|
||||
><ng-container i18n="@@changeRequest.failed">Het indienen is niet gelukt:</ng-container>
|
||||
{{ failedError() }}</app-alert
|
||||
>
|
||||
}
|
||||
|
||||
<app-button type="submit" variant="primary" [disabled]="state().tag === 'Submitting'">
|
||||
|
||||
@@ -5,7 +5,11 @@ import { ChangeRequestFormComponent } from './change-request-form.component';
|
||||
import { provideApiClient } from '@shared/infrastructure/api-client.provider';
|
||||
import { Postcode } from '@registratie/domain/value-objects/postcode';
|
||||
|
||||
const validData = { straat: 'Lange Voorhout 9', postcode: '2514 EA' as Postcode, woonplaats: 'Den Haag' };
|
||||
const validData = {
|
||||
straat: 'Lange Voorhout 9',
|
||||
postcode: '2514 EA' as Postcode,
|
||||
woonplaats: 'Den Haag',
|
||||
};
|
||||
|
||||
const meta: Meta<ChangeRequestFormComponent> = {
|
||||
title: 'Organisms/Change Request Form',
|
||||
@@ -17,16 +21,27 @@ export default meta;
|
||||
type Story = StoryObj<ChangeRequestFormComponent>;
|
||||
|
||||
// One render per state of the machine.
|
||||
export const Empty: Story = { args: { seed: { tag: 'Editing', draft: { straat: '', postcode: '', woonplaats: '' }, errors: {} } } };
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
seed: { tag: 'Editing', draft: { straat: '', postcode: '', woonplaats: '' }, errors: {} },
|
||||
},
|
||||
};
|
||||
export const WithErrors: Story = {
|
||||
args: {
|
||||
seed: {
|
||||
tag: 'Editing',
|
||||
draft: { straat: '', postcode: 'nope', woonplaats: '' },
|
||||
errors: { straat: 'Vul straat en huisnummer in.', postcode: 'Voer een geldige postcode in, bijv. 1234 AB.' },
|
||||
errors: {
|
||||
straat: 'Vul straat en huisnummer in.',
|
||||
postcode: 'Voer een geldige postcode in, bijv. 1234 AB.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
export const Submitting: Story = { args: { seed: { tag: 'Submitting', data: validData } } };
|
||||
export const Submitted: Story = { args: { seed: { tag: 'Submitted', data: validData, referentie: 'BIG-2026-123456' } } };
|
||||
export const Failed: Story = { args: { seed: { tag: 'Failed', data: validData, error: 'Netwerkfout' } } };
|
||||
export const Submitted: Story = {
|
||||
args: { seed: { tag: 'Submitted', data: validData, referentie: 'BIG-2026-123456' } },
|
||||
};
|
||||
export const Failed: Story = {
|
||||
args: { seed: { tag: 'Failed', data: validData, error: 'Netwerkfout' } },
|
||||
};
|
||||
|
||||
@@ -25,91 +25,163 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
@Component({
|
||||
selector: 'app-dashboard-page',
|
||||
imports: [
|
||||
PageShellComponent, HeadingComponent, AlertComponent, SkeletonComponent,
|
||||
DataRowComponent, DataBlockComponent, TaskListComponent, ApplicationListComponent, ApplicationLinkComponent,
|
||||
PageShellComponent,
|
||||
HeadingComponent,
|
||||
AlertComponent,
|
||||
SkeletonComponent,
|
||||
DataRowComponent,
|
||||
DataBlockComponent,
|
||||
TaskListComponent,
|
||||
ApplicationListComponent,
|
||||
ApplicationLinkComponent,
|
||||
...ASYNC,
|
||||
RegistrationSummaryComponent, RegistrationTableComponent, AanvraagBlockComponent,
|
||||
RegistrationSummaryComponent,
|
||||
RegistrationTableComponent,
|
||||
AanvraagBlockComponent,
|
||||
],
|
||||
template: `
|
||||
<app-page-shell i18n-heading="@@dashboard.heading" heading="Mijn overzicht" i18n-intro="@@dashboard.intro" intro="Welkom in uw persoonlijke omgeving van het BIG-register. Hier ziet u uw registratie en regelt u uw zaken.">
|
||||
<div class="app-stack">
|
||||
@if (aanvragen().length) {
|
||||
<app-page-shell
|
||||
i18n-heading="@@dashboard.heading"
|
||||
heading="Mijn overzicht"
|
||||
i18n-intro="@@dashboard.intro"
|
||||
intro="Welkom in uw persoonlijke omgeving van het BIG-register. Hier ziet u uw registratie en regelt u uw zaken."
|
||||
>
|
||||
<div class="app-stack">
|
||||
@if (aanvragen().length) {
|
||||
<section>
|
||||
@for (a of concepten(); track a.id) {
|
||||
<app-aanvraag-block
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[aanvraag]="a"
|
||||
(resume)="resume(a)"
|
||||
(cancel)="cancelAanvraag(a)"
|
||||
/>
|
||||
}
|
||||
@if (ingediend().length) {
|
||||
<app-heading [level]="2" class="app-section" i18n="@@dashboard.mijnAanvragen"
|
||||
>Mijn aanvragen</app-heading
|
||||
>
|
||||
<app-application-list>
|
||||
@for (a of ingediend(); track a.id) {
|
||||
@let row = submittedRow(a);
|
||||
<li
|
||||
app-application-link
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[heading]="row.heading"
|
||||
[subtitle]="row.subtitle"
|
||||
[status]="row.status"
|
||||
[to]="'/aanvraag/' + a.id"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (store.pendingHerregistratie()) {
|
||||
<app-alert type="info" i18n="@@dashboard.pendingHerregistratie"
|
||||
>Uw herregistratie-aanvraag is in behandeling.</app-alert
|
||||
>
|
||||
}
|
||||
|
||||
<app-async [data]="store.profile()">
|
||||
<ng-template appAsyncLoaded let-p>
|
||||
@let tasks = tasksFor($any(p).registration);
|
||||
|
||||
<section>
|
||||
@for (a of concepten(); track a.id) {
|
||||
<app-aanvraag-block animate.enter="app-item-enter" animate.leave="app-item-leave" [aanvraag]="a" (resume)="resume(a)" (cancel)="cancelAanvraag(a)" />
|
||||
}
|
||||
@if (ingediend().length) {
|
||||
<app-heading [level]="2" class="app-section" i18n="@@dashboard.mijnAanvragen">Mijn aanvragen</app-heading>
|
||||
<app-application-list>
|
||||
@for (a of ingediend(); track a.id) {
|
||||
@let row = submittedRow(a);
|
||||
<li app-application-link animate.enter="app-item-enter" animate.leave="app-item-leave" [heading]="row.heading" [subtitle]="row.subtitle" [status]="row.status" [to]="'/aanvraag/' + a.id"></li>
|
||||
}
|
||||
</app-application-list>
|
||||
@if (tasks.length) {
|
||||
<app-task-list
|
||||
class="app-section"
|
||||
i18n-listHeading="@@dashboard.watMoetIkRegelen"
|
||||
listHeading="Wat moet ik regelen"
|
||||
[tasks]="tasks"
|
||||
/>
|
||||
} @else {
|
||||
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen"
|
||||
>Wat moet ik regelen</app-heading
|
||||
>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">
|
||||
U heeft op dit moment niets openstaan.
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (store.pendingHerregistratie()) {
|
||||
<app-alert type="info" i18n="@@dashboard.pendingHerregistratie">Uw herregistratie-aanvraag is in behandeling.</app-alert>
|
||||
}
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie"
|
||||
>Mijn registratie</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-registration-summary [reg]="$any(p).registration" />
|
||||
</div>
|
||||
<app-data-block
|
||||
class="app-section"
|
||||
i18n-heading="@@dashboard.persoonsgegevens"
|
||||
heading="Persoonsgegevens (BRP)"
|
||||
>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.straat"
|
||||
key="Straat"
|
||||
[value]="$any(p).person.adres.straat"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.postcode"
|
||||
key="Postcode"
|
||||
[value]="$any(p).person.adres.postcode"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.woonplaats"
|
||||
key="Woonplaats"
|
||||
[value]="$any(p).person.adres.woonplaats"
|
||||
></div>
|
||||
</app-data-block>
|
||||
</section>
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="6" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
|
||||
<app-async [data]="store.profile()">
|
||||
<ng-template appAsyncLoaded let-p>
|
||||
@let tasks = tasksFor($any(p).registration);
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.specialismen"
|
||||
>Specialismen en aantekeningen</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-async [data]="store.aantekeningen()">
|
||||
<ng-template appAsyncLoaded let-r>
|
||||
<app-registration-table [rows]="$any(r)" />
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="3" />
|
||||
</ng-template>
|
||||
<ng-template appAsyncEmpty>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.geenSpecialismen">
|
||||
U heeft nog geen specialismen of aantekeningen.
|
||||
</p>
|
||||
</ng-template>
|
||||
</app-async>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@if (tasks.length) {
|
||||
<app-task-list class="app-section" i18n-listHeading="@@dashboard.watMoetIkRegelen" listHeading="Wat moet ik regelen" [tasks]="tasks" />
|
||||
} @else {
|
||||
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen">Wat moet ik regelen</app-heading>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">U heeft op dit moment niets openstaan.</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie">Mijn registratie</app-heading>
|
||||
<div class="app-section">
|
||||
<app-registration-summary [reg]="$any(p).registration" />
|
||||
</div>
|
||||
<app-data-block class="app-section" i18n-heading="@@dashboard.persoonsgegevens" heading="Persoonsgegevens (BRP)">
|
||||
<div app-data-row i18n-key="@@dashboard.straat" key="Straat" [value]="$any(p).person.adres.straat"></div>
|
||||
<div app-data-row i18n-key="@@dashboard.postcode" key="Postcode" [value]="$any(p).person.adres.postcode"></div>
|
||||
<div app-data-row i18n-key="@@dashboard.woonplaats" key="Woonplaats" [value]="$any(p).person.adres.woonplaats"></div>
|
||||
</app-data-block>
|
||||
</section>
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="6" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.specialismen">Specialismen en aantekeningen</app-heading>
|
||||
<div class="app-section">
|
||||
<app-async [data]="store.aantekeningen()">
|
||||
<ng-template appAsyncLoaded let-r>
|
||||
<app-registration-table [rows]="$any(r)" />
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="3" />
|
||||
</ng-template>
|
||||
<ng-template appAsyncEmpty>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.geenSpecialismen">U heeft nog geen specialismen of aantekeningen.</p>
|
||||
</ng-template>
|
||||
</app-async>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.watWiltUDoen">Wat wilt u doen?</app-heading>
|
||||
<app-application-list class="app-section">
|
||||
@for (a of acties; track a.to) {
|
||||
<li app-application-link [heading]="a.titel" [subtitle]="a.tekst" [cta]="a.actie" [to]="a.to"></li>
|
||||
}
|
||||
</app-application-list>
|
||||
</section>
|
||||
</div>
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.watWiltUDoen">Wat wilt u doen?</app-heading>
|
||||
<app-application-list class="app-section">
|
||||
@for (a of acties; track a.to) {
|
||||
<li
|
||||
app-application-link
|
||||
[heading]="a.titel"
|
||||
[subtitle]="a.tekst"
|
||||
[cta]="a.actie"
|
||||
[to]="a.to"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
</section>
|
||||
</div>
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
@@ -132,7 +204,12 @@ export class DashboardPage {
|
||||
protected aanvragen = computed<Aanvraag[]>(() => {
|
||||
const rd = this.apps.applications();
|
||||
if (rd.tag !== 'Success') return [];
|
||||
const order: Record<Aanvraag['status']['tag'], number> = { Concept: 0, InBehandeling: 1, Goedgekeurd: 2, Afgewezen: 2 };
|
||||
const order: Record<Aanvraag['status']['tag'], number> = {
|
||||
Concept: 0,
|
||||
InBehandeling: 1,
|
||||
Goedgekeurd: 2,
|
||||
Afgewezen: 2,
|
||||
};
|
||||
return rd.value.slice().sort((a, b) => order[a.status.tag] - order[b.status.tag]);
|
||||
});
|
||||
/** A Concept ("lopende aanvraag") renders as a melding above the list; the rest
|
||||
@@ -166,11 +243,41 @@ export class DashboardPage {
|
||||
componenten/aanvragen). The core portal sections live in the header nav now;
|
||||
the teaching pages (concepts/brief) are only reachable from here. */
|
||||
protected readonly acties = [
|
||||
{ to: '/registreren', titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`, tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`, actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving` },
|
||||
{ to: '/herregistratie', titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`, tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`, actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan` },
|
||||
{ to: '/intake', titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`, tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`, actie: $localize`:@@dashboard.actie.intake.actie:Start intake` },
|
||||
{ to: '/registratie', titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`, tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`, actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens` },
|
||||
{ to: '/concepts', titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`, tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`, actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen` },
|
||||
{ to: '/brief', titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`, tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`, actie: $localize`:@@dashboard.actie.brief.actie:Start brief` },
|
||||
{
|
||||
to: '/registreren',
|
||||
titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`,
|
||||
tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`,
|
||||
actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving`,
|
||||
},
|
||||
{
|
||||
to: '/herregistratie',
|
||||
titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`,
|
||||
tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`,
|
||||
actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan`,
|
||||
},
|
||||
{
|
||||
to: '/intake',
|
||||
titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`,
|
||||
tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`,
|
||||
actie: $localize`:@@dashboard.actie.intake.actie:Start intake`,
|
||||
},
|
||||
{
|
||||
to: '/registratie',
|
||||
titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`,
|
||||
tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`,
|
||||
actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens`,
|
||||
},
|
||||
{
|
||||
to: '/concepts',
|
||||
titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`,
|
||||
tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`,
|
||||
actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen`,
|
||||
},
|
||||
{
|
||||
to: '/brief',
|
||||
titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`,
|
||||
tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`,
|
||||
actie: $localize`:@@dashboard.actie.brief.actie:Start brief`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,7 +9,12 @@ import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';
|
||||
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
||||
import { WizardShellComponent, WizardError, WizardStatus, naarStapLabel } from '@shared/layout/wizard-shell/wizard-shell.component';
|
||||
import {
|
||||
WizardShellComponent,
|
||||
WizardError,
|
||||
WizardStatus,
|
||||
naarStapLabel,
|
||||
} from '@shared/layout/wizard-shell/wizard-shell.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component';
|
||||
import { createStore } from '@shared/application/store';
|
||||
@@ -54,138 +59,291 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
|
||||
@Component({
|
||||
selector: 'app-registratie-wizard',
|
||||
imports: [
|
||||
FormsModule, FormFieldComponent, TextInputComponent, RadioGroupComponent, ButtonComponent,
|
||||
AlertComponent, SkeletonComponent, DataRowComponent, ReviewSectionComponent, ConfirmationComponent, WizardShellComponent,
|
||||
AddressFieldsComponent, DocumentUploadComponent, ...ASYNC,
|
||||
FormsModule,
|
||||
FormFieldComponent,
|
||||
TextInputComponent,
|
||||
RadioGroupComponent,
|
||||
ButtonComponent,
|
||||
AlertComponent,
|
||||
SkeletonComponent,
|
||||
DataRowComponent,
|
||||
ReviewSectionComponent,
|
||||
ConfirmationComponent,
|
||||
WizardShellComponent,
|
||||
AddressFieldsComponent,
|
||||
DocumentUploadComponent,
|
||||
...ASYNC,
|
||||
],
|
||||
template: `
|
||||
<app-wizard-shell
|
||||
[steps]="stepLabels"
|
||||
[current]="cursor()"
|
||||
[stepTitle]="stepTitle()"
|
||||
i18n-processName="@@regWizard.processName" processName="Inschrijven in het BIG-register"
|
||||
i18n-processName="@@regWizard.processName"
|
||||
processName="Inschrijven in het BIG-register"
|
||||
[status]="shellStatus()"
|
||||
[primaryLabel]="primaryLabel()"
|
||||
[canGoBack]="cursor() > 0"
|
||||
[errors]="errorList()"
|
||||
[errorMessage]="errorMessage()"
|
||||
i18n-submittingLabel="@@regWizard.submitting" submittingLabel="Uw registratie wordt verwerkt…"
|
||||
i18n-submittingLabel="@@regWizard.submitting"
|
||||
submittingLabel="Uw registratie wordt verwerkt…"
|
||||
(primary)="onPrimary()"
|
||||
(back)="dispatch({ tag: 'Back' })"
|
||||
(cancel)="restart()"
|
||||
(retry)="onRetry()"
|
||||
(goToStep)="dispatch({ tag: 'GaNaarStap', cursor: $event })">
|
||||
|
||||
(goToStep)="dispatch({ tag: 'GaNaarStap', cursor: $event })"
|
||||
>
|
||||
@switch (step()) {
|
||||
@case ('adres') {
|
||||
@if (adresStatus() === 'laden') {
|
||||
<app-skeleton height="2.5rem" [count]="4" />
|
||||
} @else {
|
||||
@switch (adresStatus()) {
|
||||
@case ('gevonden') {
|
||||
<app-alert type="info" i18n="@@regWizard.brpGevonden">Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert>
|
||||
}
|
||||
@case ('geen') {
|
||||
<app-alert type="warning" i18n="@@regWizard.brpGeen">We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert>
|
||||
}
|
||||
@case ('fout') {
|
||||
<app-alert type="warning" i18n="@@regWizard.brpFout">We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.</app-alert>
|
||||
}
|
||||
}
|
||||
<app-address-fields
|
||||
[value]="{ straat: draft().straat ?? '', postcode: draft().postcode ?? '', woonplaats: draft().woonplaats ?? '' }"
|
||||
[errors]="{ straat: err('straat'), postcode: err('postcode'), woonplaats: err('woonplaats') }"
|
||||
(fieldChange)="set($event.key, $event.value)" />
|
||||
<app-form-field i18n-label="@@regWizard.correspondentieLabel" label="Hoe wilt u correspondentie ontvangen?" fieldId="correspondentie" required [error]="err('correspondentie')">
|
||||
<app-radio-group name="correspondentie" [options]="kanalen" [invalid]="!!err('correspondentie')"
|
||||
[ngModel]="draft().correspondentie ?? ''" (ngModelChange)="setKanaal($event)" />
|
||||
@if (adresStatus() === 'laden') {
|
||||
<app-skeleton height="2.5rem" [count]="4" />
|
||||
} @else {
|
||||
@switch (adresStatus()) {
|
||||
@case ('gevonden') {
|
||||
<app-alert type="info" i18n="@@regWizard.brpGevonden"
|
||||
>Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert
|
||||
>
|
||||
}
|
||||
@case ('geen') {
|
||||
<app-alert type="warning" i18n="@@regWizard.brpGeen"
|
||||
>We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert
|
||||
>
|
||||
}
|
||||
@case ('fout') {
|
||||
<app-alert type="warning" i18n="@@regWizard.brpFout"
|
||||
>We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig
|
||||
in.</app-alert
|
||||
>
|
||||
}
|
||||
}
|
||||
<app-address-fields
|
||||
[value]="{
|
||||
straat: draft().straat ?? '',
|
||||
postcode: draft().postcode ?? '',
|
||||
woonplaats: draft().woonplaats ?? '',
|
||||
}"
|
||||
[errors]="{
|
||||
straat: err('straat'),
|
||||
postcode: err('postcode'),
|
||||
woonplaats: err('woonplaats'),
|
||||
}"
|
||||
(fieldChange)="set($event.key, $event.value)"
|
||||
/>
|
||||
<app-form-field
|
||||
i18n-label="@@regWizard.correspondentieLabel"
|
||||
label="Hoe wilt u correspondentie ontvangen?"
|
||||
fieldId="correspondentie"
|
||||
required
|
||||
[error]="err('correspondentie')"
|
||||
>
|
||||
<app-radio-group
|
||||
name="correspondentie"
|
||||
[options]="kanalen"
|
||||
[invalid]="!!err('correspondentie')"
|
||||
[ngModel]="draft().correspondentie ?? ''"
|
||||
(ngModelChange)="setKanaal($event)"
|
||||
/>
|
||||
</app-form-field>
|
||||
@if (draft().correspondentie === 'email') {
|
||||
<app-form-field
|
||||
i18n-label="@@regWizard.emailLabel"
|
||||
label="E-mailadres"
|
||||
fieldId="email"
|
||||
required
|
||||
[error]="err('email')"
|
||||
>
|
||||
<app-text-input
|
||||
inputId="email"
|
||||
type="email"
|
||||
[invalid]="!!err('email')"
|
||||
[ngModel]="draft().email ?? ''"
|
||||
(ngModelChange)="set('email', $event)"
|
||||
name="email"
|
||||
i18n-placeholder="@@regWizard.emailPlaceholder"
|
||||
placeholder="naam@voorbeeld.nl"
|
||||
/>
|
||||
</app-form-field>
|
||||
}
|
||||
}
|
||||
}
|
||||
@case ('beroep') {
|
||||
<app-async [data]="lookupRd()">
|
||||
<ng-template appAsyncLoaded let-data>
|
||||
<app-form-field
|
||||
i18n-label="@@regWizard.diplomaLabel"
|
||||
label="Kies het diploma waarmee u zich wilt registreren"
|
||||
fieldId="diploma"
|
||||
required
|
||||
[error]="err('diploma')"
|
||||
>
|
||||
<app-radio-group
|
||||
name="diploma"
|
||||
[options]="diplomaOptions($any(data))"
|
||||
[invalid]="!!err('diploma')"
|
||||
[ngModel]="diplomaKeuze()"
|
||||
(ngModelChange)="onDiplomaKeuze($any(data), $event)"
|
||||
/>
|
||||
</app-form-field>
|
||||
|
||||
@if (handmatigActief()) {
|
||||
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing"
|
||||
>Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw
|
||||
beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig
|
||||
beoordeeld.</app-alert
|
||||
>
|
||||
<app-form-field
|
||||
i18n-label="@@regWizard.beroepLabel"
|
||||
label="Voor welk beroep wilt u zich registreren?"
|
||||
fieldId="hm-beroep"
|
||||
[error]="err('diploma')"
|
||||
>
|
||||
<app-radio-group
|
||||
name="hm-beroep"
|
||||
[options]="beroepOptions($any(data))"
|
||||
[invalid]="!!err('diploma')"
|
||||
[ngModel]="draft().beroep ?? ''"
|
||||
(ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })"
|
||||
/>
|
||||
</app-form-field>
|
||||
@if (draft().correspondentie === 'email') {
|
||||
<app-form-field i18n-label="@@regWizard.emailLabel" label="E-mailadres" fieldId="email" required [error]="err('email')">
|
||||
<app-text-input inputId="email" type="email" [invalid]="!!err('email')" [ngModel]="draft().email ?? ''" (ngModelChange)="set('email', $event)" name="email" i18n-placeholder="@@regWizard.emailPlaceholder" placeholder="naam@voorbeeld.nl" />
|
||||
</app-form-field>
|
||||
}
|
||||
} @else if (draft().beroep) {
|
||||
<dl class="mb-0 app-section">
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@regWizard.beroepAfgeleid"
|
||||
key="Beroep (afgeleid uit diploma)"
|
||||
[value]="draft().beroep ?? ''"
|
||||
></div>
|
||||
</dl>
|
||||
}
|
||||
}
|
||||
@case ('beroep') {
|
||||
<app-async [data]="lookupRd()">
|
||||
<ng-template appAsyncLoaded let-data>
|
||||
<app-form-field i18n-label="@@regWizard.diplomaLabel" label="Kies het diploma waarmee u zich wilt registreren" fieldId="diploma" required [error]="err('diploma')">
|
||||
<app-radio-group name="diploma" [options]="diplomaOptions($any(data))" [invalid]="!!err('diploma')"
|
||||
[ngModel]="diplomaKeuze()" (ngModelChange)="onDiplomaKeuze($any(data), $event)" />
|
||||
</app-form-field>
|
||||
|
||||
@if (handmatigActief()) {
|
||||
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing">Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig beoordeeld.</app-alert>
|
||||
<app-form-field i18n-label="@@regWizard.beroepLabel" label="Voor welk beroep wilt u zich registreren?" fieldId="hm-beroep" [error]="err('diploma')">
|
||||
<app-radio-group name="hm-beroep" [options]="beroepOptions($any(data))" [invalid]="!!err('diploma')"
|
||||
[ngModel]="draft().beroep ?? ''" (ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })" />
|
||||
</app-form-field>
|
||||
} @else if (draft().beroep) {
|
||||
<dl class="mb-0 app-section">
|
||||
<div app-data-row i18n-key="@@regWizard.beroepAfgeleid" key="Beroep (afgeleid uit diploma)" [value]="draft().beroep ?? ''"></div>
|
||||
</dl>
|
||||
@for (q of actieveVragen($any(data)); track q.id) {
|
||||
<app-form-field
|
||||
[label]="q.vraag"
|
||||
[fieldId]="'vraag-' + q.id"
|
||||
[error]="vraagErr(q.id)"
|
||||
>
|
||||
@if (q.type === 'ja-nee') {
|
||||
<app-radio-group
|
||||
[name]="'vraag-' + q.id"
|
||||
[options]="jaNee"
|
||||
[invalid]="!!vraagErr(q.id)"
|
||||
[ngModel]="antwoord(q.id)"
|
||||
(ngModelChange)="
|
||||
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
|
||||
"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
} @else {
|
||||
<app-text-input
|
||||
[inputId]="'vraag-' + q.id"
|
||||
[invalid]="!!vraagErr(q.id)"
|
||||
[ngModel]="antwoord(q.id)"
|
||||
(ngModelChange)="
|
||||
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
|
||||
"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
}
|
||||
|
||||
@for (q of actieveVragen($any(data)); track q.id) {
|
||||
<app-form-field [label]="q.vraag" [fieldId]="'vraag-' + q.id" [error]="vraagErr(q.id)">
|
||||
@if (q.type === 'ja-nee') {
|
||||
<app-radio-group [name]="'vraag-' + q.id" [options]="jaNee" [invalid]="!!vraagErr(q.id)"
|
||||
[ngModel]="antwoord(q.id)" (ngModelChange)="dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })" [ngModelOptions]="{ standalone: true }" />
|
||||
} @else {
|
||||
<app-text-input [inputId]="'vraag-' + q.id" [invalid]="!!vraagErr(q.id)" [ngModel]="antwoord(q.id)"
|
||||
(ngModelChange)="dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })" [ngModelOptions]="{ standalone: true }" />
|
||||
}
|
||||
</app-form-field>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="3" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
|
||||
<app-document-upload
|
||||
class="app-section"
|
||||
[state]="upload()"
|
||||
[previewUrlFor]="previewUrlFor"
|
||||
(fileSelected)="uploadCtl.onFileSelected($event.categoryId, $event.files)"
|
||||
(removeUpload)="uploadCtl.onRemove($event)"
|
||||
(retryUpload)="uploadCtl.onRetry($event)"
|
||||
(deleteUpload)="uploadCtl.onDelete($event)"
|
||||
(channelChange)="uploadCtl.onChannelChange($event.categoryId, $event.channel)" />
|
||||
@if (err('documenten')) {
|
||||
<app-alert type="warning">{{ err('documenten') }}</app-alert>
|
||||
</app-form-field>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="3" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
|
||||
<app-document-upload
|
||||
class="app-section"
|
||||
[state]="upload()"
|
||||
[previewUrlFor]="previewUrlFor"
|
||||
(fileSelected)="uploadCtl.onFileSelected($event.categoryId, $event.files)"
|
||||
(removeUpload)="uploadCtl.onRemove($event)"
|
||||
(retryUpload)="uploadCtl.onRetry($event)"
|
||||
(deleteUpload)="uploadCtl.onDelete($event)"
|
||||
(channelChange)="uploadCtl.onChannelChange($event.categoryId, $event.channel)"
|
||||
/>
|
||||
@if (err('documenten')) {
|
||||
<app-alert type="warning">{{ err('documenten') }}</app-alert>
|
||||
}
|
||||
}
|
||||
@case ('controle') {
|
||||
<app-alert type="info" i18n="@@regWizard.controleer"
|
||||
>Controleer uw gegevens en dien de registratie in.</app-alert
|
||||
>
|
||||
<app-review-section
|
||||
i18n-heading="@@regWizard.sectie.adres"
|
||||
heading="Adres en correspondentie"
|
||||
i18n-editAriaLabel="@@regWizard.adresWijzigenAria"
|
||||
editAriaLabel="Wijzigen adresgegevens"
|
||||
(edit)="dispatch({ tag: 'GaNaarStap', cursor: 0 })"
|
||||
>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@regWizard.summary.adres"
|
||||
key="Adres"
|
||||
[value]="adresSamenvatting()"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@regWizard.summary.herkomstAdres"
|
||||
key="Herkomst adres"
|
||||
[value]="adresHerkomstLabel()"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@regWizard.summary.correspondentie"
|
||||
key="Correspondentie"
|
||||
[value]="correspondentieLabel()"
|
||||
></div>
|
||||
@if (draft().correspondentie === 'email') {
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@regWizard.summary.email"
|
||||
key="E-mailadres"
|
||||
[value]="draft().email ?? ''"
|
||||
></div>
|
||||
}
|
||||
@case ('controle') {
|
||||
<app-alert type="info" i18n="@@regWizard.controleer">Controleer uw gegevens en dien de registratie in.</app-alert>
|
||||
<app-review-section i18n-heading="@@regWizard.sectie.adres" heading="Adres en correspondentie"
|
||||
i18n-editAriaLabel="@@regWizard.adresWijzigenAria" editAriaLabel="Wijzigen adresgegevens"
|
||||
(edit)="dispatch({ tag: 'GaNaarStap', cursor: 0 })">
|
||||
<div app-data-row i18n-key="@@regWizard.summary.adres" key="Adres" [value]="adresSamenvatting()"></div>
|
||||
<div app-data-row i18n-key="@@regWizard.summary.herkomstAdres" key="Herkomst adres" [value]="adresHerkomstLabel()"></div>
|
||||
<div app-data-row i18n-key="@@regWizard.summary.correspondentie" key="Correspondentie" [value]="correspondentieLabel()"></div>
|
||||
@if (draft().correspondentie === 'email') {
|
||||
<div app-data-row i18n-key="@@regWizard.summary.email" key="E-mailadres" [value]="draft().email ?? ''"></div>
|
||||
}
|
||||
</app-review-section>
|
||||
<app-review-section class="app-section" i18n-heading="@@regWizard.sectie.beroep" heading="Beroep en diploma"
|
||||
i18n-editAriaLabel="@@regWizard.diplomaWijzigenAria" editAriaLabel="Wijzigen beroep en diploma"
|
||||
(edit)="dispatch({ tag: 'GaNaarStap', cursor: 1 })">
|
||||
<div app-data-row i18n-key="@@regWizard.summary.beroep" key="Beroep" [value]="draft().beroep ?? ''"></div>
|
||||
<div app-data-row i18n-key="@@regWizard.summary.herkomstDiploma" key="Herkomst diploma" [value]="diplomaHerkomstLabel()"></div>
|
||||
@for (item of samenvattingVragen(); track item.vraag) {
|
||||
<div app-data-row [key]="item.vraag" [value]="item.antwoord"></div>
|
||||
}
|
||||
</app-review-section>
|
||||
</app-review-section>
|
||||
<app-review-section
|
||||
class="app-section"
|
||||
i18n-heading="@@regWizard.sectie.beroep"
|
||||
heading="Beroep en diploma"
|
||||
i18n-editAriaLabel="@@regWizard.diplomaWijzigenAria"
|
||||
editAriaLabel="Wijzigen beroep en diploma"
|
||||
(edit)="dispatch({ tag: 'GaNaarStap', cursor: 1 })"
|
||||
>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@regWizard.summary.beroep"
|
||||
key="Beroep"
|
||||
[value]="draft().beroep ?? ''"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@regWizard.summary.herkomstDiploma"
|
||||
key="Herkomst diploma"
|
||||
[value]="diplomaHerkomstLabel()"
|
||||
></div>
|
||||
@for (item of samenvattingVragen(); track item.vraag) {
|
||||
<div app-data-row [key]="item.vraag" [value]="item.antwoord"></div>
|
||||
}
|
||||
</app-review-section>
|
||||
}
|
||||
}
|
||||
|
||||
<div wizardSuccess>
|
||||
<app-confirmation i18n-title="@@regWizard.success.title" title="Uw registratie is ontvangen">
|
||||
<p class="app-section" i18n="@@regWizard.success.referentie">Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.</p>
|
||||
<app-confirmation
|
||||
i18n-title="@@regWizard.success.title"
|
||||
title="Uw registratie is ontvangen"
|
||||
>
|
||||
<p class="app-section" i18n="@@regWizard.success.referentie">
|
||||
Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.
|
||||
</p>
|
||||
<div class="app-section">
|
||||
<app-button variant="secondary" (click)="restart()" i18n="@@regWizard.nieuweRegistratie">Nieuwe registratie starten</app-button>
|
||||
<app-button variant="secondary" (click)="restart()" i18n="@@regWizard.nieuweRegistratie"
|
||||
>Nieuwe registratie starten</app-button
|
||||
>
|
||||
</div>
|
||||
</app-confirmation>
|
||||
</div>
|
||||
@@ -206,8 +364,16 @@ export class RegistratieWizardComponent {
|
||||
seed = input<RegistratieState>(initial);
|
||||
|
||||
readonly kanalen = KANALEN;
|
||||
readonly stepLabels = [$localize`:@@regWizard.step.adres:Adres`, $localize`:@@regWizard.step.beroep:Beroep`, $localize`:@@regWizard.step.controle:Controle`]; // short labels for the stepper
|
||||
private stepTitles = [$localize`:@@regWizard.title.adres:Adres en correspondentievoorkeur`, $localize`:@@regWizard.title.beroep:Beroep op basis van uw diploma`, $localize`:@@regWizard.title.controle:Controleren en indienen`];
|
||||
readonly stepLabels = [
|
||||
$localize`:@@regWizard.step.adres:Adres`,
|
||||
$localize`:@@regWizard.step.beroep:Beroep`,
|
||||
$localize`:@@regWizard.step.controle:Controle`,
|
||||
]; // short labels for the stepper
|
||||
private stepTitles = [
|
||||
$localize`:@@regWizard.title.adres:Adres en correspondentievoorkeur`,
|
||||
$localize`:@@regWizard.title.beroep:Beroep op basis van uw diploma`,
|
||||
$localize`:@@regWizard.title.controle:Controleren en indienen`,
|
||||
];
|
||||
readonly state = this.store.model;
|
||||
readonly dispatch = this.store.dispatch;
|
||||
|
||||
@@ -234,14 +400,18 @@ export class RegistratieWizardComponent {
|
||||
snapshot: () => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Invullen' || !hasProgress(s)) return null;
|
||||
const documentIds = deliveryRefs(s.upload).filter((r) => r.channel === 'digital' && r.documentId).map((r) => r.documentId!);
|
||||
const documentIds = deliveryRefs(s.upload)
|
||||
.filter((r) => r.channel === 'digital' && r.documentId)
|
||||
.map((r) => r.documentId!);
|
||||
return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds };
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as RegistratieState }),
|
||||
enabled: () => this.seed() === initial,
|
||||
});
|
||||
protected step = computed<StepId>(() => STEPS[Math.min(this.cursor(), STEPS.length - 1)]);
|
||||
protected stepTitle = computed(() => this.stepTitles[Math.min(this.cursor(), this.stepTitles.length - 1)]);
|
||||
protected stepTitle = computed(
|
||||
() => this.stepTitles[Math.min(this.cursor(), this.stepTitles.length - 1)],
|
||||
);
|
||||
protected referentie = computed(() => whenTag(this.state(), 'Ingediend')?.referentie ?? '');
|
||||
protected failedError = computed(() => whenTag(this.state(), 'Mislukt')?.error ?? '');
|
||||
|
||||
@@ -251,13 +421,21 @@ export class RegistratieWizardComponent {
|
||||
const next = this.cursor() + 1;
|
||||
return naarStapLabel(next + 1, this.stepLabels[next]);
|
||||
});
|
||||
protected errorMessage = computed(() => $localize`:@@regWizard.indienenMislukt:Het indienen is niet gelukt:` + ` ${this.failedError()}`);
|
||||
protected errorMessage = computed(
|
||||
() =>
|
||||
$localize`:@@regWizard.indienenMislukt:Het indienen is niet gelukt:` +
|
||||
` ${this.failedError()}`,
|
||||
);
|
||||
protected shellStatus = computed<WizardStatus>(() => {
|
||||
switch (this.state().tag) {
|
||||
case 'Invullen': return 'editing';
|
||||
case 'Indienen': return 'submitting';
|
||||
case 'Ingediend': return 'submitted';
|
||||
case 'Mislukt': return 'failed';
|
||||
case 'Invullen':
|
||||
return 'editing';
|
||||
case 'Indienen':
|
||||
return 'submitting';
|
||||
case 'Ingediend':
|
||||
return 'submitted';
|
||||
case 'Mislukt':
|
||||
return 'failed';
|
||||
}
|
||||
});
|
||||
/** Current step's errors (incl. per-question), flattened for the error summary. */
|
||||
@@ -274,12 +452,32 @@ export class RegistratieWizardComponent {
|
||||
});
|
||||
protected adresSamenvatting = computed(() => {
|
||||
const d = this.draft();
|
||||
return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')].filter(Boolean).join(', ');
|
||||
return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')]
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
});
|
||||
// Readable labels for the controle summary (instead of raw enum values).
|
||||
protected adresHerkomstLabel = computed(() => ({ brp: $localize`:@@regWizard.herkomst.adresBrp:Automatisch uit de BRP`, handmatig: $localize`:@@regWizard.herkomst.adresHandmatig:Handmatig ingevoerd` }[this.draft().adresHerkomst ?? 'handmatig']));
|
||||
protected correspondentieLabel = computed(() => ({ email: $localize`:@@regWizard.corr.email:Per e-mail`, post: $localize`:@@regWizard.corr.post:Per post` }[this.draft().correspondentie ?? 'post']));
|
||||
protected diplomaHerkomstLabel = computed(() => ({ duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`, handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)` }[this.draft().diplomaHerkomst ?? 'handmatig']));
|
||||
protected adresHerkomstLabel = computed(
|
||||
() =>
|
||||
({
|
||||
brp: $localize`:@@regWizard.herkomst.adresBrp:Automatisch uit de BRP`,
|
||||
handmatig: $localize`:@@regWizard.herkomst.adresHandmatig:Handmatig ingevoerd`,
|
||||
})[this.draft().adresHerkomst ?? 'handmatig'],
|
||||
);
|
||||
protected correspondentieLabel = computed(
|
||||
() =>
|
||||
({
|
||||
email: $localize`:@@regWizard.corr.email:Per e-mail`,
|
||||
post: $localize`:@@regWizard.corr.post:Per post`,
|
||||
})[this.draft().correspondentie ?? 'post'],
|
||||
);
|
||||
protected diplomaHerkomstLabel = computed(
|
||||
() =>
|
||||
({
|
||||
duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`,
|
||||
handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)`,
|
||||
})[this.draft().diplomaHerkomst ?? 'handmatig'],
|
||||
);
|
||||
|
||||
/** BRP lookup outcome (laden/gevonden/geen/fout) and the parsed DUO lookup, both
|
||||
served by the application facade — the wizard renders, it does not fetch/parse. */
|
||||
@@ -295,23 +493,35 @@ export class RegistratieWizardComponent {
|
||||
|
||||
readonly jaNee = JA_NEE;
|
||||
|
||||
protected err = (k: DraftField | 'correspondentie' | 'diploma' | 'documenten') => this.invullen()?.errors[k] ?? '';
|
||||
protected err = (k: DraftField | 'correspondentie' | 'diploma' | 'documenten') =>
|
||||
this.invullen()?.errors[k] ?? '';
|
||||
protected vraagErr = (id: string) => this.invullen()?.errors.antwoorden?.[id] ?? '';
|
||||
protected antwoord = (id: string) => this.draft().antwoorden[id] ?? ''; // runtime guard: missing key → undefined
|
||||
protected set = (key: DraftField, value: string) => this.dispatch({ tag: 'SetField', key, value });
|
||||
protected setKanaal = (value: string) => this.dispatch({ tag: 'SetCorrespondentie', value: value as Correspondentie });
|
||||
protected set = (key: DraftField, value: string) =>
|
||||
this.dispatch({ tag: 'SetField', key, value });
|
||||
protected setKanaal = (value: string) =>
|
||||
this.dispatch({ tag: 'SetCorrespondentie', value: value as Correspondentie });
|
||||
|
||||
/** True while the user is entering a diploma manually (not in the DUO list). */
|
||||
protected handmatigActief = computed(() => this.draft().diplomaHerkomst === 'handmatig');
|
||||
/** The radio selection: a diploma id, or the"not listed" sentinel in manual mode. */
|
||||
protected diplomaKeuze = computed(() => (this.handmatigActief() ? HANDMATIG : this.draft().diplomaId ?? ''));
|
||||
protected diplomaKeuze = computed(() =>
|
||||
this.handmatigActief() ? HANDMATIG : (this.draft().diplomaId ?? ''),
|
||||
);
|
||||
|
||||
protected diplomaOptions = (data: DuoLookupDto) => [
|
||||
...data.diplomas.map((d) => ({ value: d.id, label: `${d.naam} — ${d.instelling} (${d.jaar})` })),
|
||||
{ value: HANDMATIG, label: $localize`:@@regWizard.diplomaNietBij:Mijn diploma staat er niet bij` },
|
||||
...data.diplomas.map((d) => ({
|
||||
value: d.id,
|
||||
label: `${d.naam} — ${d.instelling} (${d.jaar})`,
|
||||
})),
|
||||
{
|
||||
value: HANDMATIG,
|
||||
label: $localize`:@@regWizard.diplomaNietBij:Mijn diploma staat er niet bij`,
|
||||
},
|
||||
];
|
||||
|
||||
protected beroepOptions = (data: DuoLookupDto) => data.handmatig.beroepen.map((b) => ({ value: b, label: b }));
|
||||
protected beroepOptions = (data: DuoLookupDto) =>
|
||||
data.handmatig.beroepen.map((b) => ({ value: b, label: b }));
|
||||
|
||||
/** The policy questions that apply to the current choice (server-decided). */
|
||||
protected actieveVragen = (data: DuoLookupDto): PolicyQuestionDto[] => {
|
||||
@@ -324,24 +534,41 @@ export class RegistratieWizardComponent {
|
||||
const data = this.duoData();
|
||||
const d = this.draft();
|
||||
if (!data) return [] as { vraag: string; antwoord: string }[];
|
||||
const alle = [...data.diplomas.flatMap((x) => x.policyQuestions), ...data.handmatig.policyQuestions];
|
||||
return (d.vraagIds ?? []).map((id) => ({ vraag: alle.find((q) => q.id === id)?.vraag ?? id, antwoord: d.antwoorden[id] ?? '' }));
|
||||
const alle = [
|
||||
...data.diplomas.flatMap((x) => x.policyQuestions),
|
||||
...data.handmatig.policyQuestions,
|
||||
];
|
||||
return (d.vraagIds ?? []).map((id) => ({
|
||||
vraag: alle.find((q) => q.id === id)?.vraag ?? id,
|
||||
antwoord: d.antwoorden[id] ?? '',
|
||||
}));
|
||||
});
|
||||
|
||||
protected onDiplomaKeuze(data: DuoLookupDto, id: string) {
|
||||
if (id === HANDMATIG) {
|
||||
this.dispatch({ tag: 'KiesHandmatig', vraagIds: data.handmatig.policyQuestions.map((q) => q.id) });
|
||||
this.dispatch({
|
||||
tag: 'KiesHandmatig',
|
||||
vraagIds: data.handmatig.policyQuestions.map((q) => q.id),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const d = data.diplomas.find((x) => x.id === id);
|
||||
if (d) this.dispatch({ tag: 'KiesDiploma', diplomaId: d.id, beroep: d.beroep, vraagIds: d.policyQuestions.map((q) => q.id) });
|
||||
if (d)
|
||||
this.dispatch({
|
||||
tag: 'KiesDiploma',
|
||||
diplomaId: d.id,
|
||||
beroep: d.beroep,
|
||||
vraagIds: d.policyQuestions.map((q) => q.id),
|
||||
});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// An explicit seed (stories/tests) wins; otherwise resume from the backend draft
|
||||
// (`?aanvraag=<id>`), or start fresh. Persistence is the draftSync controller's job.
|
||||
const seeded = this.seed();
|
||||
queueMicrotask(() => (seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume()));
|
||||
queueMicrotask(() =>
|
||||
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
|
||||
);
|
||||
// Prefill the address from the BRP lookup as it arrives. Track only the facade's
|
||||
// parsed prefill signal; untrack the dispatch (it reads the state signal, which
|
||||
// would otherwise make this effect loop on its own write). Don't clobber
|
||||
@@ -352,7 +579,12 @@ export class RegistratieWizardComponent {
|
||||
untracked(() => {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Invullen' || s.draft.straat) return;
|
||||
this.dispatch({ tag: 'PrefillAdres', straat: a.straat, postcode: a.postcode, woonplaats: a.woonplaats });
|
||||
this.dispatch({
|
||||
tag: 'PrefillAdres',
|
||||
straat: a.straat,
|
||||
postcode: a.postcode,
|
||||
woonplaats: a.woonplaats,
|
||||
});
|
||||
});
|
||||
});
|
||||
// A11y: focus management (step heading on step change, error summary on a
|
||||
@@ -384,7 +616,10 @@ export class RegistratieWizardComponent {
|
||||
private async runIfIndienen() {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Indienen') return;
|
||||
const r = await this.draftSync.submit({ diplomaHerkomst: s.data.diplomaHerkomst, documents: s.data.documents });
|
||||
const r = await this.draftSync.submit({
|
||||
diplomaHerkomst: s.data.diplomaHerkomst,
|
||||
documents: s.data.documents,
|
||||
});
|
||||
if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value.referentie ?? '' });
|
||||
else this.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
}
|
||||
|
||||
@@ -3,14 +3,36 @@ import { applicationConfig } from '@storybook/angular';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideApiClient } from '@shared/infrastructure/api-client.provider';
|
||||
import { RegistratieWizardComponent } from './registratie-wizard.component';
|
||||
import { Draft, RegistratieState, ValidRegistratie } from '@registratie/domain/registratie-wizard.machine';
|
||||
import {
|
||||
Draft,
|
||||
RegistratieState,
|
||||
ValidRegistratie,
|
||||
} from '@registratie/domain/registratie-wizard.machine';
|
||||
import { initialUpload } from '@shared/upload/upload.machine';
|
||||
import { Postcode } from '@registratie/domain/value-objects/postcode';
|
||||
|
||||
const adres: Partial<Draft> = { straat: 'Lange Voorhout 9', postcode: '2514 EA', woonplaats: 'Den Haag', adresHerkomst: 'brp', correspondentie: 'post' };
|
||||
const filled: Partial<Draft> = { ...adres, diplomaId: 'd1', beroep: 'Arts', diplomaHerkomst: 'duo', vraagIds: [] };
|
||||
const adres: Partial<Draft> = {
|
||||
straat: 'Lange Voorhout 9',
|
||||
postcode: '2514 EA',
|
||||
woonplaats: 'Den Haag',
|
||||
adresHerkomst: 'brp',
|
||||
correspondentie: 'post',
|
||||
};
|
||||
const filled: Partial<Draft> = {
|
||||
...adres,
|
||||
diplomaId: 'd1',
|
||||
beroep: 'Arts',
|
||||
diplomaHerkomst: 'duo',
|
||||
vraagIds: [],
|
||||
};
|
||||
|
||||
const invullen = (draft: Partial<Draft>, cursor = 0): RegistratieState => ({ tag: 'Invullen', draft: { antwoorden: {}, ...draft }, cursor, errors: {}, upload: initialUpload });
|
||||
const invullen = (draft: Partial<Draft>, cursor = 0): RegistratieState => ({
|
||||
tag: 'Invullen',
|
||||
draft: { antwoorden: {}, ...draft },
|
||||
cursor,
|
||||
errors: {},
|
||||
upload: initialUpload,
|
||||
});
|
||||
|
||||
const validData: ValidRegistratie = {
|
||||
adres: { straat: 'Lange Voorhout 9', postcode: '2514 EA' as Postcode, woonplaats: 'Den Haag' },
|
||||
@@ -34,10 +56,39 @@ type Story = StoryObj<RegistratieWizardComponent>;
|
||||
export const Adres: Story = { args: { seed: invullen(adres, 0) } };
|
||||
export const Beroep: Story = { args: { seed: invullen(filled, 1) } };
|
||||
/** English-language diploma → the Dutch-proficiency policy question appears. */
|
||||
export const BeroepEngelstalig: Story = { args: { seed: invullen({ ...adres, diplomaId: 'd2', beroep: 'Arts', diplomaHerkomst: 'duo', vraagIds: ['nl-taalvaardigheid'] }, 1) } };
|
||||
export const BeroepEngelstalig: Story = {
|
||||
args: {
|
||||
seed: invullen(
|
||||
{
|
||||
...adres,
|
||||
diplomaId: 'd2',
|
||||
beroep: 'Arts',
|
||||
diplomaHerkomst: 'duo',
|
||||
vraagIds: ['nl-taalvaardigheid'],
|
||||
},
|
||||
1,
|
||||
),
|
||||
},
|
||||
};
|
||||
/** Diploma not in DUO → declare beroep + the maximal policy-question set. */
|
||||
export const BeroepHandmatig: Story = { args: { seed: invullen({ ...adres, diplomaId: 'handmatig', diplomaHerkomst: 'handmatig', vraagIds: ['nl-taalvaardigheid', 'diploma-erkend', 'toelichting'] }, 1) } };
|
||||
export const BeroepHandmatig: Story = {
|
||||
args: {
|
||||
seed: invullen(
|
||||
{
|
||||
...adres,
|
||||
diplomaId: 'handmatig',
|
||||
diplomaHerkomst: 'handmatig',
|
||||
vraagIds: ['nl-taalvaardigheid', 'diploma-erkend', 'toelichting'],
|
||||
},
|
||||
1,
|
||||
),
|
||||
},
|
||||
};
|
||||
export const Controle: Story = { args: { seed: invullen(filled, 2) } };
|
||||
export const Indienen: Story = { args: { seed: { tag: 'Indienen', data: validData } } };
|
||||
export const Ingediend: Story = { args: { seed: { tag: 'Ingediend', data: validData, referentie: 'BIG-2026-123456' } } };
|
||||
export const Mislukt: Story = { args: { seed: { tag: 'Mislukt', data: validData, error: 'Netwerkfout' } } };
|
||||
export const Ingediend: Story = {
|
||||
args: { seed: { tag: 'Ingediend', data: validData, referentie: 'BIG-2026-123456' } },
|
||||
};
|
||||
export const Mislukt: Story = {
|
||||
args: { seed: { tag: 'Mislukt', data: validData, error: 'Netwerkfout' } },
|
||||
};
|
||||
|
||||
@@ -12,11 +12,11 @@ import { RegistratieWizardComponent } from '@registratie/ui/registratie-wizard/r
|
||||
<app-page-shell
|
||||
i18n-heading="@@registratie.heading"
|
||||
heading="Inschrijven in het BIG-register"
|
||||
backLink="/dashboard">
|
||||
backLink="/dashboard"
|
||||
>
|
||||
<app-alert type="info" i18n="@@registratie.intro">
|
||||
In drie stappen schrijft u zich in: uw adres en correspondentievoorkeur, het
|
||||
diploma waarmee u zich registreert, en een controle. Uw gegevens blijven bewaard
|
||||
als u de pagina herlaadt.
|
||||
In drie stappen schrijft u zich in: uw adres en correspondentievoorkeur, het diploma waarmee
|
||||
u zich registreert, en een controle. Uw gegevens blijven bewaard als u de pagina herlaadt.
|
||||
</app-alert>
|
||||
<div class="app-section">
|
||||
<app-registratie-wizard />
|
||||
|
||||
@@ -9,11 +9,18 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
@Component({
|
||||
selector: 'app-registration-detail-page',
|
||||
imports: [
|
||||
PageShellComponent, SkeletonComponent, ...ASYNC,
|
||||
RegistrationSummaryComponent, ChangeRequestFormComponent,
|
||||
PageShellComponent,
|
||||
SkeletonComponent,
|
||||
...ASYNC,
|
||||
RegistrationSummaryComponent,
|
||||
ChangeRequestFormComponent,
|
||||
],
|
||||
template: `
|
||||
<app-page-shell i18n-heading="@@registratieDetail.heading" heading="Mijn gegevens" backLink="/dashboard">
|
||||
<app-page-shell
|
||||
i18n-heading="@@registratieDetail.heading"
|
||||
heading="Mijn gegevens"
|
||||
backLink="/dashboard"
|
||||
>
|
||||
<app-async [data]="store.profile()">
|
||||
<ng-template appAsyncLoaded let-p>
|
||||
<app-registration-summary [reg]="$any(p).registration" />
|
||||
|
||||
@@ -13,25 +13,60 @@ import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
imports: [DatePipe, DataRowComponent, StatusBadgeComponent, DataBlockComponent],
|
||||
template: `
|
||||
<app-data-block i18n-ariaLabel="@@summary.ariaLabel" ariaLabel="Registratiegegevens">
|
||||
<div app-data-row i18n-key="@@summary.bigNummer" key="BIG-nummer" [value]="reg().bigNummer"></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.bigNummer"
|
||||
key="BIG-nummer"
|
||||
[value]="reg().bigNummer"
|
||||
></div>
|
||||
<div app-data-row i18n-key="@@summary.naam" key="Naam" [value]="reg().naam"></div>
|
||||
<div app-data-row i18n-key="@@summary.beroep" key="Beroep" [value]="reg().beroep"></div>
|
||||
<div app-data-row i18n-key="@@summary.status" key="Status">
|
||||
<app-status-badge [label]="label()" [color]="color()" />
|
||||
</div>
|
||||
<div app-data-row i18n-key="@@summary.registratiedatum" key="Registratiedatum" [value]="reg().registratiedatum | date:'longDate'"></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.registratiedatum"
|
||||
key="Registratiedatum"
|
||||
[value]="reg().registratiedatum | date: 'longDate'"
|
||||
></div>
|
||||
<!-- Each status variant renders only the row its own data supports. -->
|
||||
@switch (reg().status.tag) {
|
||||
@case ('Geregistreerd') {
|
||||
<div app-data-row i18n-key="@@summary.uiterste" key="Uiterste herregistratie" [value]="$any(reg().status).herregistratieDatum | date:'longDate'"></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.uiterste"
|
||||
key="Uiterste herregistratie"
|
||||
[value]="$any(reg().status).herregistratieDatum | date: 'longDate'"
|
||||
></div>
|
||||
}
|
||||
@case ('Geschorst') {
|
||||
<div app-data-row i18n-key="@@summary.geschorstTot" key="Geschorst tot" [value]="$any(reg().status).geschorstTot | date:'longDate'"></div>
|
||||
<div app-data-row i18n-key="@@summary.reden" key="Reden" [value]="$any(reg().status).reden"></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.geschorstTot"
|
||||
key="Geschorst tot"
|
||||
[value]="$any(reg().status).geschorstTot | date: 'longDate'"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.reden"
|
||||
key="Reden"
|
||||
[value]="$any(reg().status).reden"
|
||||
></div>
|
||||
}
|
||||
@case ('Doorgehaald') {
|
||||
<div app-data-row i18n-key="@@summary.doorgehaaldOp" key="Doorgehaald op" [value]="$any(reg().status).doorgehaaldOp | date:'longDate'"></div>
|
||||
<div app-data-row i18n-key="@@summary.reden" key="Reden" [value]="$any(reg().status).reden"></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.doorgehaaldOp"
|
||||
key="Doorgehaald op"
|
||||
[value]="$any(reg().status).doorgehaaldOp | date: 'longDate'"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.reden"
|
||||
key="Reden"
|
||||
[value]="$any(reg().status).reden"
|
||||
></div>
|
||||
}
|
||||
}
|
||||
</app-data-block>
|
||||
|
||||
@@ -23,8 +23,18 @@ export const Geregistreerd: Story = {
|
||||
args: { reg: { ...base, status: { tag: 'Geregistreerd', herregistratieDatum: '2027-09-01' } } },
|
||||
};
|
||||
export const Geschorst: Story = {
|
||||
args: { reg: { ...base, status: { tag: 'Geschorst', geschorstTot: '2026-12-31', reden: 'Lopend tuchtonderzoek' } } },
|
||||
args: {
|
||||
reg: {
|
||||
...base,
|
||||
status: { tag: 'Geschorst', geschorstTot: '2026-12-31', reden: 'Lopend tuchtonderzoek' },
|
||||
},
|
||||
},
|
||||
};
|
||||
export const Doorgehaald: Story = {
|
||||
args: { reg: { ...base, status: { tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'Op eigen verzoek' } } },
|
||||
args: {
|
||||
reg: {
|
||||
...base,
|
||||
status: { tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'Op eigen verzoek' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Aantekening } from '@registratie/domain/registration';
|
||||
<tr>
|
||||
<td>{{ row.type }}</td>
|
||||
<td>{{ row.omschrijving }}</td>
|
||||
<td>{{ row.datum | date:'mediumDate' }}</td>
|
||||
<td>{{ row.datum | date: 'mediumDate' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
||||
@@ -12,7 +12,11 @@ export const Default: Story = {
|
||||
args: {
|
||||
rows: [
|
||||
{ type: 'Specialisme', omschrijving: 'Huisartsgeneeskunde', datum: '2016-04-12' },
|
||||
{ type: 'Aantekening', omschrijving: 'Erkend opleider huisartsgeneeskunde', datum: '2019-01-08' },
|
||||
{
|
||||
type: 'Aantekening',
|
||||
omschrijving: 'Erkend opleider huisartsgeneeskunde',
|
||||
datum: '2019-01-08',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user