import { Injectable, inject, signal } from '@angular/core'; import { RemoteData } from '@shared/application/remote-data'; import { runSubmit, SUBMIT_FAILED } from '@shared/application/submit'; 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 as a writable * RemoteData signal (CLAUDE.md §3: change state only through methods). Cancel removes * the row SYNCHRONOUSLY, so the block disappears deterministically — no dependence on * change-detection timing, HTTP caching, or a resource `reload()`. `reload()` re-fetches * so a page revisit reflects auto-approval (Concept → In behandeling → Goedgekeurd is * computed server-side on read). Cancel goes through `runSubmit` and rolls back plus * surfaces `lastError` on failure (RB-20). */ @Injectable({ providedIn: 'root' }) export class ApplicationsStore { private adapter = inject(ApplicationsAdapter); private state = signal>({ tag: 'Loading' }); readonly applications = this.state.asReadonly(); /** Set on a failed cancel (RB-20): the optimistic removal already rolled back by then, this is only the message for the alert the page renders above the list. */ private error = signal(null); readonly lastError = this.error.asReadonly(); constructor() { void this.load(); } /** Fetch + parse at the trust boundary, then publish as RemoteData. Keeps the last-good value on a resync (only shows Loading on the first load). */ async load() { 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) }, ); } catch (e) { this.state.set({ tag: 'Failure', error: e as Error }); } } /** Re-fetch (e.g. on dashboard revisit) so auto-approval transitions show up. */ reload() { void this.load(); } /** Cancel a Concept: drop it now (synchronous, guaranteed), then confirm the DELETE. No resync — the delete succeeded, so the optimistic removal is authoritative. On failure, roll back AND surface the error (RB-20) — a silent reappearance leaves the user guessing why the block came back. */ async cancel(id: string) { const before = this.state(); if (before.tag === 'Success') { this.state.set({ tag: 'Success', value: before.value.filter((a) => a.id !== id) }); } this.error.set(null); const r = await runSubmit(() => this.adapter.cancel(id), SUBMIT_FAILED); if (!r.ok) { this.state.set(before); // roll back: the block reappears this.error.set(r.error); } } }