From 9f217abe19596854cebf6bc371a605d769b9b963 Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Wed, 1 Jul 2026 14:25:02 +0200 Subject: [PATCH] Mijn aanvragen (E): two-flow submit through the aanvraag + all-wizard persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three wizards now submit through the backend aanvraag lifecycle, so a submitted Concept actually transitions (dashboard shows it correctly in F). - blockActions(status) (domain + spec): the pure per-status action decision (Concept → resume/cancel; In behandeling → viewDocuments; resolved → none). - createDraftSync.submit(): ensure the Concept exists, then POST /applications/{id}/submit; folded into a Result like the old commands. - registratie: submit via draftSync (duo → auto, handmatig → manual pending — the old 422 path is gone from the wizard). - intake + herregistratie: adopt createDraftSync (persistence + resume-by-link); intake retires sessionStorage `intake-v3`; herregistratie gains persistence. Both submit through the aanvraag too. hasProgress added to each machine (+spec). - Delete now-dead submit-registratie/submit-intake/submit-herregistratie commands. Deferred: the old /registrations, /intakes, /herregistraties backend endpoints + RejectRegistratie are now unused by the FE but still present (+ tested) — retiring them cascades into backend test rewrites, so it's a focused follow-up cleanup. Gates green: vitest 128, lint, build; backend unchanged (dotnet 56). Co-Authored-By: Claude Opus 4.8 --- .../application/submit-herregistratie.ts | 15 ------ .../application/submit-intake.spec.ts | 26 ---------- .../application/submit-intake.ts | 15 ------ .../herregistratie-has-progress.spec.ts | 15 ++++++ .../domain/herregistratie.machine.ts | 5 ++ .../domain/intake-has-progress.spec.ts | 15 ++++++ .../herregistratie/domain/intake.machine.ts | 6 +++ .../herregistratie-wizard.component.ts | 34 +++++++++---- .../intake-wizard/intake-wizard.component.ts | 49 ++++++++----------- src/app/registratie/application/draft-sync.ts | 10 ++++ .../application/submit-registratie.spec.ts | 27 ---------- .../application/submit-registratie.ts | 17 ------- .../registratie/domain/block-actions.spec.ts | 17 +++++++ src/app/registratie/domain/block-actions.ts | 18 +++++++ .../registratie-wizard.component.ts | 11 ++--- 15 files changed, 136 insertions(+), 144 deletions(-) delete mode 100644 src/app/herregistratie/application/submit-herregistratie.ts delete mode 100644 src/app/herregistratie/application/submit-intake.spec.ts delete mode 100644 src/app/herregistratie/application/submit-intake.ts create mode 100644 src/app/herregistratie/domain/herregistratie-has-progress.spec.ts create mode 100644 src/app/herregistratie/domain/intake-has-progress.spec.ts delete mode 100644 src/app/registratie/application/submit-registratie.spec.ts delete mode 100644 src/app/registratie/application/submit-registratie.ts create mode 100644 src/app/registratie/domain/block-actions.spec.ts create mode 100644 src/app/registratie/domain/block-actions.ts diff --git a/src/app/herregistratie/application/submit-herregistratie.ts b/src/app/herregistratie/application/submit-herregistratie.ts deleted file mode 100644 index ff41329..0000000 --- a/src/app/herregistratie/application/submit-herregistratie.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Result } from '@shared/kernel/fp'; -import { Valid } from '../domain/herregistratie.machine'; -import { ApiClient } from '@shared/infrastructure/api-client'; -import { runSubmit, SUBMIT_FAILED } from '@shared/application/submit'; - -/** - * Command: POST a herregistratie application to the backend (`/api/herregistraties`). - * The "uren must be > 0" rule lives server-side; a 422 ProblemDetails is surfaced - * as the error by `runSubmit`. - */ -export function submitHerregistratie(client: ApiClient, data: Valid): Promise> { - return runSubmit(async () => { - await client.herregistraties({ uren: data.uren, documents: data.documents }); - }, SUBMIT_FAILED); -} diff --git a/src/app/herregistratie/application/submit-intake.spec.ts b/src/app/herregistratie/application/submit-intake.spec.ts deleted file mode 100644 index 0305e02..0000000 --- a/src/app/herregistratie/application/submit-intake.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { submitIntake } from './submit-intake'; -import { ApiClient } from '@shared/infrastructure/api-client'; -import { ValidIntake } from '../domain/intake.machine'; - -// submit-herregistratie shares this exact shape (POST uren → Result); covered here. -const data = { uren: 40 } as unknown as ValidIntake; - -describe('submitIntake', () => { - it('returns ok on success', async () => { - const client = { intakes: async () => ({ referentie: 'BIG-2026-1' }) } as unknown as ApiClient; - const r = await submitIntake(client, data); - expect(r.ok).toBe(true); - }); - - it('returns err with the ProblemDetails detail when the server rejects', async () => { - const client = { - intakes: async () => { - throw { detail: 'Aanvraag afgewezen: geen gewerkte uren geregistreerd.', status: 422 }; - }, - } as unknown as ApiClient; - const r = await submitIntake(client, data); - expect(r.ok).toBe(false); - expect(r).toMatchObject({ error: expect.stringContaining('geen gewerkte uren') }); - }); -}); diff --git a/src/app/herregistratie/application/submit-intake.ts b/src/app/herregistratie/application/submit-intake.ts deleted file mode 100644 index 857b292..0000000 --- a/src/app/herregistratie/application/submit-intake.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Result } from '@shared/kernel/fp'; -import { ValidIntake } from '../domain/intake.machine'; -import { ApiClient } from '@shared/infrastructure/api-client'; -import { runSubmit, SUBMIT_FAILED } from '@shared/application/submit'; - -/** - * Command: POST the intake questionnaire to the backend (`/api/intakes`). The - * "uren must be > 0" rule lives server-side; a 422 ProblemDetails is surfaced as - * the error by `runSubmit`. - */ -export function submitIntake(client: ApiClient, data: ValidIntake): Promise> { - return runSubmit(async () => { - await client.intakes({ uren: data.uren }); - }, SUBMIT_FAILED); -} diff --git a/src/app/herregistratie/domain/herregistratie-has-progress.spec.ts b/src/app/herregistratie/domain/herregistratie-has-progress.spec.ts new file mode 100644 index 0000000..74a4156 --- /dev/null +++ b/src/app/herregistratie/domain/herregistratie-has-progress.spec.ts @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest'; +import { hasProgress, initial, WizardState } from './herregistratie.machine'; + +const editing = initial as Extract; + +describe('herregistratie hasProgress', () => { + it('is false for a fresh form', () => { + expect(hasProgress(editing)).toBe(false); + }); + + it('is true once a field is filled or the user advances', () => { + expect(hasProgress({ ...editing, draft: { uren: '40', jaren: '', punten: '' } })).toBe(true); + expect(hasProgress({ ...editing, step: 2 })).toBe(true); + }); +}); diff --git a/src/app/herregistratie/domain/herregistratie.machine.ts b/src/app/herregistratie/domain/herregistratie.machine.ts index 5bb1a95..388c920 100644 --- a/src/app/herregistratie/domain/herregistratie.machine.ts +++ b/src/app/herregistratie/domain/herregistratie.machine.ts @@ -40,6 +40,11 @@ export type WizardState = export const initial: WizardState = { tag: 'Editing', step: 1, draft: { uren: '', jaren: '', punten: '' }, errors: {}, upload: initialUpload }; +/** Has the user meaningfully started, so it's worth persisting as a Concept? */ +export function hasProgress(s: Extract): boolean { + return s.step > 1 || !!s.draft.uren || !!s.draft.jaren || !!s.draft.punten || deliveryRefs(s.upload).some((r) => r.channel === 'digital' && !!r.documentId); +} + /** Parse every field; on success hand back a Valid, else the per-field errors. */ function validate(draft: Draft, upload: UploadState): Result { const uren = parseUren(draft.uren); diff --git a/src/app/herregistratie/domain/intake-has-progress.spec.ts b/src/app/herregistratie/domain/intake-has-progress.spec.ts new file mode 100644 index 0000000..ca0aa6c --- /dev/null +++ b/src/app/herregistratie/domain/intake-has-progress.spec.ts @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest'; +import { hasProgress, initial, IntakeState } from './intake.machine'; + +const answering = initial as Extract; + +describe('intake hasProgress', () => { + it('is false for a fresh questionnaire', () => { + expect(hasProgress(answering)).toBe(false); + }); + + it('is true once an answer is given or the user advances', () => { + expect(hasProgress({ ...answering, answers: { buitenlandGewerkt: 'ja' } })).toBe(true); + expect(hasProgress({ ...answering, cursor: 1 })).toBe(true); + }); +}); diff --git a/src/app/herregistratie/domain/intake.machine.ts b/src/app/herregistratie/domain/intake.machine.ts index 3131949..f3e8d1e 100644 --- a/src/app/herregistratie/domain/intake.machine.ts +++ b/src/app/herregistratie/domain/intake.machine.ts @@ -68,6 +68,12 @@ export function currentStep(s: Extract): Step return STEPS[Math.min(s.cursor, STEPS.length - 1)]; } +/** Has the user meaningfully started, so it's worth persisting as a Concept? + (No auto-prefill here — pristine means truly untouched.) */ +export function hasProgress(s: Extract): boolean { + return s.cursor > 0 || Object.keys(s.answers).length > 0; +} + /** Validate every question currently visible in ONE step. Errors keyed per field. */ function validateStep(step: StepId, a: Answers, scholingThreshold: number): Result { const errors: Errors = {}; diff --git a/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts b/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts index 7b356f3..05f9863 100644 --- a/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts +++ b/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts @@ -7,12 +7,11 @@ import { WizardShellComponent, WizardError, WizardStatus } from '@shared/layout/ import { createStore } from '@shared/application/store'; import { whenTag } from '@shared/kernel/fp'; import { BigProfileStore } from '@registratie/application/big-profile.store'; -import { WizardState, WizardMsg, Draft, initial, reduce } from '@herregistratie/domain/herregistratie.machine'; -import { submitHerregistratie } from '@herregistratie/application/submit-herregistratie'; -import { ApiClient } from '@shared/infrastructure/api-client'; +import { WizardState, WizardMsg, Draft, initial, reduce, hasProgress } from '@herregistratie/domain/herregistratie.machine'; +import { createDraftSync } from '@registratie/application/draft-sync'; import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component'; import { createUploadController } from '@shared/upload/upload-controller'; -import { UploadState, initialUpload } from '@shared/upload/upload.machine'; +import { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload.machine'; /** Organism: multi-step herregistratie wizard. ALL state lives in one signal driven by the pure `reduce` function (see herregistratie.machine.ts) via an @@ -77,7 +76,6 @@ import { UploadState, initialUpload } from '@shared/upload/upload.machine'; }) export class HerregistratieWizardComponent { private profile = inject(BigProfileStore); - private apiClient = inject(ApiClient); private store = createStore(initial, reduce); /** Optional seed so Storybook / the showcase can mount any state directly. */ @@ -86,6 +84,20 @@ export class HerregistratieWizardComponent { readonly state = this.store.model; // public so the showcase can highlight the live state protected dispatch = this.store.dispatch; + // Backend draft-sync (new persistence for this wizard): create a Concept on first + // progress, debounced-sync the snapshot, resume by `?aanvraag=`. + private draftSync = createDraftSync({ + type: 'herregistratie', + snapshot: () => { + const s = this.state(); + if (s.tag !== 'Editing' || !hasProgress(s)) return null; + const documentIds = deliveryRefs(s.upload).filter((r) => r.channel === 'digital' && r.documentId).map((r) => r.documentId!); + return { draft: s, stepIndex: s.step - 1, stepCount: this.stepLabels.length, documentIds }; + }, + onResume: (draft) => this.dispatch({ tag: 'Seed', state: (draft as WizardState | null) ?? initial }), + enabled: () => this.seed() === initial, + }); + // Stepper labels + per-step heading titles (presentational only). readonly stepLabels = [$localize`:@@herregWizard.step.werkervaring:Werkervaring`, $localize`:@@herregWizard.step.nascholing:Nascholing`, $localize`:@@herregWizard.step.documenten:Documenten`]; private stepTitles = [$localize`:@@herregWizard.title.werkervaring:Werkervaring (afgelopen 5 jaar)`, $localize`:@@herregWizard.title.nascholing:Nascholing`, $localize`:@@herregWizard.title.documenten:Documenten aanleveren`]; @@ -126,7 +138,10 @@ export class HerregistratieWizardComponent { }); constructor() { - queueMicrotask(() => this.dispatch({ tag: 'Seed', state: this.seed() })); + // An explicit seed (stories/tests) wins; otherwise resume the backend draft + // (`?aanvraag=`) 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())); } onPrimary() { @@ -143,16 +158,17 @@ export class HerregistratieWizardComponent { /** Reset the wizard to a fresh, empty start. */ restart() { + this.draftSync.reset(); this.dispatch({ tag: 'Seed', state: initial }); } - /** The effect: when we entered Submitting, call the backend command, flip the - optimistic cross-page flag, then dispatch the result (and commit/rollback). */ + /** The effect: when we entered Submitting, submit through the aanvraag lifecycle, + flip the optimistic cross-page flag, then dispatch the result (commit/rollback). */ private async runIfSubmitting() { const s = this.state(); if (s.tag !== 'Submitting') return; this.profile.beginHerregistratie(); - const r = await submitHerregistratie(this.apiClient, s.data); + const r = await this.draftSync.submit({ uren: s.data.uren, documents: s.data.documents }); if (r.ok) { this.dispatch({ tag: 'SubmitConfirmed' }); this.profile.confirmHerregistratie(); diff --git a/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts b/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts index 551be97..3cb4523 100644 --- a/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts +++ b/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts @@ -19,14 +19,12 @@ import { reduce, STEPS, lageUren, + hasProgress, SCHOLING_THRESHOLD_DEFAULT, } from '@herregistratie/domain/intake.machine'; -import { submitIntake } from '@herregistratie/application/submit-intake'; -import { ApiClient } from '@shared/infrastructure/api-client'; +import { createDraftSync } from '@registratie/application/draft-sync'; import { IntakePolicyAdapter } from '@herregistratie/infrastructure/intake-policy.adapter'; -const STORAGE_KEY = 'intake-v3'; // ponytail: bump the suffix if the persisted state shape changes; no migration. - /** Organism: a BRANCHING intake questionnaire. All state lives in one signal driven by the pure `reduce` (intake.machine.ts). Which step renders is derived from the answers via `visibleSteps`, never stored — so editing an earlier @@ -111,7 +109,6 @@ const STORAGE_KEY = 'intake-v3'; // ponytail: bump the suffix if the persisted s }) export class IntakeWizardComponent { private profile = inject(BigProfileStore); - private apiClient = inject(ApiClient); private policy = inject(IntakePolicyAdapter); private store = createStore(initial, reduce); @@ -126,6 +123,18 @@ export class IntakeWizardComponent { readonly state = this.store.model; readonly dispatch = this.store.dispatch; + // Backend draft-sync (replaces sessionStorage); the intake has no uploads. + private draftSync = createDraftSync({ + type: 'intake', + snapshot: () => { + const s = this.state(); + if (s.tag !== 'Answering' || !hasProgress(s)) return null; + return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds: [] }; + }, + onResume: (draft) => this.dispatch({ tag: 'Seed', state: (draft as IntakeState | null) ?? initial }), + enabled: () => this.seed() === initial, + }); + private answering = computed(() => whenTag(this.state(), 'Answering')); /** Public so the showcase can render the (fixed) step list next to the wizard. */ readonly steps = STEPS; @@ -169,16 +178,10 @@ export class IntakeWizardComponent { protected set = (key: keyof Answers, value: string) => this.dispatch({ tag: 'SetAnswer', key, value }); constructor() { - // An explicit seed (stories) wins; otherwise resume from sessionStorage. + // An explicit seed (stories/tests) wins; otherwise resume the backend draft + // (`?aanvraag=`) or start fresh. Persistence is the draftSync controller's job. const seeded = this.seed(); - queueMicrotask(() => this.dispatch({ tag: 'Seed', state: seeded !== initial ? seeded : (this.restore() ?? initial) })); - // Persist only while answering; clear once the flow is done. G1: sessionStorage - // (not localStorage) — the draft holds personal data and must not outlive the tab. - effect(() => { - const s = this.state(); - if (s.tag === 'Answering') sessionStorage.setItem(STORAGE_KEY, JSON.stringify(s)); - else sessionStorage.removeItem(STORAGE_KEY); - }); + queueMicrotask(() => (seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume())); // Apply the server-owned threshold into machine state as it arrives. Track // only the policy value; untrack the dispatch (it reads the state signal // internally, which would otherwise make this effect loop on its own write). @@ -188,17 +191,6 @@ export class IntakeWizardComponent { }); } - private restore(): IntakeState | null { - const raw = sessionStorage.getItem(STORAGE_KEY); - if (!raw) return null; - try { - const parsed = JSON.parse(raw) as IntakeState; - return parsed?.tag === 'Answering' ? parsed : null; // G2: only resume a known shape. - } catch { - return null; // ponytail: corrupt entry -> start fresh, no migration. - } - } - onPrimary() { const s = this.state(); if (s.tag !== 'Answering') return; @@ -212,16 +204,17 @@ export class IntakeWizardComponent { } restart() { + this.draftSync.reset(); this.dispatch({ tag: 'Seed', state: initial }); } - /** The effect: when we enter Submitting, call the backend, flip the optimistic - cross-page flag, then dispatch the outcome (and commit/rollback). */ + /** The effect: when we enter Submitting, submit through the aanvraag lifecycle, + flip the optimistic cross-page flag, then dispatch the outcome (commit/rollback). */ private async runIfSubmitting() { const s = this.state(); if (s.tag !== 'Submitting') return; this.profile.beginHerregistratie(); - const r = await submitIntake(this.apiClient, s.data); + const r = await this.draftSync.submit({ uren: s.data.uren }); if (r.ok) { this.dispatch({ tag: 'SubmitConfirmed' }); this.profile.confirmHerregistratie(); diff --git a/src/app/registratie/application/draft-sync.ts b/src/app/registratie/application/draft-sync.ts index ffa5fe2..e86e896 100644 --- a/src/app/registratie/application/draft-sync.ts +++ b/src/app/registratie/application/draft-sync.ts @@ -1,5 +1,8 @@ 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 { SubmitApplicationRequest, SubmitApplicationResponse } from '@shared/infrastructure/api-client'; import { AanvraagType } from '@registratie/domain/aanvraag'; import { ApplicationsAdapter } from '@registratie/infrastructure/applications.adapter'; @@ -94,6 +97,13 @@ export function createDraftSync(deps: DraftSyncDeps) { .catch(() => deps.onResume(null)); // unknown/deleted id → start fresh }, + /** Submit through the aanvraag lifecycle: ensure the Concept exists, then + `POST /applications/{id}/submit` (server sets autoApprovable + transitions). + Folded into a Result like the old submit-* commands. */ + submit(body: SubmitApplicationRequest): Promise> { + return runSubmit(async () => adapter.submit(await ensureId(), body), SUBMIT_FAILED); + }, + /** Detach from the current Concept (a new one is created on next progress) and drop the `?aanvraag` link. Used when the wizard restarts. */ reset() { diff --git a/src/app/registratie/application/submit-registratie.spec.ts b/src/app/registratie/application/submit-registratie.spec.ts deleted file mode 100644 index d07573b..0000000 --- a/src/app/registratie/application/submit-registratie.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { submitRegistratie } from './submit-registratie'; -import { ApiClient } from '@shared/infrastructure/api-client'; -import { ValidRegistratie } from '@registratie/domain/registratie-wizard.machine'; - -// Mocked at the client boundary: the rule itself lives server-side now, so these -// tests only assert that the command maps the client's response onto a Result. -const data = { diplomaHerkomst: 'duo' } as unknown as ValidRegistratie; - -describe('submitRegistratie', () => { - it('returns ok with the server reference on success', async () => { - const client = { registrations: async () => ({ referentie: 'BIG-2026-123456' }) } as unknown as ApiClient; - const r = await submitRegistratie(client, data); - expect(r).toEqual({ ok: true, value: 'BIG-2026-123456' }); - }); - - it('returns err with the ProblemDetails detail when the server rejects (422)', async () => { - const client = { - registrations: async () => { - throw { detail: 'Handmatig diploma kan niet automatisch worden geverifieerd.', status: 422 }; - }, - } as unknown as ApiClient; - const r = await submitRegistratie(client, data); - expect(r.ok).toBe(false); - expect(r).toMatchObject({ error: expect.stringContaining('Handmatig diploma') }); - }); -}); diff --git a/src/app/registratie/application/submit-registratie.ts b/src/app/registratie/application/submit-registratie.ts deleted file mode 100644 index b3cdcff..0000000 --- a/src/app/registratie/application/submit-registratie.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Result } from '@shared/kernel/fp'; -import { ValidRegistratie } from '@registratie/domain/registratie-wizard.machine'; -import { ApiClient } from '@shared/infrastructure/api-client'; -import { runSubmit, SUBMIT_FAILED } from '@shared/application/submit'; - -/** - * Command: POST the completed registration to the backend (`/api/registrations`), - * which re-validates and decides. The rule that a manually entered diploma cannot - * be auto-verified lives server-side (surfaced as a 422 by `runSubmit`). On - * success it yields the server-generated confirmation reference (PRD §9). - */ -export function submitRegistratie(client: ApiClient, data: ValidRegistratie): Promise> { - return runSubmit(async () => { - const res = await client.registrations({ diplomaHerkomst: data.diplomaHerkomst, documents: data.documents }); - return res.referentie ?? ''; - }, SUBMIT_FAILED); -} diff --git a/src/app/registratie/domain/block-actions.spec.ts b/src/app/registratie/domain/block-actions.spec.ts new file mode 100644 index 0000000..8fc51ae --- /dev/null +++ b/src/app/registratie/domain/block-actions.spec.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +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']); + }); + + it('an in-behandeling aanvraag only exposes its documents', () => { + expect(blockActions({ tag: 'InBehandeling', referentie: 'BIG-1', manual: true })).toEqual(['viewDocuments']); + }); + + it('resolved aanvragen have no actions', () => { + expect(blockActions({ tag: 'Goedgekeurd', referentie: 'BIG-1' })).toEqual([]); + expect(blockActions({ tag: 'Afgewezen', referentie: 'BIG-1', reden: 'x' })).toEqual([]); + }); +}); diff --git a/src/app/registratie/domain/block-actions.ts b/src/app/registratie/domain/block-actions.ts new file mode 100644 index 0000000..457a2b6 --- /dev/null +++ b/src/app/registratie/domain/block-actions.ts @@ -0,0 +1,18 @@ +import { AanvraagStatus } from './aanvraag'; + +/** What a dashboard "Mijn aanvragen" block offers per status. The badge itself + follows directly from `status.tag` (the UI maps tag → colour + label), so this + pure function owns only the *actions* decision. */ +export type BlockAction = 'resume' | 'cancel' | 'viewDocuments'; + +export function blockActions(status: AanvraagStatus): BlockAction[] { + switch (status.tag) { + case 'Concept': + return ['resume', 'cancel']; + case 'InBehandeling': + return ['viewDocuments']; + case 'Goedgekeurd': + case 'Afgewezen': + return []; + } +} diff --git a/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts b/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts index 511a431..2dadda6 100644 --- a/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts +++ b/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts @@ -28,9 +28,7 @@ import { hasProgress, STEPS, } from '@registratie/domain/registratie-wizard.machine'; -import { submitRegistratie } from '@registratie/application/submit-registratie'; import { createDraftSync } from '@registratie/application/draft-sync'; -import { ApiClient } from '@shared/infrastructure/api-client'; import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component'; import { createUploadController } from '@shared/upload/upload-controller'; import { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload.machine'; @@ -185,7 +183,6 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed" export class RegistratieWizardComponent { private brp = inject(BrpAdapter); private duo = inject(DuoAdapter); - private apiClient = inject(ApiClient); private store = createStore(initial, reduce); protected adresRes = this.brp.adresResource(); @@ -375,13 +372,13 @@ export class RegistratieWizardComponent { this.adresRes.reload(); } - /** The effect: when we enter Indienen, call the backend, then dispatch the - outcome (success carries the confirmation reference). */ + /** The effect: when we enter Indienen, submit through the aanvraag lifecycle + (duo → auto-approve, handmatig → manual), then dispatch the outcome. */ private async runIfIndienen() { const s = this.state(); if (s.tag !== 'Indienen') return; - const r = await submitRegistratie(this.apiClient, s.data); - if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value }); + 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 }); } }