Make "at most one unsubmitted Concept per type" a server invariant instead of a
client-only convenience. ApplicationStore.Create → CreateConcept guards atomically
under the write gate and POST /applications returns 409 when a duplicate would be
created. The FE draft-sync recovers from the 409 by adopting the existing Concept
(ensureId → findConcept) rather than erroring — one-per-type means the second
attempt lands on the existing draft. Typed client regenerated (documents the 409).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the last-mile autosave gap: a debounced edit made in the final <600ms
before leaving a page was lost — the wizard draft-sync timer is cleared on
destroy without flushing, and root stores keep an armed timer the teardown
ignores.
New `shared/application/pending-saves.ts`: a root `PendingSaves` registry every
autosave owner joins (BriefStore, OrgTemplateStore, each createDraftSync). Two
seams flush through it — `flushPendingGuard` (CanDeactivate, on the five
autosave routes) awaits the pending write before an in-app route change; a
`beforeunload` handler (provideUnloadFlush) fires it best-effort and raises the
browser's native unsaved-changes prompt. ponytail: the HTTP seam is Angular
HttpClient (no keepalive/sendBeacon), so a hard-close flush can't be guaranteed
— hence the prompt; upgrade path noted in a comment. Each owner now nulls its
timer handle on fire so `hasPendingSave()` is accurate, and exposes
`flushPending()`.
Verified live against the running stack: navigating away 91ms after a keystroke
(well inside the debounce) fires one PUT /brief before the route changes; a
dirty reload raises the prompt, a clean reload does not. FE lint / check:tokens
/ 299 tests (+11) / build / build-storybook green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Lint-enforce two architecture rules that were only documented (ADR-0001),
landing the rules with the fixes so the build stays green:
- contracts/ imports nothing: dashboard-view.dto.ts is now pure wire shapes
(inline string-union enums, no domain imports). The DashboardView FE-view
type moves to the adapter, which maps wire → domain (compiler-enforced seam).
- ApiClient lives only in infrastructure: change-request-form (UI) no longer
injects ApiClient — a new ChangeRequestAdapter owns the client and the submit
becomes a createSubmitChangeRequest() command factory (createDraftSync shape).
draft-sync's wire-DTO import becomes type-only (allowed via allowTypeImports).
- Role type moves to shared/domain/role.ts; the ?role= reader stays in
shared/infrastructure/role.ts.
- eslint: contracts import-ban + @typescript-eslint/no-restricted-imports on
api-client (value-only; type imports permitted; infra + shared/upload exempt).
Also fixes a PRE-EXISTING bug found while verifying the flow: change-request-form
never imported FormsModule, so (ngSubmit) didn't bind and the submit button did a
native form submit (page reload) instead of submitting. Verified end-to-end in the
running app: submit → command → adapter → backend → reference, success alert shown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
draftSync.resume() does async network work and dispatched Seed on completion,
which could land after the user's first action and reset the machine (cursor +
fields) — the "click Volgende twice" symptom. Guard centrally: applyResume()
skips when the user already has progress (snapshot() != null) or there's nothing
to restore. onResume is now only ever called with a real draft on a pristine
machine, so the three wizard callbacks drop the dead `?? initial`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two dashboard bugs from the just-built feature.
1. Cancel didn't reflect until a browser refresh. ApplicationsStore now OWNS the
list in a writable RemoteData signal instead of projecting a resource() through
fromResource; cancel removes the row synchronously (guaranteed disappear, no
dependence on CD timing / HTTP cache / the reloading gap), then confirms the
DELETE (rollback on failure, no resync). Adapter gains list(); applicationsResource()
removed. Shared fromResource/remote-data.ts deliberately untouched.
2. Duplicate / inconsistent Concepts per type. createDraftSync.resume() now: a
?aanvraag link wins; else it resumes THIS type's existing Concept (loads its
draft); else fresh. ensureId is gated behind resume so a fast typist can't create
a duplicate before the lookup lands. restart()/reset() deletes the current Concept
(submitted → 409, kept) so there's at most one active Concept per type. A non-Concept
id can't reopen as an editable draft. Backend unchanged.
Gates green: lint, vitest 128, build, check:tokens, backend dotnet 56.
Wiring is not unit-covered — needs live verification (see plan).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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>