import { Component, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { interval } from 'rxjs'; import { BffApiV1Service, type WerkbakItem } from 'api-client'; import { UtrechtComponentsModule } from 'ui'; /** * How often an open werkbak re-reads itself (S-26/#162, ADR-0032). Exported so the spec advances the * clock by exactly one interval instead of hard-coding the number. */ export const WERKBAK_REFRESH_MS = 5_000; /** The two decisions a behandelaar can make; the BFF validates these exact values (ADR-0013). */ type Besluit = 'goedkeuren' | 'afwijzen'; /** * The behandel werkbak: a signed-in behandelaar sees the registrations awaiting beoordeling (the open * Flowable `Beoordelen` tasks, read through the domain) and decides each — goedkeuren or afwijzen. A * decision posts to the BFF, which applies the domain transition and completes the workflow task * (ADR-0013; S-12). After a decision the werkbak refreshes so the handled item drops off the list. * * The page also re-reads itself every {@link WERKBAK_REFRESH_MS} while it is open, so a registration * that reaches beoordeling after the behandelaar opened the werkbak shows up on its own — no reload * (S-26/#162). Polling rather than a pushed stream: nothing notifies the BFF either, so a stream * would poll the domain in the BFF instead and add connection state for the same freshness (ADR-0032). */ @Component({ selector: 'app-werkbak-page', imports: [UtrechtComponentsModule], templateUrl: './werkbak-page.html', }) export class WerkbakPage { private readonly bff = inject(BffApiV1Service); protected readonly items = signal([]); protected readonly loading = signal(false); protected readonly loaded = signal(false); protected readonly failed = signal(false); protected readonly deciding = signal(undefined); constructor() { this.load(); // ponytail: a fixed interval, polled while the page lives — it keeps refreshing in a background // tab. Gate on `document.visibilityState` if the request volume ever matters. interval(WERKBAK_REFRESH_MS) .pipe(takeUntilDestroyed()) .subscribe(() => this.load({ background: true })); } /** * Read the werkbak. A `background` read is the interval refresh: it leaves the rows and the states * the behandelaar is looking at alone until it has an answer — no loading flash on every tick, and * a blip does not swap the list for the failure alert (the next tick recovers). Only a foreground * read — on open, or after a decision — speaks for whether the werkbak is readable at all. */ load(options: { background?: boolean } = {}): void { const background = options.background ?? false; if (!background) { this.loading.set(true); this.failed.set(false); } this.bff.getBehandelWerkbak().subscribe({ next: (rows: WerkbakItem[]) => { this.items.set(rows); this.loading.set(false); this.loaded.set(true); // A read that came back is the answer, so a refresh also clears an earlier failure — the // werkbak recovers on its own instead of showing the error until someone reloads. this.failed.set(false); }, // Surface the failure (e.g. 403 for a non-behandelaar) instead of swallowing it. error: () => { if (background) return; this.items.set([]); this.loading.set(false); this.loaded.set(true); this.failed.set(true); }, }); } decide(registrationId: string, besluit: Besluit): void { this.deciding.set(registrationId); this.bff.postBehandelRegistrationsIdDecide(registrationId, { besluit }).subscribe({ // Refresh so the decided registration drops off the werkbak (its task is now completed). next: () => { this.deciding.set(undefined); this.load(); }, error: () => { this.deciding.set(undefined); this.failed.set(true); }, }); } }