Mijn aanvragen (F1): ApplicationsStore + dashboard blocks

The payoff — A–E become visible. The dashboard now shows a "Mijn aanvragen"
section at the top with a block per application.

- ApplicationsStore (registratie/application, root): the list as a RemoteData
  signal, parsed at the trust boundary; reload() (dashboard revisit reflects
  server-computed auto-approval); optimistic cancel (hide → reload / un-hide on fail).
- aanvraag-block (organism): badge (tag → colour/label) + per-status body
  ("Stap X van Y" / referentie + ingediend-datum / manual note / reden) + actions
  from the pure blockActions. Composes card + status-badge + button. Stories per status.
- dashboard: "Mijn aanvragen" section (hidden when empty), sorted Concept → In
  behandeling → resolved; Verder gaan deep-links the wizard (?aanvraag=<id>),
  Annuleren cancels via the store.

Deferred to F2: document-chip preview/download affordance.
Gates green: vitest 128, lint, build, check:tokens; backend dotnet 56.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-01 14:43:36 +02:00
co-authored by Claude Opus 4.8
parent 9f217abe19
commit 0bef08e5b3
4 changed files with 231 additions and 1 deletions
@@ -0,0 +1,51 @@
import { Injectable, computed, inject, signal } from '@angular/core';
import { RemoteData, fromResource } from '@shared/application/remote-data';
import { Aanvraag } from '@registratie/domain/aanvraag';
import { ApplicationsAdapter, parseApplications } from '@registratie/infrastructure/applications.adapter';
type Err = Error | undefined;
/**
* The dashboard's view of the user's applications (aanvragen) — the backend is the
* system of record (PRD 0001). One root singleton owns the list resource and exposes
* it as a RemoteData signal, validated at the trust boundary (DTO → domain). Cancel
* is optimistic (hide immediately, reload to confirm, un-hide on failure); `reload`
* re-fetches so a page revisit reflects auto-approval (Concept → In behandeling →
* Goedgekeurd is computed server-side on read).
*/
@Injectable({ providedIn: 'root' })
export class ApplicationsStore {
private adapter = inject(ApplicationsAdapter);
private res = this.adapter.applicationsResource();
/** Ids optimistically hidden while their cancel is in flight (or done). */
private cancelling = signal<ReadonlySet<string>>(new Set());
readonly applications = computed<RemoteData<Err, Aanvraag[]>>(() => {
const rd = fromResource(this.res);
if (rd.tag !== 'Success') return rd;
const parsed = parseApplications(rd.value ?? []);
if (!parsed.ok) return { tag: 'Failure', error: new Error(parsed.error) };
const hidden = this.cancelling();
return { tag: 'Success', value: parsed.value.filter((a) => !hidden.has(a.id)) };
});
/** Re-fetch (e.g. on dashboard revisit) so auto-approval transitions show up. */
reload() {
this.res.reload();
}
/** Cancel a Concept: hide it optimistically, then confirm (reload) or roll back. */
async cancel(id: string) {
this.cancelling.update((s) => new Set(s).add(id));
try {
await this.adapter.cancel(id);
this.res.reload(); // the reloaded list no longer contains it; the hide is now a no-op
} catch {
this.cancelling.update((s) => {
const next = new Set(s);
next.delete(id);
return next; // roll back: the block reappears
});
}
}
}