Mijn aanvragen (D): backend draft-sync + resume-by-link (registratie slice)
Replaces the registratie wizard's sessionStorage draft with a backend-owned Concept aanvraag (PRD 0001, phase D — the registratie vertical slice). - createDraftSync (registratie/application): reusable controller (field-initializer idiom, like createUploadController). Creates the Concept lazily on first progress, stamps `?aanvraag=<id>` into the URL, debounced-syncs the machine snapshot per change, and resumes from `?aanvraag` on load. Inert without a Router or when an explicit seed is present (Storybook/tests) — no network there. - hasProgress (machine, pure + spec): "worth persisting?" — excludes the automatic BRP address prefill so a bare page visit creates nothing. Accepted regression: a step-0-only address edit isn't persisted until the user advances/chooses. - Wizard: dropped STORAGE_KEY/restore + the sessionStorage effect; restart() detaches the Concept and drops the link. Deferred (noted): ApplicationsStore -> phase F (dashboard is its only consumer); intake-v3 + herregistratie persistence -> phase E (copy this pattern). Gates green: vitest 125, lint, build; backend unchanged (dotnet 56). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,15 +25,16 @@ import {
|
||||
StepId,
|
||||
initial,
|
||||
reduce,
|
||||
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 } from '@shared/upload/upload.machine';
|
||||
import { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload.machine';
|
||||
|
||||
const STORAGE_KEY = 'registratie-v2'; // ponytail: bump the suffix if the persisted shape changes; no migration.
|
||||
const KANALEN = [
|
||||
{ value: 'email', label: $localize`:@@registratie.kanaalEmail:E-mail` },
|
||||
{ value: 'post', label: $localize`:@@registratie.kanaalPost:Post` },
|
||||
@@ -43,8 +44,9 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
|
||||
/** Organism: the BIG-registration wizard. All state lives in one signal driven by
|
||||
the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the
|
||||
draft via an effect; the DUO diploma list renders through <app-async>; choosing
|
||||
a diploma reveals its server-derived beroep. The draft is persisted to
|
||||
sessionStorage so a reload keeps the user's progress (cleared on tab close). Built from existing
|
||||
a diploma reveals its server-derived beroep. The draft is persisted to the
|
||||
backend as a Concept aanvraag (createDraftSync) so a reload — or a "Verder gaan"
|
||||
from the dashboard via `?aanvraag=<id>` — resumes progress. Built from existing
|
||||
atoms/molecules. */
|
||||
@Component({
|
||||
selector: 'app-registratie-wizard',
|
||||
@@ -207,6 +209,19 @@ export class RegistratieWizardComponent {
|
||||
getUpload: () => this.upload(),
|
||||
dispatch: (msg) => this.dispatch({ tag: 'Upload', msg }),
|
||||
});
|
||||
// Backend draft-sync (replaces sessionStorage): create a Concept once the user has
|
||||
// made progress, then debounced-sync the whole machine snapshot; resume by `?aanvraag`.
|
||||
private draftSync = createDraftSync({
|
||||
type: 'registratie',
|
||||
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!);
|
||||
return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds };
|
||||
},
|
||||
onResume: (draft) => this.dispatch({ tag: 'Seed', state: (draft as RegistratieState | null) ?? initial }),
|
||||
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 referentie = computed(() => whenTag(this.state(), 'Ingediend')?.referentie ?? '');
|
||||
@@ -315,16 +330,10 @@ export class RegistratieWizardComponent {
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// An explicit seed (stories) wins; otherwise resume from sessionStorage.
|
||||
// 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(() => this.dispatch({ tag: 'Seed', state: seeded !== initial ? seeded : (this.restore() ?? initial) }));
|
||||
// Persist only while filling in; clear once the flow is done. G1: sessionStorage
|
||||
// (not localStorage) — the draft holds address/email and must not outlive the tab.
|
||||
effect(() => {
|
||||
const s = this.state();
|
||||
if (s.tag === 'Invullen') sessionStorage.setItem(STORAGE_KEY, JSON.stringify(s));
|
||||
else sessionStorage.removeItem(STORAGE_KEY);
|
||||
});
|
||||
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 resource
|
||||
// value; untrack the dispatch (it reads the state signal, which would otherwise
|
||||
// make this effect loop on its own write). Don't clobber edits/restored data.
|
||||
@@ -346,17 +355,6 @@ export class RegistratieWizardComponent {
|
||||
// failed submit) now lives in the shared WizardShellComponent.
|
||||
}
|
||||
|
||||
private restore(): RegistratieState | null {
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as RegistratieState;
|
||||
return parsed?.tag === 'Invullen' ? 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 !== 'Invullen') return;
|
||||
@@ -372,6 +370,7 @@ export class RegistratieWizardComponent {
|
||||
/** Reset the wizard to a fresh start. Reload the BRP lookup so the address
|
||||
re-prefills, keeping the form and the "vooraf ingevuld" note consistent. */
|
||||
restart() {
|
||||
this.draftSync.reset(); // detach from the old Concept; next progress creates a new one
|
||||
this.dispatch({ tag: 'Seed', state: initial });
|
||||
this.adresRes.reload();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user