ApplicationsStore.cancel and AdminCasesStore.delete rolled an optimistic write back on failure but showed no message — a bare catch with no Result and no error channel (CQ-002). Both now call runSubmit and set a lastError signal on failure, mirroring createSubmitChangeRequest in the same folder. Each page renders the error with the existing app-alert atom, the same pattern brief.page.ts already uses for lastError. Added a spec file for ApplicationsStore (none existed) and extended AdminCasesStore's spec, each asserting the rollback AND the surfaced error. Verified both new assertions fail without the fix (an Edit undo/redo of the store method, not git checkout). Regenerated libs/shared/docs/behaviour-spec.mdx (gen:behaviour-spec) to pick up the new/renamed test names. Marked RB-20 done in 99-backlog.md and recorded the change in implementation/rb-20.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
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;
|
|
|
|
/**
|
|
* Admin view of ALL cases across owners (WP-36; `cases:manage`) — the back-office
|
|
* counterpart of the user-facing `ApplicationsStore`. Same shape: one root singleton
|
|
* owns the list as a writable RemoteData signal, delete removes the row synchronously
|
|
* (optimistic), goes through `runSubmit`, and rolls back plus surfaces `lastError` on
|
|
* failure (RB-20). Admin delete removes any case (any owner, submitted or not — the
|
|
* server enforces the capability).
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class AdminCasesStore {
|
|
private adapter = inject(ApplicationsAdapter);
|
|
|
|
private state = signal<RemoteData<Err, Aanvraag[]>>({ tag: 'Loading' });
|
|
readonly cases = this.state.asReadonly();
|
|
|
|
/** Set on a failed delete (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<string | null>(null);
|
|
readonly lastError = this.error.asReadonly();
|
|
|
|
/** 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.listAll());
|
|
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 });
|
|
}
|
|
}
|
|
|
|
reload() {
|
|
void this.load();
|
|
}
|
|
|
|
/** Delete a case: drop it now (synchronous), then confirm the DELETE; roll back on error
|
|
AND surface it (RB-20) — a silent reappearance leaves the admin guessing why. */
|
|
async delete(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.deleteAny(id), SUBMIT_FAILED);
|
|
if (!r.ok) {
|
|
this.state.set(before); // roll back: the row reappears
|
|
this.error.set(r.error);
|
|
}
|
|
}
|
|
}
|