Refactor/readable dashboard #2
@@ -1,6 +1,6 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import { ApiClient, ApplicationSummaryDto } from '@shared/infrastructure/api-client';
|
||||
import { ApiClient, AanvraagSummaryDto } from '@shared/infrastructure/api-client';
|
||||
import {
|
||||
WerkvoorraadItem,
|
||||
WerkvoorraadStatus,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
export class WerkvoorraadAdapter {
|
||||
private client = inject(ApiClient);
|
||||
|
||||
list(): Promise<ApplicationSummaryDto[]> {
|
||||
list(): Promise<AanvraagSummaryDto[]> {
|
||||
return this.client.werkvoorraad();
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export class WerkvoorraadAdapter {
|
||||
const AANVRAAG_TYPES: readonly string[] = ['registratie', 'herregistratie', 'intake'];
|
||||
|
||||
function parseWerkvoorraadStatus(
|
||||
s: ApplicationSummaryDto['status'] | undefined,
|
||||
s: AanvraagSummaryDto['status'] | undefined,
|
||||
): Result<string, WerkvoorraadStatus> {
|
||||
if (!s || typeof s.tag !== 'string') return err('werkvoorraad: missing status');
|
||||
switch (s.tag) {
|
||||
@@ -44,7 +44,7 @@ function parseWerkvoorraadStatus(
|
||||
|
||||
export function parseWerkvoorraadItem(json: unknown): Result<string, WerkvoorraadItem> {
|
||||
if (typeof json !== 'object' || json === null) return err('werkvoorraad: not an object');
|
||||
const dto = json as ApplicationSummaryDto;
|
||||
const dto = json as AanvraagSummaryDto;
|
||||
if (typeof dto.id !== 'string') return err('werkvoorraad: missing id');
|
||||
if (typeof dto.type !== 'string' || !AANVRAAG_TYPES.includes(dto.type))
|
||||
return err(`werkvoorraad: bad type ${dto.type}`);
|
||||
|
||||
+9
-9
@@ -1,8 +1,8 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { SUBMIT_FAILED } from '@shared/application/submit';
|
||||
import { ApplicationsAdapter } from '@registratie/infrastructure/applications.adapter';
|
||||
import { ApplicationsStore } from './applications.store';
|
||||
import { AanvragenAdapter } from '@registratie/infrastructure/aanvragen.adapter';
|
||||
import { AanvragenStore } from './aanvragen.store';
|
||||
|
||||
const summary = (id: string) => ({
|
||||
id,
|
||||
@@ -13,20 +13,20 @@ const summary = (id: string) => ({
|
||||
updatedAt: '2026-07-23T10:00:00Z',
|
||||
});
|
||||
|
||||
function setup(adapter: Partial<ApplicationsAdapter>): ApplicationsStore {
|
||||
function setup(adapter: Partial<AanvragenAdapter>): AanvragenStore {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: ApplicationsAdapter, useValue: adapter }],
|
||||
providers: [{ provide: AanvragenAdapter, useValue: adapter }],
|
||||
});
|
||||
// The store's own constructor kicks off `load()` (dashboard revisit refresh) —
|
||||
// give every test a `list` so that initial call has something to resolve.
|
||||
return TestBed.inject(ApplicationsStore);
|
||||
return TestBed.inject(AanvragenStore);
|
||||
}
|
||||
|
||||
describe('ApplicationsStore', () => {
|
||||
describe('AanvragenStore', () => {
|
||||
it('loads and parses the list', async () => {
|
||||
const store = setup({ list: () => Promise.resolve([summary('a'), summary('b')]) });
|
||||
await store.load();
|
||||
const s = store.applications();
|
||||
const s = store.aanvragen();
|
||||
expect(s.tag).toBe('Success');
|
||||
expect(s.tag === 'Success' && s.value.map((a) => a.id)).toEqual(['a', 'b']);
|
||||
});
|
||||
@@ -41,7 +41,7 @@ describe('ApplicationsStore', () => {
|
||||
|
||||
await store.cancel('a');
|
||||
expect(cancel).toHaveBeenCalledWith('a');
|
||||
const s = store.applications();
|
||||
const s = store.aanvragen();
|
||||
expect(s.tag === 'Success' && s.value.map((a) => a.id)).toEqual(['b']);
|
||||
expect(store.lastError()).toBeNull();
|
||||
});
|
||||
@@ -55,7 +55,7 @@ describe('ApplicationsStore', () => {
|
||||
await store.load();
|
||||
|
||||
await store.cancel('a');
|
||||
const s = store.applications();
|
||||
const s = store.aanvragen();
|
||||
expect(s.tag === 'Success' && s.value.map((a) => a.id)).toEqual(['a']); // reappears
|
||||
expect(store.lastError()).toBe(SUBMIT_FAILED);
|
||||
});
|
||||
+6
-9
@@ -2,15 +2,12 @@ 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';
|
||||
import { AanvragenAdapter, parseAanvragen } from '@registratie/infrastructure/aanvragen.adapter';
|
||||
|
||||
type Err = Error | undefined;
|
||||
|
||||
/**
|
||||
* The dashboard's view of the user's applications (aanvragen) — the backend is the
|
||||
* The dashboard's view of the user's 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
|
||||
@@ -20,11 +17,11 @@ type Err = Error | undefined;
|
||||
* surfaces `lastError` on failure (RB-20).
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ApplicationsStore {
|
||||
private adapter = inject(ApplicationsAdapter);
|
||||
export class AanvragenStore {
|
||||
private adapter = inject(AanvragenAdapter);
|
||||
|
||||
private state = signal<RemoteData<Err, Aanvraag[]>>({ tag: 'Loading' });
|
||||
readonly applications = this.state.asReadonly();
|
||||
readonly aanvragen = 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. */
|
||||
@@ -40,7 +37,7 @@ export class ApplicationsStore {
|
||||
async load() {
|
||||
if (this.state().tag !== 'Success') this.state.set({ tag: 'Loading' });
|
||||
try {
|
||||
const parsed = parseApplications(await this.adapter.list());
|
||||
const parsed = parseAanvragen(await this.adapter.list());
|
||||
this.state.set(
|
||||
parsed.ok
|
||||
? { tag: 'Success', value: parsed.value }
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { SUBMIT_FAILED } from '@shared/application/submit';
|
||||
import { ApplicationsAdapter } from '@registratie/infrastructure/applications.adapter';
|
||||
import { AanvragenAdapter } from '@registratie/infrastructure/aanvragen.adapter';
|
||||
import { AdminCasesStore } from './admin-cases.store';
|
||||
|
||||
const summary = (id: string) => ({
|
||||
@@ -14,9 +14,9 @@ const summary = (id: string) => ({
|
||||
owner: '19012345601',
|
||||
});
|
||||
|
||||
function setup(adapter: Partial<ApplicationsAdapter>): AdminCasesStore {
|
||||
function setup(adapter: Partial<AanvragenAdapter>): AdminCasesStore {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: ApplicationsAdapter, useValue: adapter }],
|
||||
providers: [{ provide: AanvragenAdapter, useValue: adapter }],
|
||||
});
|
||||
return TestBed.inject(AdminCasesStore);
|
||||
}
|
||||
|
||||
@@ -2,16 +2,13 @@ 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';
|
||||
import { AanvragenAdapter, parseAanvragen } from '@registratie/infrastructure/aanvragen.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
|
||||
* counterpart of the user-facing `AanvragenStore`. 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
|
||||
@@ -19,7 +16,7 @@ type Err = Error | undefined;
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AdminCasesStore {
|
||||
private adapter = inject(ApplicationsAdapter);
|
||||
private adapter = inject(AanvragenAdapter);
|
||||
|
||||
private state = signal<RemoteData<Err, Aanvraag[]>>({ tag: 'Loading' });
|
||||
readonly cases = this.state.asReadonly();
|
||||
@@ -34,7 +31,7 @@ export class AdminCasesStore {
|
||||
async load() {
|
||||
if (this.state().tag !== 'Success') this.state.set({ tag: 'Loading' });
|
||||
try {
|
||||
const parsed = parseApplications(await this.adapter.listAll());
|
||||
const parsed = parseAanvragen(await this.adapter.listAll());
|
||||
this.state.set(
|
||||
parsed.ok
|
||||
? { tag: 'Success', value: parsed.value }
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, computed, inject, signal } from '@angular/core';
|
||||
import { RemoteData, fromResource, map } from '@shared/application/remote-data';
|
||||
import { Aantekening } from '../domain/registration';
|
||||
import { BigProfile } from '../domain/big-profile';
|
||||
import { HerregistratieDecisions } from '../contracts/dashboard-view.dto';
|
||||
import { HerregistratieDecisions } from '../domain/registration';
|
||||
import { BigRegisterAdapter } from '../infrastructure/big-register.adapter';
|
||||
import {
|
||||
DashboardView,
|
||||
|
||||
@@ -2,14 +2,14 @@ import { ApplicationRef, signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { ApplicationsAdapter } from '@registratie/infrastructure/applications.adapter';
|
||||
import { AanvragenAdapter } from '@registratie/infrastructure/aanvragen.adapter';
|
||||
import { createDraftSync, DraftSnapshot } from './draft-sync';
|
||||
|
||||
function setup(adapter: Partial<ApplicationsAdapter>) {
|
||||
function setup(adapter: Partial<AanvragenAdapter>) {
|
||||
const navigate = vi.fn().mockResolvedValue(true);
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ApplicationsAdapter, useValue: adapter },
|
||||
{ provide: AanvragenAdapter, useValue: adapter },
|
||||
{ provide: Router, useValue: { navigate } },
|
||||
{ provide: ActivatedRoute, useValue: { snapshot: { queryParamMap: { get: () => null } } } },
|
||||
],
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Result } from '@shared/kernel/fp';
|
||||
import { runSubmit, SUBMIT_FAILED } from '@shared/application/submit';
|
||||
import { registerPendingSave } from '@shared/application/pending-saves';
|
||||
import type {
|
||||
SubmitApplicationRequest,
|
||||
SubmitApplicationResponse,
|
||||
AanvraagIndienenRequest,
|
||||
AanvraagIndienenResponse,
|
||||
} from '@shared/infrastructure/api-client';
|
||||
import { AanvraagType } from '@registratie/domain/aanvraag';
|
||||
import { ApplicationsAdapter } from '@registratie/infrastructure/applications.adapter';
|
||||
import { AanvragenAdapter } from '@registratie/infrastructure/aanvragen.adapter';
|
||||
import { findConcept, loadConcept } from './find-concept';
|
||||
|
||||
/** What a wizard persists per step: the opaque machine snapshot + progress + docs. */
|
||||
@@ -46,7 +46,7 @@ const DEBOUNCE_MS = 600; // ponytail: fixed debounce; tune if the sync feels lag
|
||||
* Inert without a Router (stories) or when `enabled()` is false — no network, no resume.
|
||||
*/
|
||||
export function createDraftSync(deps: DraftSyncDeps) {
|
||||
const adapter = inject(ApplicationsAdapter);
|
||||
const adapter = inject(AanvragenAdapter);
|
||||
const router = inject(Router, { optional: true });
|
||||
const route = inject(ActivatedRoute, { optional: true });
|
||||
const active = () => deps.enabled() && !!router && !!route;
|
||||
@@ -190,7 +190,7 @@ export function createDraftSync(deps: DraftSyncDeps) {
|
||||
/** Submit through the aanvraag lifecycle: ensure the Concept exists, then
|
||||
`POST /applications/{id}/submit` (server sets autoApprovable + transitions).
|
||||
Folded into a Result like the old submit-* commands. */
|
||||
submit(body: SubmitApplicationRequest): Promise<Result<string, SubmitApplicationResponse>> {
|
||||
submit(body: AanvraagIndienenRequest): Promise<Result<string, AanvraagIndienenResponse>> {
|
||||
return runSubmit(async () => adapter.submit(await ensureId(), body), SUBMIT_FAILED);
|
||||
},
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ApplicationsAdapter } from '@registratie/infrastructure/applications.adapter';
|
||||
import { AanvragenAdapter } from '@registratie/infrastructure/aanvragen.adapter';
|
||||
import { findConcept, loadConcept } from './find-concept';
|
||||
|
||||
// Free functions taking the adapter as a parameter (no inject()) — a plain fake
|
||||
// object is enough, no Angular TestBed needed.
|
||||
function fakeAdapter(overrides: Partial<ApplicationsAdapter>): ApplicationsAdapter {
|
||||
return overrides as ApplicationsAdapter;
|
||||
function fakeAdapter(overrides: Partial<AanvragenAdapter>): AanvragenAdapter {
|
||||
return overrides as AanvragenAdapter;
|
||||
}
|
||||
|
||||
describe('findConcept', () => {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { AanvraagType } from '@registratie/domain/aanvraag';
|
||||
import {
|
||||
ApplicationsAdapter,
|
||||
parseApplications,
|
||||
} from '@registratie/infrastructure/applications.adapter';
|
||||
import { AanvragenAdapter, parseAanvragen } from '@registratie/infrastructure/aanvragen.adapter';
|
||||
|
||||
/**
|
||||
* Read half of the Concept lookup that `createDraftSync` (`draft-sync.ts`) needs
|
||||
@@ -14,11 +11,11 @@ import {
|
||||
|
||||
/** Find the user's existing Concept of a given type (at most one), if any. */
|
||||
export async function findConcept(
|
||||
adapter: ApplicationsAdapter,
|
||||
adapter: AanvragenAdapter,
|
||||
type: AanvraagType,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const parsed = parseApplications(await adapter.list());
|
||||
const parsed = parseAanvragen(await adapter.list());
|
||||
return parsed.ok
|
||||
? parsed.value.find((a) => a.type === type && a.status.tag === 'Concept')?.id
|
||||
: undefined;
|
||||
@@ -34,10 +31,7 @@ export async function findConcept(
|
||||
export type LoadedConcept = { tag: 'concept'; draft: unknown | null } | { tag: 'not-concept' };
|
||||
|
||||
/** Load a specific Concept by id and report whether it is still editable. */
|
||||
export async function loadConcept(
|
||||
adapter: ApplicationsAdapter,
|
||||
id: string,
|
||||
): Promise<LoadedConcept> {
|
||||
export async function loadConcept(adapter: AanvragenAdapter, id: string): Promise<LoadedConcept> {
|
||||
try {
|
||||
const dto = await adapter.detail(id);
|
||||
if (dto.status && dto.status.tag !== 'Concept') return { tag: 'not-concept' };
|
||||
|
||||
@@ -41,7 +41,7 @@ export class RegistratieLookupStore {
|
||||
const json = this.adresRes.value();
|
||||
if (json === undefined) return null;
|
||||
const parsed = parseBrpAddress(json);
|
||||
return parsed.ok && parsed.value.gevonden && parsed.value.adres ? parsed.value.adres : null;
|
||||
return parsed.ok && parsed.value.adres ? parsed.value.adres : null;
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* WIRE CONTRACT for the BRP address lookup ("BFF-lite" — one screen-shaped call).
|
||||
*
|
||||
* In production this is GENERATED from the OpenAPI/TypeSpec spec and served by our
|
||||
* own backend, which talks to the BRP behind an adapter. The frontend never sees
|
||||
* the BRP's own wire format. See docs/reference/architecture/0001-bff-lite-decision-dtos.md.
|
||||
*
|
||||
* "Geen adres bekend" is a first-class outcome (`gevonden: false`), not an error —
|
||||
* the wizard falls back to manual entry (PRD §7). Slice 1 ships only the happy
|
||||
* path (gevonden: true).
|
||||
*/
|
||||
export interface BrpAddressDto {
|
||||
gevonden: boolean;
|
||||
adres?: { straat: string; postcode: string; woonplaats: string };
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* WIRE CONTRACT for the dashboard screen — the "BFF-lite" response.
|
||||
*
|
||||
* PURE wire shapes: this file imports NOTHING (CLAUDE.md §1, ADR-0001). Enums are
|
||||
* inlined string-literal unions that describe the wire, not the domain. The
|
||||
* adapter's `parseDashboardView` validates this untrusted shape and MAPS it onto
|
||||
* the FE domain model (Registration/Person/BigProfile) — that map is the
|
||||
* decoupling seam: the wire can change without the domain following.
|
||||
*
|
||||
* In production these types are GENERATED from the OpenAPI/TypeSpec spec (one
|
||||
* source of truth for both sides), and the `decisions` block is computed BY THE
|
||||
* BACKEND — never recomputed on the client. The frontend renders decisions; it
|
||||
* does not own the rules. See docs/reference/architecture/0001-bff-lite-decision-dtos.md.
|
||||
*
|
||||
* One screen-shaped call replaces the previous three (BIG-register + BRP + …),
|
||||
* so the page always sees one consistent snapshot instead of three independently
|
||||
* loading/erroring resources.
|
||||
*/
|
||||
|
||||
/** Registration status on the wire: the discriminant tags as they arrive. */
|
||||
export type RegistrationStatusDto =
|
||||
| { tag: 'Geregistreerd'; herregistratieDatum: string } // ISO date
|
||||
| { tag: 'Geschorst'; geschorstTot: string; reden: string }
|
||||
| { tag: 'Doorgehaald'; doorgehaaldOp: string; reden: string };
|
||||
|
||||
export interface RegistrationDto {
|
||||
bigNummer: string;
|
||||
naam: string;
|
||||
beroep: string;
|
||||
registratiedatum: string; // ISO date
|
||||
geboortedatum: string;
|
||||
status: RegistrationStatusDto;
|
||||
}
|
||||
|
||||
export interface AdresDto {
|
||||
straat: string;
|
||||
postcode: string;
|
||||
woonplaats: string;
|
||||
}
|
||||
|
||||
export interface PersonDto {
|
||||
naam: string;
|
||||
geboortedatum: string; // ISO date
|
||||
adres: AdresDto;
|
||||
}
|
||||
|
||||
/** Server-computed decisions. Rendered by the FE as-is (decision DTO, ADR-0001):
|
||||
the eligibility rule lives on the backend; the optional reason lets the UI
|
||||
explain itself without knowing the rule. */
|
||||
export interface HerregistratieDecisions {
|
||||
eligibleForHerregistratie: boolean;
|
||||
herregistratieReason?: string;
|
||||
}
|
||||
|
||||
export interface DashboardViewDto {
|
||||
registration: RegistrationDto;
|
||||
person: PersonDto;
|
||||
decisions: HerregistratieDecisions;
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { submittedRow, detailRows, purposeLabel, statusLabel, TYPE_LABELS } from './aanvraag-view';
|
||||
import {
|
||||
submittedRow,
|
||||
detailRows,
|
||||
purposeLabel,
|
||||
statusLabel,
|
||||
TYPE_LABELS,
|
||||
sortForDashboard,
|
||||
concepten,
|
||||
ingediend,
|
||||
} from './aanvraag-view';
|
||||
import { Aanvraag } from './aanvraag';
|
||||
|
||||
const base: Omit<Aanvraag, 'status'> = {
|
||||
@@ -114,3 +123,40 @@ describe('detailRows', () => {
|
||||
expect(rows.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortForDashboard / concepten / ingediend', () => {
|
||||
const withStatus = (id: string, tag: Aanvraag['status']['tag']): Aanvraag => ({
|
||||
...base,
|
||||
id,
|
||||
status:
|
||||
tag === 'Concept'
|
||||
? { tag, stepIndex: 0, stepCount: 1 }
|
||||
: tag === 'Afgewezen' || tag === 'MeerInfoGevraagd'
|
||||
? { tag, referentie: 'R', reden: 'x' }
|
||||
: tag === 'InBehandeling'
|
||||
? { tag, referentie: 'R', manual: false }
|
||||
: { tag, referentie: 'R' },
|
||||
});
|
||||
|
||||
it('sorts Concept, then still-open, then resolved last', () => {
|
||||
const goedgekeurd = withStatus('1', 'Goedgekeurd');
|
||||
const concept = withStatus('2', 'Concept');
|
||||
const inBehandeling = withStatus('3', 'InBehandeling');
|
||||
const sorted = sortForDashboard([goedgekeurd, concept, inBehandeling]);
|
||||
expect(sorted.map((a) => a.id)).toEqual(['2', '3', '1']);
|
||||
});
|
||||
|
||||
it('does not mutate the input array', () => {
|
||||
const list = [withStatus('1', 'Goedgekeurd'), withStatus('2', 'Concept')];
|
||||
const copy = [...list];
|
||||
sortForDashboard(list);
|
||||
expect(list).toEqual(copy);
|
||||
});
|
||||
|
||||
it('concepten/ingediend split on the Concept tag', () => {
|
||||
const concept = withStatus('1', 'Concept');
|
||||
const ingediendItem = withStatus('2', 'Ingediend');
|
||||
expect(concepten([concept, ingediendItem])).toEqual([concept]);
|
||||
expect(ingediend([concept, ingediendItem])).toEqual([ingediendItem]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,3 +106,30 @@ export function detailRows(a: Aanvraag): { key: string; value: string }[] {
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Dashboard sort order: still-open work first (Concept, then submitted-and-pending),
|
||||
resolved aanvragen (Goedgekeurd/Afgewezen) last. Within a group, order is stable
|
||||
(the sort is by rank only). */
|
||||
const SORT_RANK: Record<AanvraagStatus['tag'], number> = {
|
||||
Concept: 0,
|
||||
Ingediend: 1,
|
||||
InBehandeling: 1,
|
||||
MeerInfoGevraagd: 1,
|
||||
Goedgekeurd: 2,
|
||||
Afgewezen: 2,
|
||||
};
|
||||
|
||||
/** The dashboard's "Mijn aanvragen" ordering: open work before resolved cases. */
|
||||
export function sortForDashboard(aanvragen: Aanvraag[]): Aanvraag[] {
|
||||
return aanvragen.slice().sort((a, b) => SORT_RANK[a.status.tag] - SORT_RANK[b.status.tag]);
|
||||
}
|
||||
|
||||
/** A Concept ("lopende aanvraag") renders as a melding above the list; a submitted
|
||||
aanvraag as a keuzelijst item — the two shapes need different HTML contexts. */
|
||||
export function concepten(aanvragen: Aanvraag[]): Aanvraag[] {
|
||||
return aanvragen.filter((a) => a.status.tag === 'Concept');
|
||||
}
|
||||
|
||||
export function ingediend(aanvragen: Aanvraag[]): Aanvraag[] {
|
||||
return aanvragen.filter((a) => a.status.tag !== 'Concept');
|
||||
}
|
||||
|
||||
@@ -33,3 +33,11 @@ export interface Aantekening {
|
||||
omschrijving: string;
|
||||
datum: string;
|
||||
}
|
||||
|
||||
/** Server-computed eligibility for herregistratie (ADR-0001 decision DTO): the FE
|
||||
renders this as-is, it never recomputes the rule. The optional reason lets the
|
||||
UI explain a "not eligible" outcome without knowing why. */
|
||||
export interface HerregistratieDecisions {
|
||||
eligibleForHerregistratie: boolean;
|
||||
herregistratieReason?: string;
|
||||
}
|
||||
|
||||
+14
-14
@@ -1,10 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
parseAanvraagStatus,
|
||||
parseApplicationSummary,
|
||||
parseApplications,
|
||||
parseApplicationDetail,
|
||||
} from './applications.adapter';
|
||||
parseAanvraagSummary,
|
||||
parseAanvragen,
|
||||
parseAanvraagDetail,
|
||||
} from './aanvragen.adapter';
|
||||
|
||||
const concept = {
|
||||
id: 'a1',
|
||||
@@ -42,29 +42,29 @@ describe('parseAanvraagStatus', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseApplicationSummary', () => {
|
||||
describe('parseAanvraagSummary', () => {
|
||||
it('maps a valid DTO to domain', () => {
|
||||
const r = parseApplicationSummary(concept);
|
||||
const r = parseAanvraagSummary(concept);
|
||||
expect(r.ok && r.value.type).toBe('registratie');
|
||||
expect(r.ok && r.value.status.tag).toBe('Concept');
|
||||
});
|
||||
|
||||
it('rejects a bad type and non-objects', () => {
|
||||
expect(parseApplicationSummary({ ...concept, type: 'onbekend' }).ok).toBe(false);
|
||||
expect(parseApplicationSummary(null).ok).toBe(false);
|
||||
expect(parseApplicationSummary({ ...concept, id: 42 }).ok).toBe(false);
|
||||
expect(parseAanvraagSummary({ ...concept, type: 'onbekend' }).ok).toBe(false);
|
||||
expect(parseAanvraagSummary(null).ok).toBe(false);
|
||||
expect(parseAanvraagSummary({ ...concept, id: 42 }).ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseApplications / parseApplicationDetail', () => {
|
||||
describe('parseAanvragen / parseAanvraagDetail', () => {
|
||||
it('parses a list and fails fast on a bad element', () => {
|
||||
expect(parseApplications([concept, concept]).ok).toBe(true);
|
||||
expect(parseApplications([concept, { ...concept, status: { tag: 'x' } }]).ok).toBe(false);
|
||||
expect(parseApplications({}).ok).toBe(false);
|
||||
expect(parseAanvragen([concept, concept]).ok).toBe(true);
|
||||
expect(parseAanvragen([concept, { ...concept, status: { tag: 'x' } }]).ok).toBe(false);
|
||||
expect(parseAanvragen({}).ok).toBe(false);
|
||||
});
|
||||
|
||||
it('carries the opaque draft through detail', () => {
|
||||
const r = parseApplicationDetail({ ...concept, draft: { beroep: 'arts' } });
|
||||
const r = parseAanvraagDetail({ ...concept, draft: { beroep: 'arts' } });
|
||||
expect(r.ok && (r.value.draft as { beroep: string }).beroep).toBe('arts');
|
||||
});
|
||||
});
|
||||
+24
-24
@@ -3,11 +3,11 @@ import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import {
|
||||
ApiClient,
|
||||
AanvraagStatusDto,
|
||||
ApplicationSummaryDto,
|
||||
ApplicationDetailDto,
|
||||
AanvraagSummaryDto,
|
||||
AanvraagDetailDto,
|
||||
DraftSyncRequest,
|
||||
SubmitApplicationRequest,
|
||||
SubmitApplicationResponse,
|
||||
AanvraagIndienenRequest,
|
||||
AanvraagIndienenResponse,
|
||||
} from '@shared/infrastructure/api-client';
|
||||
import {
|
||||
Aanvraag,
|
||||
@@ -19,21 +19,21 @@ import {
|
||||
/**
|
||||
* Infrastructure adapter for the backend-owned Aanvraag aggregate — the only place
|
||||
* its HTTP lives (ADR-0001 anti-corruption boundary). The list is a resource; the
|
||||
* mutations (create/sync/cancel/submit) are thin commands the ApplicationsStore
|
||||
* mutations (create/sync/cancel/submit) are thin commands the AanvragenStore
|
||||
* orchestrates optimistically. The untrusted response is validated + mapped to
|
||||
* domain by the hand-written parse* boundary below.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ApplicationsAdapter {
|
||||
export class AanvragenAdapter {
|
||||
private client = inject(ApiClient);
|
||||
|
||||
/** The dashboard's application list (raw DTOs; the store parses at the boundary). */
|
||||
list(): Promise<ApplicationSummaryDto[]> {
|
||||
return this.client.applicationsAll();
|
||||
/** The dashboard's aanvraag list (raw DTOs; the store parses at the boundary). */
|
||||
list(): Promise<AanvraagSummaryDto[]> {
|
||||
return this.client.aanvragenAll();
|
||||
}
|
||||
|
||||
/** Admin: every case across all owners (WP-36; `cases:manage`). Parsed at the boundary. */
|
||||
listAll(): Promise<ApplicationSummaryDto[]> {
|
||||
listAll(): Promise<AanvraagSummaryDto[]> {
|
||||
return this.client.casesAll();
|
||||
}
|
||||
|
||||
@@ -42,26 +42,26 @@ export class ApplicationsAdapter {
|
||||
return this.client.cases(id);
|
||||
}
|
||||
|
||||
detail(id: string): Promise<ApplicationDetailDto> {
|
||||
return this.client.applicationsGET(id);
|
||||
detail(id: string): Promise<AanvraagDetailDto> {
|
||||
return this.client.aanvragenGET(id);
|
||||
}
|
||||
|
||||
/** Create a Concept for a wizard type; resolves to the new aanvraag id. */
|
||||
create(type: AanvraagType): Promise<string> {
|
||||
return this.client.applicationsPOST({ type }).then((d) => d.id ?? '');
|
||||
return this.client.aanvragenPOST({ type }).then((d) => d.id ?? '');
|
||||
}
|
||||
|
||||
/** Draft sync per step (idempotent). Keep it debounced at the call site — it is chatty. */
|
||||
syncDraft(id: string, body: DraftSyncRequest): Promise<void> {
|
||||
return this.client.applicationsPUT(id, body);
|
||||
return this.client.aanvragenPUT(id, body);
|
||||
}
|
||||
|
||||
/** Cancel a Concept (cascades to its unlinked documents server-side). */
|
||||
cancel(id: string): Promise<void> {
|
||||
return this.client.applicationsDELETE(id);
|
||||
return this.client.aanvragenDELETE(id);
|
||||
}
|
||||
|
||||
submit(id: string, body: SubmitApplicationRequest): Promise<SubmitApplicationResponse> {
|
||||
submit(id: string, body: AanvraagIndienenRequest): Promise<AanvraagIndienenResponse> {
|
||||
return this.client.submit(id, body);
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export function parseAanvraagStatus(
|
||||
}
|
||||
}
|
||||
|
||||
function parseCommon(dto: ApplicationSummaryDto): Result<string, Aanvraag> {
|
||||
function parseCommon(dto: AanvraagSummaryDto): Result<string, Aanvraag> {
|
||||
if (typeof dto.id !== 'string') return err('aanvraag: missing id');
|
||||
if (typeof dto.type !== 'string' || !AANVRAAG_TYPES.includes(dto.type))
|
||||
return err(`aanvraag: bad type ${dto.type}`);
|
||||
@@ -121,25 +121,25 @@ function parseCommon(dto: ApplicationSummaryDto): Result<string, Aanvraag> {
|
||||
});
|
||||
}
|
||||
|
||||
export function parseApplicationSummary(json: unknown): Result<string, Aanvraag> {
|
||||
export function parseAanvraagSummary(json: unknown): Result<string, Aanvraag> {
|
||||
if (typeof json !== 'object' || json === null) return err('aanvraag: not an object');
|
||||
return parseCommon(json as ApplicationSummaryDto);
|
||||
return parseCommon(json as AanvraagSummaryDto);
|
||||
}
|
||||
|
||||
export function parseApplications(json: unknown): Result<string, Aanvraag[]> {
|
||||
export function parseAanvragen(json: unknown): Result<string, Aanvraag[]> {
|
||||
if (!Array.isArray(json)) return err('aanvragen: not an array');
|
||||
const out: Aanvraag[] = [];
|
||||
for (const item of json) {
|
||||
const parsed = parseApplicationSummary(item);
|
||||
const parsed = parseAanvraagSummary(item);
|
||||
if (!parsed.ok) return parsed;
|
||||
out.push(parsed.value);
|
||||
}
|
||||
return ok(out);
|
||||
}
|
||||
|
||||
export function parseApplicationDetail(json: unknown): Result<string, AanvraagDetail> {
|
||||
export function parseAanvraagDetail(json: unknown): Result<string, AanvraagDetail> {
|
||||
if (typeof json !== 'object' || json === null) return err('aanvraag: not an object');
|
||||
const base = parseCommon(json as ApplicationDetailDto);
|
||||
const base = parseCommon(json as AanvraagDetailDto);
|
||||
if (!base.ok) return base;
|
||||
return ok({ ...base.value, draft: (json as ApplicationDetailDto).draft ?? null });
|
||||
return ok({ ...base.value, draft: (json as AanvraagDetailDto).draft ?? null });
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Injectable, inject, resource } from '@angular/core';
|
||||
import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import { BrpAddressDto } from '@registratie/contracts/brp-address.dto';
|
||||
import { BrpAddressDto } from '@shared/infrastructure/api-client';
|
||||
import { ApiClient } from '@shared/infrastructure/api-client';
|
||||
|
||||
/** BRP address lookup, narrowed from the generated (all-optional) `BrpAddressDto`
|
||||
to what `gevonden` actually guarantees. */
|
||||
export interface BrpAddress {
|
||||
gevonden: boolean;
|
||||
adres?: { straat: string; postcode: string; woonplaats: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Infrastructure adapter for the BRP address lookup, reached only through our own
|
||||
* ("BFF-lite") endpoint — the anti-corruption boundary. Data comes from the .NET
|
||||
@@ -21,20 +28,22 @@ export class BrpAdapter {
|
||||
/** Trust-boundary parse: validate the untrusted response shape. "Geen adres" is a
|
||||
valid outcome (gevonden: false), not a malformed response. ponytail: hand-written;
|
||||
reach for a schema lib once the contract count grows. */
|
||||
export function parseBrpAddress(json: unknown): Result<string, BrpAddressDto> {
|
||||
export function parseBrpAddress(json: unknown): Result<string, BrpAddress> {
|
||||
if (typeof json !== 'object' || json === null) return err('brp-address: not an object');
|
||||
const dto = json as Partial<BrpAddressDto>;
|
||||
if (typeof dto.gevonden !== 'boolean') return err('brp-address: missing/invalid gevonden');
|
||||
if (dto.gevonden) {
|
||||
const a = dto.adres;
|
||||
if (
|
||||
!a ||
|
||||
typeof a.straat !== 'string' ||
|
||||
typeof a.postcode !== 'string' ||
|
||||
typeof a.woonplaats !== 'string'
|
||||
) {
|
||||
return err('brp-address: missing/invalid adres');
|
||||
}
|
||||
if (!dto.gevonden) return ok({ gevonden: false });
|
||||
const a = dto.adres;
|
||||
if (
|
||||
!a ||
|
||||
typeof a.straat !== 'string' ||
|
||||
typeof a.postcode !== 'string' ||
|
||||
typeof a.woonplaats !== 'string'
|
||||
) {
|
||||
return err('brp-address: missing/invalid adres');
|
||||
}
|
||||
return ok({ gevonden: dto.gevonden, adres: dto.adres });
|
||||
return ok({
|
||||
gevonden: true,
|
||||
adres: { straat: a.straat, postcode: a.postcode, woonplaats: a.woonplaats },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,4 +35,43 @@ describe('parseDashboardView (trust boundary)', () => {
|
||||
parseDashboardView({ ...valid, decisions: { eligibleForHerregistratie: 'yes' } }).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a status whose tag is present but its required fields are missing', () => {
|
||||
// The generated RegistrationStatusDto flattens the union — every field is
|
||||
// optional, so a wire bug (a Geregistreerd row with no herregistratieDatum)
|
||||
// must be caught here, not by the compiler.
|
||||
expect(
|
||||
parseDashboardView({
|
||||
...valid,
|
||||
registration: { ...valid.registration, status: { tag: 'Geregistreerd' } },
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
expect(
|
||||
parseDashboardView({
|
||||
...valid,
|
||||
registration: {
|
||||
...valid.registration,
|
||||
status: { tag: 'Geschorst', geschorstTot: '2027-01-01' }, // missing reden
|
||||
},
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an unknown status tag', () => {
|
||||
expect(
|
||||
parseDashboardView({
|
||||
...valid,
|
||||
registration: { ...valid.registration, status: { tag: 'Ingetrokken' } },
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a person with an incomplete adres', () => {
|
||||
expect(
|
||||
parseDashboardView({
|
||||
...valid,
|
||||
person: { ...valid.person, adres: { straat: 'X 1' } },
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import { Injectable, inject, resource } from '@angular/core';
|
||||
import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import {
|
||||
ApiClient,
|
||||
DashboardViewDto,
|
||||
RegistrationDto,
|
||||
RegistrationStatusDto,
|
||||
PersonDto,
|
||||
} from '@shared/infrastructure/api-client';
|
||||
import {
|
||||
Registration,
|
||||
RegistrationStatus,
|
||||
HerregistratieDecisions,
|
||||
} from '@registratie/contracts/dashboard-view.dto';
|
||||
import { Registration } from '@registratie/domain/registration';
|
||||
} from '@registratie/domain/registration';
|
||||
import { Person } from '@registratie/domain/person';
|
||||
import { BigProfile } from '@registratie/domain/big-profile';
|
||||
import { ApiClient } from '@shared/infrastructure/api-client';
|
||||
|
||||
/**
|
||||
* The parsed, frontend-side view: the wire DTO mapped onto our own domain model.
|
||||
* Lives HERE, not in contracts/, because it references domain types — contracts
|
||||
* stays import-free. This split is the decoupling seam (CLAUDE.md §1, ADR-0001).
|
||||
* Lives HERE, not in a `contracts/*.dto.ts` file, because it references domain
|
||||
* types — that split is the decoupling seam (CLAUDE.md §1, ADR-0001).
|
||||
*/
|
||||
export interface DashboardView {
|
||||
profile: BigProfile;
|
||||
@@ -31,17 +37,80 @@ export class DashboardViewAdapter {
|
||||
|
||||
// The value is still untrusted JSON — parseDashboardView validates it at the
|
||||
// boundary and maps DTO → domain before the app uses it.
|
||||
//
|
||||
// SEAM (G5): retry-with-backoff for non-mutating reads wraps the loader here —
|
||||
// e.g. `loader: () => withBackoff(() => this.client.dashboardView())` — since the
|
||||
// adapter is the single place HTTP lives. Reads are safe to retry; MUTATING calls
|
||||
// (the submit-* commands) must NEVER auto-retry — and don't. Manual retry
|
||||
// (resource.reload via <app-async>) covers the UX today, so backoff stays unbuilt.
|
||||
dashboardViewResource() {
|
||||
return resource({ loader: () => this.client.dashboardView() });
|
||||
}
|
||||
}
|
||||
|
||||
/** Trust-boundary parse of the status union — the generated `RegistrationStatusDto`
|
||||
flattens all three variants into one object with every field optional (NSwag
|
||||
can't express a discriminated union), so the tag drives which fields must
|
||||
actually be present. Mirrors `parseAanvraagStatus` in `aanvragen.adapter.ts`. */
|
||||
export function parseRegistrationStatus(
|
||||
s: RegistrationStatusDto | undefined,
|
||||
): Result<string, RegistrationStatus> {
|
||||
if (!s || typeof s.tag !== 'string') return err('registration: missing status');
|
||||
switch (s.tag) {
|
||||
case 'Geregistreerd':
|
||||
if (typeof s.herregistratieDatum !== 'string')
|
||||
return err('registration: bad Geregistreerd status');
|
||||
return ok({ tag: 'Geregistreerd', herregistratieDatum: s.herregistratieDatum });
|
||||
case 'Geschorst':
|
||||
if (typeof s.geschorstTot !== 'string' || typeof s.reden !== 'string')
|
||||
return err('registration: bad Geschorst status');
|
||||
return ok({ tag: 'Geschorst', geschorstTot: s.geschorstTot, reden: s.reden });
|
||||
case 'Doorgehaald':
|
||||
if (typeof s.doorgehaaldOp !== 'string' || typeof s.reden !== 'string')
|
||||
return err('registration: bad Doorgehaald status');
|
||||
return ok({ tag: 'Doorgehaald', doorgehaaldOp: s.doorgehaaldOp, reden: s.reden });
|
||||
default:
|
||||
return err(`registration: unknown status tag ${s.tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseRegistration(dto: RegistrationDto | undefined): Result<string, Registration> {
|
||||
if (
|
||||
!dto ||
|
||||
typeof dto.bigNummer !== 'string' ||
|
||||
typeof dto.naam !== 'string' ||
|
||||
typeof dto.beroep !== 'string' ||
|
||||
typeof dto.registratiedatum !== 'string' ||
|
||||
typeof dto.geboortedatum !== 'string'
|
||||
) {
|
||||
return err('dashboard-view: missing/invalid registration');
|
||||
}
|
||||
const status = parseRegistrationStatus(dto.status);
|
||||
if (!status.ok) return status;
|
||||
return ok({
|
||||
bigNummer: dto.bigNummer,
|
||||
naam: dto.naam,
|
||||
beroep: dto.beroep,
|
||||
registratiedatum: dto.registratiedatum,
|
||||
geboortedatum: dto.geboortedatum,
|
||||
status: status.value,
|
||||
});
|
||||
}
|
||||
|
||||
function parsePerson(dto: PersonDto | undefined): Result<string, Person> {
|
||||
const a = dto?.adres;
|
||||
if (
|
||||
!dto ||
|
||||
typeof dto.naam !== 'string' ||
|
||||
typeof dto.geboortedatum !== 'string' ||
|
||||
!a ||
|
||||
typeof a.straat !== 'string' ||
|
||||
typeof a.postcode !== 'string' ||
|
||||
typeof a.woonplaats !== 'string'
|
||||
) {
|
||||
return err('dashboard-view: missing/invalid person');
|
||||
}
|
||||
return ok({
|
||||
naam: dto.naam,
|
||||
geboortedatum: dto.geboortedatum,
|
||||
adres: { straat: a.straat, postcode: a.postcode, woonplaats: a.woonplaats },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trust-boundary parse: validate the untrusted response shape and map the DTO
|
||||
* onto our own domain model. Hand-written on purpose — no Zod for a single
|
||||
@@ -51,43 +120,17 @@ export function parseDashboardView(json: unknown): Result<string, DashboardView>
|
||||
if (typeof json !== 'object' || json === null) return err('dashboard-view: not an object');
|
||||
const dto = json as Partial<DashboardViewDto>;
|
||||
|
||||
const reg = dto.registration;
|
||||
if (
|
||||
!reg ||
|
||||
typeof reg.bigNummer !== 'string' ||
|
||||
!reg.status ||
|
||||
typeof reg.status.tag !== 'string'
|
||||
) {
|
||||
return err('dashboard-view: missing/invalid registration');
|
||||
}
|
||||
const person = dto.person;
|
||||
if (!person || !person.adres || typeof person.adres.postcode !== 'string') {
|
||||
return err('dashboard-view: missing/invalid person');
|
||||
}
|
||||
const registration = parseRegistration(dto.registration);
|
||||
if (!registration.ok) return registration;
|
||||
const person = parsePerson(dto.person);
|
||||
if (!person.ok) return person;
|
||||
const d = dto.decisions;
|
||||
if (!d || typeof d.eligibleForHerregistratie !== 'boolean') {
|
||||
return err('dashboard-view: missing/invalid decisions');
|
||||
}
|
||||
|
||||
// Map wire → domain. The shapes are identical today, so this reads as an
|
||||
// identity copy — but the TYPES differ (wire DTO vs domain), so the moment the
|
||||
// wire diverges the compiler forces a real mapping here. That's the seam.
|
||||
const registration: Registration = {
|
||||
bigNummer: reg.bigNummer,
|
||||
naam: reg.naam,
|
||||
beroep: reg.beroep,
|
||||
registratiedatum: reg.registratiedatum,
|
||||
geboortedatum: reg.geboortedatum,
|
||||
status: reg.status,
|
||||
};
|
||||
const persoon: Person = {
|
||||
naam: person.naam,
|
||||
geboortedatum: person.geboortedatum,
|
||||
adres: person.adres,
|
||||
};
|
||||
|
||||
return ok({
|
||||
profile: { registration, person: persoon },
|
||||
profile: { registration: registration.value, person: person.value },
|
||||
decisions: {
|
||||
eligibleForHerregistratie: d.eligibleForHerregistratie,
|
||||
herregistratieReason: d.herregistratieReason,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
@@ -6,7 +7,7 @@ import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { ApplicationsStore } from '@registratie/application/applications.store';
|
||||
import { AanvragenStore } from '@registratie/application/aanvragen.store';
|
||||
import { Aanvraag } from '@registratie/domain/aanvraag';
|
||||
import { detailRows } from '@registratie/domain/aanvraag-view';
|
||||
|
||||
@@ -29,9 +30,9 @@ import { detailRows } from '@registratie/domain/aanvraag-view';
|
||||
heading="Aanvraag"
|
||||
backLink="/dashboard"
|
||||
>
|
||||
<app-async [data]="store.applications()">
|
||||
<app-async [data]="store.aanvragen()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (applications(); as list) {
|
||||
@if (aanvragen(); as list) {
|
||||
@let a = find(list);
|
||||
@if (a) {
|
||||
<app-data-block
|
||||
@@ -60,15 +61,13 @@ import { detailRows } from '@registratie/domain/aanvraag-view';
|
||||
`,
|
||||
})
|
||||
export class AanvraagDetailPage {
|
||||
protected store = inject(ApplicationsStore);
|
||||
protected store = inject(AanvragenStore);
|
||||
private id = inject(ActivatedRoute).snapshot.paramMap.get('id') ?? '';
|
||||
|
||||
protected find = (list: Aanvraag[]): Aanvraag | undefined => list.find((a) => a.id === this.id);
|
||||
protected rows = detailRows;
|
||||
|
||||
/** See DashboardPage's `profile` for why this narrows via a computed instead of `let-`. */
|
||||
protected readonly applications = computed(() => {
|
||||
const rd = this.store.applications();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
/** `successOf`: `<ng-template>` can't inherit a generic from a sibling host input,
|
||||
so the Success value is unwrapped here instead of through `let-`. */
|
||||
protected readonly aanvragen = computed(() => successOf(this.store.aanvragen()));
|
||||
}
|
||||
|
||||
@@ -1,47 +1,25 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Component } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { TaskListComponent } from '@shared/ui/task-list/task-list.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { FeatureFlagStore } from '@shared/application/feature-flags.store';
|
||||
import { FLAG_INSCHRIJVING_OPEN } from '@shared/domain/feature-flag';
|
||||
import { ADMIN_LINKS } from '../../shell/nav.config';
|
||||
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
|
||||
import { RegistrationTableComponent } from '@registratie/ui/registration-table/registration-table.component';
|
||||
import { AanvraagBlockComponent } from '@registratie/ui/aanvraag-block/aanvraag-block.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { ApplicationsStore } from '@registratie/application/applications.store';
|
||||
import { Registration } from '@registratie/domain/registration';
|
||||
import { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';
|
||||
import { submittedRow } from '@registratie/domain/aanvraag-view';
|
||||
import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
import { MijnAanvragenSection } from './dashboard/mijn-aanvragen.section';
|
||||
import { WatMoetIkRegelenSection } from './dashboard/wat-moet-ik-regelen.section';
|
||||
import { MijnRegistratieSection } from './dashboard/mijn-registratie.section';
|
||||
import { SpecialismenSection } from './dashboard/specialismen.section';
|
||||
import { WatWiltUDoenSection } from './dashboard/wat-wilt-u-doen.section';
|
||||
import { BeheerLinksSection } from './dashboard/beheer-links.section';
|
||||
|
||||
/** Page:"Mijn overzicht" — the portal home, following the NL Design System
|
||||
"Mijn omgeving" pattern (side nav +"Wat moet ik regelen" +"Mijn zaken"). */
|
||||
/** Page: "Mijn overzicht" — the portal home, following the NL Design System "Mijn
|
||||
omgeving" pattern. Composition only: each section below answers its own data
|
||||
question (own store, own async state) — see `ui/dashboard/*.section.ts`. */
|
||||
@Component({
|
||||
selector: 'app-dashboard-page',
|
||||
imports: [
|
||||
PageShellComponent,
|
||||
HeadingComponent,
|
||||
AlertComponent,
|
||||
SkeletonComponent,
|
||||
DataRowComponent,
|
||||
DataBlockComponent,
|
||||
TaskListComponent,
|
||||
ApplicationListComponent,
|
||||
ApplicationLinkComponent,
|
||||
...ASYNC,
|
||||
RegistrationSummaryComponent,
|
||||
RegistrationTableComponent,
|
||||
AanvraagBlockComponent,
|
||||
MijnAanvragenSection,
|
||||
WatMoetIkRegelenSection,
|
||||
MijnRegistratieSection,
|
||||
SpecialismenSection,
|
||||
WatWiltUDoenSection,
|
||||
BeheerLinksSection,
|
||||
],
|
||||
template: `
|
||||
<app-page-shell
|
||||
@@ -51,290 +29,14 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
intro="Welkom in uw persoonlijke omgeving van het BIG-register. Hier ziet u uw registratie en regelt u uw zaken."
|
||||
>
|
||||
<div class="app-stack">
|
||||
@if (cancelError(); as err) {
|
||||
<app-alert type="error">{{ err }}</app-alert>
|
||||
}
|
||||
@if (aanvragen().length) {
|
||||
<section>
|
||||
@for (a of concepten(); track a.id) {
|
||||
<app-aanvraag-block
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[aanvraag]="a"
|
||||
(resume)="resume(a)"
|
||||
(cancel)="cancelAanvraag(a)"
|
||||
/>
|
||||
}
|
||||
@if (ingediend().length) {
|
||||
<app-heading [level]="2" class="app-section" i18n="@@dashboard.mijnAanvragen"
|
||||
>Mijn aanvragen</app-heading
|
||||
>
|
||||
<app-application-list>
|
||||
@for (a of ingediend(); track a.id) {
|
||||
@let row = submittedRow(a);
|
||||
<li
|
||||
app-application-link
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[heading]="row.heading"
|
||||
[subtitle]="row.subtitle"
|
||||
[status]="row.status"
|
||||
[to]="'/aanvraag/' + a.id"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (store.pendingHerregistratie()) {
|
||||
<app-alert type="info" i18n="@@dashboard.pendingHerregistratie"
|
||||
>Uw herregistratie-aanvraag is in behandeling.</app-alert
|
||||
>
|
||||
}
|
||||
|
||||
<app-async [data]="store.profile()" (retryClicked)="store.reloadProfile()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (profile(); as p) {
|
||||
@let tasks = tasksFor(p.registration);
|
||||
|
||||
<section>
|
||||
@if (tasks.length) {
|
||||
<app-task-list
|
||||
class="app-section"
|
||||
i18n-listHeading="@@dashboard.watMoetIkRegelen"
|
||||
listHeading="Wat moet ik regelen"
|
||||
[tasks]="tasks"
|
||||
/>
|
||||
} @else {
|
||||
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen"
|
||||
>Wat moet ik regelen</app-heading
|
||||
>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">
|
||||
U heeft op dit moment niets openstaan.
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie"
|
||||
>Mijn registratie</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-registration-summary [reg]="p.registration" />
|
||||
</div>
|
||||
<app-data-block
|
||||
class="app-section"
|
||||
i18n-heading="@@dashboard.persoonsgegevens"
|
||||
heading="Persoonsgegevens (BRP)"
|
||||
>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.straat"
|
||||
key="Straat"
|
||||
[value]="p.person.adres.straat"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.postcode"
|
||||
key="Postcode"
|
||||
[value]="p.person.adres.postcode"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.woonplaats"
|
||||
key="Woonplaats"
|
||||
[value]="p.person.adres.woonplaats"
|
||||
></div>
|
||||
</app-data-block>
|
||||
</section>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="6" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.specialismen"
|
||||
>Specialismen en aantekeningen</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-async [data]="store.aantekeningen()" (retryClicked)="store.reloadAantekeningen()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (aantekeningen(); as r) {
|
||||
<app-registration-table [rows]="r" />
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="3" />
|
||||
</ng-template>
|
||||
<ng-template appAsyncEmpty>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.geenSpecialismen">
|
||||
U heeft nog geen specialismen of aantekeningen.
|
||||
</p>
|
||||
</ng-template>
|
||||
</app-async>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.watWiltUDoen">Wat wilt u doen?</app-heading>
|
||||
<app-application-list class="app-section">
|
||||
@for (a of acties(); track a.to) {
|
||||
<li
|
||||
app-application-link
|
||||
[heading]="a.titel"
|
||||
[subtitle]="a.tekst"
|
||||
[cta]="a.actie"
|
||||
[to]="a.to"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
</section>
|
||||
|
||||
@if (adminLinks().length) {
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.beheer">Beheer</app-heading>
|
||||
<app-application-list class="app-section">
|
||||
@for (link of adminLinks(); track link.to) {
|
||||
<li
|
||||
app-application-link
|
||||
[heading]="link.label"
|
||||
[subtitle]="link.description"
|
||||
[to]="link.to"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
</section>
|
||||
}
|
||||
<app-mijn-aanvragen-section />
|
||||
<app-wat-moet-ik-regelen-section />
|
||||
<app-mijn-registratie-section />
|
||||
<app-specialismen-section />
|
||||
<app-wat-wilt-u-doen-section />
|
||||
<app-beheer-links-section />
|
||||
</div>
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
export class DashboardPage {
|
||||
protected store = inject(BigProfileStore);
|
||||
private apps = inject(ApplicationsStore);
|
||||
private access = inject(AccessStore);
|
||||
private flags = inject(FeatureFlagStore);
|
||||
private router = inject(Router);
|
||||
|
||||
/** Admin pages the current principal may reach — capability-gated (never role-derived),
|
||||
the same source + filter the site header uses. Empty for a non-admin → section hidden. */
|
||||
protected adminLinks = computed(() => ADMIN_LINKS.filter((l) => this.access.can(l.cap)));
|
||||
|
||||
/** Pure view mapping for a submitted aanvraag → CIBG aanvragen-row fields. */
|
||||
protected submittedRow = submittedRow;
|
||||
|
||||
constructor() {
|
||||
// Re-fetch on each visit so server-computed auto-approval transitions show up
|
||||
// (Concept → In behandeling → Goedgekeurd after the processing window).
|
||||
this.apps.reload();
|
||||
}
|
||||
|
||||
/** The user's applications, sorted Concept → In behandeling → resolved. Empty →
|
||||
the"Mijn aanvragen" section is hidden (see template). */
|
||||
protected aanvragen = computed<Aanvraag[]>(() => {
|
||||
const rd = this.apps.applications();
|
||||
if (rd.tag !== 'Success') return [];
|
||||
const order: Record<Aanvraag['status']['tag'], number> = {
|
||||
Concept: 0,
|
||||
Ingediend: 1,
|
||||
InBehandeling: 1,
|
||||
MeerInfoGevraagd: 1,
|
||||
Goedgekeurd: 2,
|
||||
Afgewezen: 2,
|
||||
};
|
||||
return rd.value.slice().sort((a, b) => order[a.status.tag] - order[b.status.tag]);
|
||||
});
|
||||
/** A Concept ("lopende aanvraag") renders as a melding above the list; the rest
|
||||
as keuzelijst items — the two shapes need different HTML contexts. */
|
||||
protected concepten = computed(() => this.aanvragen().filter((a) => a.status.tag === 'Concept'));
|
||||
protected ingediend = computed(() => this.aanvragen().filter((a) => a.status.tag !== 'Concept'));
|
||||
|
||||
private readonly resumeRoutes: Record<AanvraagType, string> = {
|
||||
registratie: '/registreren',
|
||||
herregistratie: '/herregistratie',
|
||||
intake: '/intake',
|
||||
};
|
||||
protected resume(a: Aanvraag) {
|
||||
void this.router.navigate([this.resumeRoutes[a.type]], { queryParams: { aanvraag: a.id } });
|
||||
}
|
||||
protected cancelAanvraag(a: Aanvraag) {
|
||||
void this.apps.cancel(a.id);
|
||||
}
|
||||
/** RB-20: the message from a failed cancel, rendered above the list. */
|
||||
protected cancelError = computed(() => this.apps.lastError());
|
||||
|
||||
/** Server-computed eligibility (rendered, not recomputed). */
|
||||
private readonly eligible = computed(() => {
|
||||
const d = this.store.decisions();
|
||||
return d.tag === 'Success' && d.value.eligibleForHerregistratie;
|
||||
});
|
||||
|
||||
protected tasksFor(reg: Registration) {
|
||||
return tasksFromProfile(reg, this.eligible());
|
||||
}
|
||||
|
||||
/** Typed narrowing for the `<app-async>` loaded slot — `<ng-template>`'s own
|
||||
context can't inherit a generic from a sibling host input (Angular only infers
|
||||
a structural directive's type parameter from an input on that same node), so
|
||||
the Success value is unwrapped here instead of through `let-`. */
|
||||
protected readonly profile = computed(() => {
|
||||
const rd = this.store.profile();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
protected readonly aantekeningen = computed(() => {
|
||||
const rd = this.store.aantekeningen();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
|
||||
/** Primary transactional actions, as an "aanvragen" list (see CIBG's
|
||||
componenten/aanvragen). The core portal sections live in the header nav now;
|
||||
the teaching pages (concepts/brief) are only reachable from here. */
|
||||
private readonly allActies = [
|
||||
{
|
||||
to: '/registreren',
|
||||
titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`,
|
||||
tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`,
|
||||
actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving`,
|
||||
},
|
||||
{
|
||||
to: '/herregistratie',
|
||||
titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`,
|
||||
tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`,
|
||||
actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan`,
|
||||
},
|
||||
{
|
||||
to: '/intake',
|
||||
titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`,
|
||||
tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`,
|
||||
actie: $localize`:@@dashboard.actie.intake.actie:Start intake`,
|
||||
},
|
||||
{
|
||||
to: '/registratie',
|
||||
titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`,
|
||||
tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`,
|
||||
actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens`,
|
||||
},
|
||||
{
|
||||
to: '/concepts',
|
||||
titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`,
|
||||
tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`,
|
||||
actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen`,
|
||||
},
|
||||
{
|
||||
to: '/brief',
|
||||
titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`,
|
||||
tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`,
|
||||
actie: $localize`:@@dashboard.actie.brief.actie:Start brief`,
|
||||
},
|
||||
];
|
||||
|
||||
/** Hide the "Inschrijven" action when self-service registration is flagged off (WP-47). */
|
||||
protected readonly acties = computed(() =>
|
||||
this.allActies.filter(
|
||||
(a) => a.to !== '/registreren' || this.flags.enabled(FLAG_INSCHRIJVING_OPEN),
|
||||
),
|
||||
);
|
||||
}
|
||||
export class DashboardPage {}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { ADMIN_LINKS } from '../../../shell/nav.config';
|
||||
|
||||
/** Section: "Beheer" — the admin pages the current principal may reach, capability-
|
||||
gated (never role-derived), the same source + filter the site header uses.
|
||||
Empty for a non-admin → the section renders nothing (see the page). */
|
||||
@Component({
|
||||
selector: 'app-beheer-links-section',
|
||||
imports: [HeadingComponent, ApplicationListComponent, ApplicationLinkComponent],
|
||||
template: `
|
||||
@if (adminLinks().length) {
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.beheer">Beheer</app-heading>
|
||||
<app-application-list class="app-section">
|
||||
@for (link of adminLinks(); track link.to) {
|
||||
<li
|
||||
app-application-link
|
||||
[heading]="link.label"
|
||||
[subtitle]="link.description"
|
||||
[to]="link.to"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
</section>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class BeheerLinksSection {
|
||||
private access = inject(AccessStore);
|
||||
protected adminLinks = computed(() => ADMIN_LINKS.filter((l) => this.access.can(l.cap)));
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { applicationConfig } from '@storybook/angular';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { MijnAanvragenSection } from './mijn-aanvragen.section';
|
||||
import { AanvragenStore } from '@registratie/application/aanvragen.store';
|
||||
import { Aanvraag } from '@registratie/domain/aanvraag';
|
||||
import { RemoteData } from '@shared/application/remote-data';
|
||||
import { loading, success, failure } from '@shared/testing/remote-data';
|
||||
|
||||
const base = {
|
||||
id: 'a1',
|
||||
type: 'herregistratie',
|
||||
documentIds: [],
|
||||
createdAt: '2026-06-28T10:00:00Z',
|
||||
updatedAt: '2026-06-28T10:05:00Z',
|
||||
submittedAt: '2026-06-28T10:05:00Z',
|
||||
} satisfies Omit<Aanvraag, 'status'>;
|
||||
|
||||
const concept: Aanvraag = { ...base, status: { tag: 'Concept', stepIndex: 1, stepCount: 3 } };
|
||||
const ingediend: Aanvraag = {
|
||||
...base,
|
||||
id: 'a2',
|
||||
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false },
|
||||
};
|
||||
|
||||
/** Minimal store stand-in — only the members the section's template reads. */
|
||||
function storeStub(aanvragen: RemoteData<Error | undefined, Aanvraag[]>, lastError = '') {
|
||||
return {
|
||||
aanvragen: () => aanvragen,
|
||||
reload: () => {},
|
||||
cancel: async () => {},
|
||||
lastError: () => lastError,
|
||||
};
|
||||
}
|
||||
|
||||
const meta: Meta<MijnAanvragenSection> = {
|
||||
title: 'Domein/Registratie/Dashboard/Mijn Aanvragen',
|
||||
component: MijnAanvragenSection,
|
||||
decorators: [applicationConfig({ providers: [provideRouter([])] })],
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<MijnAanvragenSection>;
|
||||
|
||||
export const Loading: Story = {
|
||||
decorators: [
|
||||
applicationConfig({ providers: [{ provide: AanvragenStore, useValue: storeStub(loading()) }] }),
|
||||
],
|
||||
};
|
||||
export const WithConceptAndSubmitted: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: AanvragenStore, useValue: storeStub(success([concept, ingediend])) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Empty: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: AanvragenStore, useValue: storeStub(success([])) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const CancelFailed: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [
|
||||
{
|
||||
provide: AanvragenStore,
|
||||
useValue: storeStub(
|
||||
success([concept]),
|
||||
$localize`:@@dashboard.cancel.failed:Verwijderen is niet gelukt.`,
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Failed: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: AanvragenStore, useValue: storeStub(failure(new Error('offline'))) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AanvragenStore } from '@registratie/application/aanvragen.store';
|
||||
import { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';
|
||||
import {
|
||||
submittedRow,
|
||||
sortForDashboard,
|
||||
concepten,
|
||||
ingediend,
|
||||
} from '@registratie/domain/aanvraag-view';
|
||||
import { AanvraagBlockComponent } from '@registratie/ui/aanvraag-block/aanvraag-block.component';
|
||||
|
||||
/** Section: "Mijn aanvragen" — the user's own aanvragen (concepten as resumable
|
||||
meldingen, submitted ones as keuzelijst rows), owning its own fetch, sort and
|
||||
the resume/cancel actions. Empty → renders nothing, same as the aanvraag-block
|
||||
convention (see AanvraagBlockComponent). */
|
||||
@Component({
|
||||
selector: 'app-mijn-aanvragen-section',
|
||||
imports: [
|
||||
AlertComponent,
|
||||
SkeletonComponent,
|
||||
HeadingComponent,
|
||||
ApplicationListComponent,
|
||||
ApplicationLinkComponent,
|
||||
AanvraagBlockComponent,
|
||||
...ASYNC,
|
||||
],
|
||||
template: `
|
||||
@if (cancelError(); as err) {
|
||||
<app-alert type="error">{{ err }}</app-alert>
|
||||
}
|
||||
<app-async [data]="store.aanvragen()" (retryClicked)="store.reload()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (aanvragen().length) {
|
||||
<section>
|
||||
@for (a of concepten_(); track a.id) {
|
||||
<app-aanvraag-block
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[aanvraag]="a"
|
||||
(resume)="resume(a)"
|
||||
(cancel)="cancel(a)"
|
||||
/>
|
||||
}
|
||||
@if (ingediend_().length) {
|
||||
<app-heading [level]="2" class="app-section" i18n="@@dashboard.mijnAanvragen"
|
||||
>Mijn aanvragen</app-heading
|
||||
>
|
||||
<app-application-list>
|
||||
@for (a of ingediend_(); track a.id) {
|
||||
@let row = submittedRow(a);
|
||||
<li
|
||||
app-application-link
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[heading]="row.heading"
|
||||
[subtitle]="row.subtitle"
|
||||
[status]="row.status"
|
||||
[to]="'/aanvraag/' + a.id"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="2" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
`,
|
||||
})
|
||||
export class MijnAanvragenSection {
|
||||
protected store = inject(AanvragenStore);
|
||||
private router = inject(Router);
|
||||
|
||||
constructor() {
|
||||
// Re-fetch on each visit so server-computed auto-approval transitions show up
|
||||
// (Concept → In behandeling → Goedgekeurd after the processing window).
|
||||
this.store.reload();
|
||||
}
|
||||
|
||||
protected submittedRow = submittedRow;
|
||||
|
||||
protected aanvragen = computed<Aanvraag[]>(() => {
|
||||
const rd = this.store.aanvragen();
|
||||
return rd.tag === 'Success' ? sortForDashboard(rd.value) : [];
|
||||
});
|
||||
protected concepten_ = computed(() => concepten(this.aanvragen()));
|
||||
protected ingediend_ = computed(() => ingediend(this.aanvragen()));
|
||||
|
||||
private readonly resumeRoutes: Record<AanvraagType, string> = {
|
||||
registratie: '/registreren',
|
||||
herregistratie: '/herregistratie',
|
||||
intake: '/intake',
|
||||
};
|
||||
protected resume(a: Aanvraag) {
|
||||
void this.router.navigate([this.resumeRoutes[a.type]], { queryParams: { aanvraag: a.id } });
|
||||
}
|
||||
protected cancel(a: Aanvraag) {
|
||||
void this.store.cancel(a.id);
|
||||
}
|
||||
|
||||
/** The message from a failed cancel, rendered above the list. */
|
||||
protected cancelError = computed(() => this.store.lastError());
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { applicationConfig } from '@storybook/angular';
|
||||
import { MijnRegistratieSection } from './mijn-registratie.section';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { BigProfile } from '@registratie/domain/big-profile';
|
||||
import { RemoteData } from '@shared/application/remote-data';
|
||||
import { loading, success, failure } from '@shared/testing/remote-data';
|
||||
|
||||
const profile: BigProfile = {
|
||||
registration: {
|
||||
bigNummer: '19012345601',
|
||||
naam: 'Dr. A. (Anna) de Vries',
|
||||
beroep: 'Arts',
|
||||
registratiedatum: '2012-09-01',
|
||||
geboortedatum: '1985-03-14',
|
||||
status: { tag: 'Geregistreerd', herregistratieDatum: '2027-09-01' },
|
||||
},
|
||||
person: {
|
||||
naam: 'Dr. A. (Anna) de Vries',
|
||||
geboortedatum: '1985-03-14',
|
||||
adres: { straat: 'Rijksweg 1', postcode: '2514 EA', woonplaats: 'Den Haag' },
|
||||
},
|
||||
};
|
||||
|
||||
/** Minimal store stand-in — only the members the section's template reads. */
|
||||
function storeStub(profileRd: RemoteData<Error | undefined, BigProfile>) {
|
||||
return { profile: () => profileRd, reloadProfile: () => {} };
|
||||
}
|
||||
|
||||
const meta: Meta<MijnRegistratieSection> = {
|
||||
title: 'Domein/Registratie/Dashboard/Mijn Registratie',
|
||||
component: MijnRegistratieSection,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<MijnRegistratieSection>;
|
||||
|
||||
export const Loading: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(loading()) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Loaded: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(success(profile)) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Failed: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(failure(new Error('offline'))) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
|
||||
|
||||
/** Section: "Mijn registratie" — the BIG-register summary plus the BRP
|
||||
persoonsgegevens, both from the one screen-shaped dashboard call
|
||||
(BigProfileStore). */
|
||||
@Component({
|
||||
selector: 'app-mijn-registratie-section',
|
||||
imports: [
|
||||
HeadingComponent,
|
||||
SkeletonComponent,
|
||||
DataBlockComponent,
|
||||
DataRowComponent,
|
||||
RegistrationSummaryComponent,
|
||||
...ASYNC,
|
||||
],
|
||||
template: `
|
||||
<app-async [data]="store.profile()" (retryClicked)="store.reloadProfile()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (profile(); as p) {
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie"
|
||||
>Mijn registratie</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-registration-summary [reg]="p.registration" />
|
||||
</div>
|
||||
<app-data-block
|
||||
class="app-section"
|
||||
i18n-heading="@@dashboard.persoonsgegevens"
|
||||
heading="Persoonsgegevens (BRP)"
|
||||
>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.straat"
|
||||
key="Straat"
|
||||
[value]="p.person.adres.straat"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.postcode"
|
||||
key="Postcode"
|
||||
[value]="p.person.adres.postcode"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.woonplaats"
|
||||
key="Woonplaats"
|
||||
[value]="p.person.adres.woonplaats"
|
||||
></div>
|
||||
</app-data-block>
|
||||
</section>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="4" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
`,
|
||||
})
|
||||
export class MijnRegistratieSection {
|
||||
protected store = inject(BigProfileStore);
|
||||
protected profile = () => successOf(this.store.profile());
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { applicationConfig } from '@storybook/angular';
|
||||
import { SpecialismenSection } from './specialismen.section';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { Aantekening } from '@registratie/domain/registration';
|
||||
import { RemoteData } from '@shared/application/remote-data';
|
||||
import { loading, success, empty, failure } from '@shared/testing/remote-data';
|
||||
|
||||
const rows: Aantekening[] = [
|
||||
{ type: 'Specialisme', omschrijving: 'Huisartsgeneeskunde', datum: '2016-04-12' },
|
||||
{ type: 'Aantekening', omschrijving: 'Erkend opleider huisartsgeneeskunde', datum: '2019-01-08' },
|
||||
];
|
||||
|
||||
/** Minimal store stand-in — only the members the section's template reads. */
|
||||
function storeStub(aantekeningen: RemoteData<Error | undefined, Aantekening[]>) {
|
||||
return { aantekeningen: () => aantekeningen, reloadAantekeningen: () => {} };
|
||||
}
|
||||
|
||||
const meta: Meta<SpecialismenSection> = {
|
||||
title: 'Domein/Registratie/Dashboard/Specialismen',
|
||||
component: SpecialismenSection,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<SpecialismenSection>;
|
||||
|
||||
export const Loading: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(loading()) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Loaded: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(success(rows)) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Empty: Story = {
|
||||
decorators: [
|
||||
applicationConfig({ providers: [{ provide: BigProfileStore, useValue: storeStub(empty()) }] }),
|
||||
],
|
||||
};
|
||||
export const Failed: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(failure(new Error('offline'))) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { RegistrationTableComponent } from '@registratie/ui/registration-table/registration-table.component';
|
||||
|
||||
/** Section: "Specialismen en aantekeningen" — a separate resource from the rest of
|
||||
the dashboard (own load/empty/error state), because it is genuinely a second
|
||||
endpoint (`big-register.adapter.ts`), not part of the BFF-lite view call. */
|
||||
@Component({
|
||||
selector: 'app-specialismen-section',
|
||||
imports: [HeadingComponent, SkeletonComponent, RegistrationTableComponent, ...ASYNC],
|
||||
template: `
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.specialismen"
|
||||
>Specialismen en aantekeningen</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-async [data]="store.aantekeningen()" (retryClicked)="store.reloadAantekeningen()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (aantekeningen(); as r) {
|
||||
<app-registration-table [rows]="r" />
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="3" />
|
||||
</ng-template>
|
||||
<ng-template appAsyncEmpty>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.geenSpecialismen">
|
||||
U heeft nog geen specialismen of aantekeningen.
|
||||
</p>
|
||||
</ng-template>
|
||||
</app-async>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class SpecialismenSection {
|
||||
protected store = inject(BigProfileStore);
|
||||
protected aantekeningen = () => successOf(this.store.aantekeningen());
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { TaskListComponent } from '@shared/ui/task-list/task-list.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
|
||||
/** Section: "Wat moet ik regelen" — the open tasks derived from the registration
|
||||
+ the server-computed herregistratie eligibility (rendered, never recomputed;
|
||||
ADR-0001). Empty task list → a plain "niets openstaan" message, not an empty
|
||||
async state (the registration itself did load). */
|
||||
@Component({
|
||||
selector: 'app-wat-moet-ik-regelen-section',
|
||||
imports: [HeadingComponent, AlertComponent, SkeletonComponent, TaskListComponent, ...ASYNC],
|
||||
template: `
|
||||
@if (store.pendingHerregistratie()) {
|
||||
<app-alert type="info" i18n="@@dashboard.pendingHerregistratie"
|
||||
>Uw herregistratie-aanvraag is in behandeling.</app-alert
|
||||
>
|
||||
}
|
||||
<app-async [data]="store.profile()" (retryClicked)="store.reloadProfile()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (tasks(); as t) {
|
||||
<section>
|
||||
@if (t.length) {
|
||||
<app-task-list
|
||||
i18n-listHeading="@@dashboard.watMoetIkRegelen"
|
||||
listHeading="Wat moet ik regelen"
|
||||
[tasks]="t"
|
||||
/>
|
||||
} @else {
|
||||
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen"
|
||||
>Wat moet ik regelen</app-heading
|
||||
>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">
|
||||
U heeft op dit moment niets openstaan.
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="2" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
`,
|
||||
})
|
||||
export class WatMoetIkRegelenSection {
|
||||
protected store = inject(BigProfileStore);
|
||||
|
||||
private eligible = computed(() => {
|
||||
const d = successOf(this.store.decisions());
|
||||
return d?.eligibleForHerregistratie ?? false;
|
||||
});
|
||||
|
||||
protected tasks = computed(() => {
|
||||
const p = successOf(this.store.profile());
|
||||
return p ? tasksFromProfile(p.registration, this.eligible()) : undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { FeatureFlagStore } from '@shared/application/feature-flags.store';
|
||||
import { FLAG_INSCHRIJVING_OPEN } from '@shared/domain/feature-flag';
|
||||
|
||||
/** Section: "Wat wilt u doen?" — the portal's primary transactional actions (see
|
||||
CIBG's componenten/aanvragen). The core pages live in the header nav now; the
|
||||
teaching pages (concepts/brief) are only reachable from here. */
|
||||
@Component({
|
||||
selector: 'app-wat-wilt-u-doen-section',
|
||||
imports: [HeadingComponent, ApplicationListComponent, ApplicationLinkComponent],
|
||||
template: `
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.watWiltUDoen">Wat wilt u doen?</app-heading>
|
||||
<app-application-list class="app-section">
|
||||
@for (a of acties(); track a.to) {
|
||||
<li
|
||||
app-application-link
|
||||
[heading]="a.titel"
|
||||
[subtitle]="a.tekst"
|
||||
[cta]="a.actie"
|
||||
[to]="a.to"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class WatWiltUDoenSection {
|
||||
private flags = inject(FeatureFlagStore);
|
||||
|
||||
private readonly allActies = [
|
||||
{
|
||||
to: '/registreren',
|
||||
titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`,
|
||||
tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`,
|
||||
actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving`,
|
||||
},
|
||||
{
|
||||
to: '/herregistratie',
|
||||
titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`,
|
||||
tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`,
|
||||
actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan`,
|
||||
},
|
||||
{
|
||||
to: '/intake',
|
||||
titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`,
|
||||
tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`,
|
||||
actie: $localize`:@@dashboard.actie.intake.actie:Start intake`,
|
||||
},
|
||||
{
|
||||
to: '/registratie',
|
||||
titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`,
|
||||
tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`,
|
||||
actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens`,
|
||||
},
|
||||
{
|
||||
to: '/concepts',
|
||||
titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`,
|
||||
tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`,
|
||||
actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen`,
|
||||
},
|
||||
{
|
||||
to: '/brief',
|
||||
titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`,
|
||||
tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`,
|
||||
actie: $localize`:@@dashboard.actie.brief.actie:Start brief`,
|
||||
},
|
||||
];
|
||||
|
||||
/** Hide "Inschrijven" when self-service registration is flagged off. */
|
||||
protected readonly acties = computed(() =>
|
||||
this.allActies.filter(
|
||||
(a) => a.to !== '/registreren' || this.flags.enabled(FLAG_INSCHRIJVING_OPEN),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
@@ -41,9 +42,7 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
export class RegistrationDetailPage {
|
||||
protected store = inject(BigProfileStore);
|
||||
|
||||
/** See DashboardPage's `profile` for why this narrows via a computed instead of `let-`. */
|
||||
protected readonly profile = computed(() => {
|
||||
const rd = this.store.profile();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
/** `successOf`: `<ng-template>` can't inherit a generic from a sibling host input,
|
||||
so the Success value is unwrapped here instead of through `let-`. */
|
||||
protected readonly profile = computed(() => successOf(this.store.profile()));
|
||||
}
|
||||
|
||||
@@ -99,19 +99,19 @@ public sealed record AanvraagStatusDto(
|
||||
bool? Manual = null,
|
||||
string? Reden = null);
|
||||
|
||||
public sealed record ApplicationSummaryDto(
|
||||
public sealed record AanvraagSummaryDto(
|
||||
string Id, string Type, AanvraagStatusDto Status,
|
||||
IReadOnlyList<string> DocumentIds,
|
||||
string CreatedAt, string UpdatedAt, string? SubmittedAt,
|
||||
string? Owner = null); // populated for the admin cross-owner list (WP-36); the user's own list ignores it
|
||||
|
||||
public sealed record ApplicationDetailDto(
|
||||
public sealed record AanvraagDetailDto(
|
||||
string Id, string Type, AanvraagStatusDto Status,
|
||||
System.Text.Json.JsonElement? Draft,
|
||||
IReadOnlyList<string> DocumentIds,
|
||||
string CreatedAt, string UpdatedAt, string? SubmittedAt);
|
||||
|
||||
public sealed record CreateApplicationRequest(string Type);
|
||||
public sealed record CreateAanvraagRequest(string Type);
|
||||
|
||||
public sealed record DraftSyncRequest(
|
||||
System.Text.Json.JsonElement Draft, int StepIndex, int StepCount,
|
||||
@@ -120,12 +120,12 @@ public sealed record DraftSyncRequest(
|
||||
// Submit carries only the fields the server re-validates per wizard type.
|
||||
// AanvullendeScholing/ScholingPunten (WP-69) — intake-typed aanvragen only (gated by
|
||||
// IntakePolicy.RejectIncompleteScholing's caller), null for the others.
|
||||
public sealed record SubmitApplicationRequest(
|
||||
public sealed record AanvraagIndienenRequest(
|
||||
string? DiplomaHerkomst = null, int? Uren = null,
|
||||
IReadOnlyList<DocumentRefDto>? Documents = null,
|
||||
bool? AanvullendeScholing = null, int? ScholingPunten = null);
|
||||
|
||||
public sealed record SubmitApplicationResponse(string Referentie, AanvraagStatusDto Status);
|
||||
public sealed record AanvraagIndienenResponse(string Referentie, AanvraagStatusDto Status);
|
||||
|
||||
// --- Beoordeling (WP-65): the behandelportal's case-detail screen. ---
|
||||
|
||||
@@ -137,7 +137,7 @@ public sealed record BeoordelingDocumentDto(string DocumentId, string CategoryId
|
||||
public sealed record BeoordelingDecisionsDto(bool CanBesluiten);
|
||||
|
||||
public sealed record BeoordelingViewDto(
|
||||
ApplicationSummaryDto Aanvraag,
|
||||
AanvraagSummaryDto Aanvraag,
|
||||
IReadOnlyList<BeoordelingDocumentDto> Documenten,
|
||||
BeoordelingDecisionsDto Decisions);
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public static class Mappers
|
||||
/// reads it past that point; see <c>AanvraagMapper.ApplyTo</c>'s Submitted branch).</summary>
|
||||
private static JsonElement? DraftOf(Aanvraag a) => a is Aanvraag.Concept c ? c.Draft : null;
|
||||
|
||||
public static ApplicationSummaryDto ToSummaryDto(this Aanvraag a, DateTimeOffset now) => new(
|
||||
public static AanvraagSummaryDto ToSummaryDto(this Aanvraag a, DateTimeOffset now) => new(
|
||||
a.Id, a.Type, a.ToStatusDto(now), a.DocumentIds,
|
||||
a.CreatedAt.ToString("o"), a.UpdatedAt.ToString("o"), SubmittedAtOf(a));
|
||||
|
||||
@@ -74,10 +74,10 @@ public static class Mappers
|
||||
/// someone who is not the subject (`/admin/cases`, `/werkvoorraad`), so it goes out masked
|
||||
/// (RB-03/BIO-003). Masking here rather than at each endpoint means a third cross-owner
|
||||
/// list cannot be added that forgets to.
|
||||
public static ApplicationSummaryDto ToAdminSummaryDto(this Aanvraag a, DateTimeOffset now) =>
|
||||
public static AanvraagSummaryDto ToAdminSummaryDto(this Aanvraag a, DateTimeOffset now) =>
|
||||
a.ToSummaryDto(now) with { Owner = Pii.MaskTail(a.Owner, 3) };
|
||||
|
||||
public static ApplicationDetailDto ToDetailDto(this Aanvraag a, DateTimeOffset now) => new(
|
||||
public static AanvraagDetailDto ToDetailDto(this Aanvraag a, DateTimeOffset now) => new(
|
||||
a.Id, a.Type, a.ToStatusDto(now), DraftOf(a), a.DocumentIds,
|
||||
a.CreatedAt.ToString("o"), a.UpdatedAt.ToString("o"), SubmittedAtOf(a));
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public static class DocumentStore
|
||||
/// SeedData.Registration.BigNummer ("19012345601", 11 digits — the seeded doctor's BIG-nummer,
|
||||
/// a different Dutch identifier scheme). Previously this constant reused that BigNummer value
|
||||
/// as a stand-in BSN, which is invalid Dutch-BSN shape: harmless against the local store, but
|
||||
/// a real OpenZaak instance rejects it outright — GET /api/v1/applications 500s (`inpBsn` query
|
||||
/// a real OpenZaak instance rejects it outright — GET /api/v1/aanvragen 500s (`inpBsn` query
|
||||
/// filter validation) and every submit's rol-creation POST fails (`inpBsn` max_length) once
|
||||
/// Zgw:Enabled=true. Not "111222333" or "999888777" — both already mean a different fixture
|
||||
/// identity (the OpenZaak-harness/unit-test caller, and ApplicationTests' "other citizen").
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace BigRegister.Api.Data;
|
||||
/// <summary>
|
||||
/// The cases (zaken) READ seam (WP-49). A "zaak" in ZGW terms is an <see cref="Aanvraag"/>
|
||||
/// here; this interface is the one injection point that lets a real ZGW backend (OpenZaak)
|
||||
/// replace the local SQLite store <em>behind the same <see cref="ApplicationSummaryDto"/>
|
||||
/// replace the local SQLite store <em>behind the same <see cref="AanvraagSummaryDto"/>
|
||||
/// contract</em> — so the frontend never changes (BFF-lite anti-corruption, ADR-0001).
|
||||
///
|
||||
/// Default binding is <see cref="LocalZaakSource"/> (offline). Setting <c>Zgw:Enabled=true</c>
|
||||
@@ -20,7 +20,7 @@ public interface IZaakSource
|
||||
{
|
||||
/// <summary>Every case across every owner, newest-first (the admin cross-owner list,
|
||||
/// WP-36) — cases:manage only, deliberately NOT citizen-scoped.</summary>
|
||||
IReadOnlyList<ApplicationSummaryDto> ListCases(DateTimeOffset now);
|
||||
IReadOnlyList<AanvraagSummaryDto> ListCases(DateTimeOffset now);
|
||||
|
||||
/// <summary>
|
||||
/// Only <paramref name="caller"/>'s own cases (WP-53) — the citizen-scoped counterpart of
|
||||
@@ -29,7 +29,7 @@ public interface IZaakSource
|
||||
/// <c>rol__betrokkeneIdentificatie__natuurlijkPersoon__inpBsn</c> query filter so a citizen
|
||||
/// can never see another citizen's zaken.
|
||||
/// </summary>
|
||||
IReadOnlyList<ApplicationSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now);
|
||||
IReadOnlyList<AanvraagSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now);
|
||||
|
||||
/// <summary>
|
||||
/// Register a just-submitted <paramref name="aanvraag"/> as a zaak (WP-50). The aanvraag is
|
||||
|
||||
@@ -12,12 +12,12 @@ namespace BigRegister.Api.Data;
|
||||
/// </summary>
|
||||
public sealed class LocalZaakSource : IZaakSource
|
||||
{
|
||||
public IReadOnlyList<ApplicationSummaryDto> ListCases(DateTimeOffset now) =>
|
||||
public IReadOnlyList<AanvraagSummaryDto> ListCases(DateTimeOffset now) =>
|
||||
ApplicationStore.ListAll().Select(a => a.ToAdminSummaryDto(now)).ToList();
|
||||
|
||||
/// <summary>Citizen-scoped (WP-53) — exactly what <c>GET /applications</c> used to compute
|
||||
/// <summary>Citizen-scoped (WP-53) — exactly what <c>GET /aanvragen</c> used to compute
|
||||
/// inline before it was routed through this seam.</summary>
|
||||
public IReadOnlyList<ApplicationSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
||||
public IReadOnlyList<AanvraagSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
||||
ApplicationStore.List(caller.Bsn)
|
||||
.OrderByDescending(a => a.UpdatedAt)
|
||||
.Select(a => a.ToSummaryDto(now)).ToList();
|
||||
|
||||
@@ -9,7 +9,7 @@ public sealed record FeatureFlagDef(string Key, string Description, bool Default
|
||||
public static class FeatureFlags
|
||||
{
|
||||
/// Whether self-service registration (inschrijving) is open. When off, the FE hides the
|
||||
/// "Inschrijven" action and POST /applications for a `registratie` is refused (server-enforced).
|
||||
/// "Inschrijven" action and POST /aanvragen for a `registratie` is refused (server-enforced).
|
||||
public const string InschrijvingOpen = "inschrijving-open";
|
||||
|
||||
public static readonly IReadOnlyList<FeatureFlagDef> Catalog = new[]
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace BigRegister.Domain.Intake;
|
||||
/// (<c>GET /intake/policy</c>) and applies it for instant UX feedback
|
||||
/// (<c>intake.machine.ts</c>'s <c>lageUren</c>); <see cref="RejectIncompleteScholing"/> is the
|
||||
/// backend re-validating it as the authority on submit (WP-69) —
|
||||
/// <c>POST /applications/{id}/submit</c> (intake-typed aanvragen only) calls it before
|
||||
/// <c>POST /aanvragen/{id}/submit</c> (intake-typed aanvragen only) calls it before
|
||||
/// writing anything, and a violation 400s (<c>ProblemDetails</c>), never silently accepts
|
||||
/// an incomplete answer.
|
||||
/// </summary>
|
||||
|
||||
@@ -342,19 +342,19 @@ api.MapDelete("/admin/uploads/{documentId}", (string documentId, HttpContext ctx
|
||||
// ApplicationStore directly — under Zgw:Enabled=true a citizen's own dashboard list comes from
|
||||
// OpenZaak (BSN-filtered) too, closing the last "reads a static store directly" gap
|
||||
// openzaak-integration.md's ACL caveat used to flag for this endpoint.
|
||||
api.MapGet("/applications", (HttpContext ctx, IZaakSource zaken) =>
|
||||
api.MapGet("/aanvragen", (HttpContext ctx, IZaakSource zaken) =>
|
||||
zaken.ListMyCases(ctx.Zorgverlener(), DateTimeOffset.UtcNow));
|
||||
|
||||
api.MapGet("/applications/{id}", (string id, HttpContext ctx) =>
|
||||
api.MapGet("/aanvragen/{id}", (string id, HttpContext ctx) =>
|
||||
ApplicationStore.Get(id, ctx.Zorgverlener().Bsn) is { } a
|
||||
? Results.Ok(a.ToDetailDto(DateTimeOffset.UtcNow))
|
||||
: Results.NotFound())
|
||||
.Produces<ApplicationDetailDto>()
|
||||
.Produces<AanvraagDetailDto>()
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// --- writes ---
|
||||
|
||||
api.MapPost("/applications", (CreateApplicationRequest req, HttpContext ctx) =>
|
||||
api.MapPost("/aanvragen", (CreateAanvraagRequest req, HttpContext ctx) =>
|
||||
{
|
||||
// Feature flag (WP-47): self-service registration can be closed by an admin.
|
||||
if (req.Type == "registratie" && !FeatureFlagStore.IsEnabled(FeatureFlags.InschrijvingOpen))
|
||||
@@ -364,13 +364,13 @@ api.MapPost("/applications", (CreateApplicationRequest req, HttpContext ctx) =>
|
||||
return Results.Problem(
|
||||
detail: "U hebt al een concept van dit type. Rond dat eerst af of verwijder het.",
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
return Results.Created($"/api/v1/applications/{a.Id}", a.ToDetailDto(DateTimeOffset.UtcNow));
|
||||
return Results.Created($"/api/v1/aanvragen/{a.Id}", a.ToDetailDto(DateTimeOffset.UtcNow));
|
||||
})
|
||||
.Produces<ApplicationDetailDto>(StatusCodes.Status201Created)
|
||||
.Produces<AanvraagDetailDto>(StatusCodes.Status201Created)
|
||||
.ProducesProblem(StatusCodes.Status409Conflict);
|
||||
|
||||
// Draft sync per step — idempotent; keep it debounced on the client (it is chatty).
|
||||
api.MapPut("/applications/{id}", (string id, DraftSyncRequest req, HttpContext ctx) =>
|
||||
api.MapPut("/aanvragen/{id}", (string id, DraftSyncRequest req, HttpContext ctx) =>
|
||||
{
|
||||
var owner = ctx.Zorgverlener().Bsn;
|
||||
// A citizen may only reference their own uploads in a draft — reject before the sync
|
||||
@@ -388,7 +388,7 @@ api.MapPut("/applications/{id}", (string id, DraftSyncRequest req, HttpContext c
|
||||
|
||||
// Cancel a Concept (cascades to its unlinked documents). Submitted aanvragen cannot
|
||||
// be withdrawn (out of scope — no "intrekken").
|
||||
api.MapDelete("/applications/{id}", (string id, HttpContext ctx) =>
|
||||
api.MapDelete("/aanvragen/{id}", (string id, HttpContext ctx) =>
|
||||
{
|
||||
var a = ApplicationStore.Get(id, ctx.Zorgverlener().Bsn);
|
||||
if (a is null) return Results.NotFound();
|
||||
@@ -403,7 +403,7 @@ api.MapDelete("/applications/{id}", (string id, HttpContext ctx) =>
|
||||
|
||||
// Submit runs the server-owned rules, sets autoApprovable, and transitions the
|
||||
// aanvraag. handmatig no longer 422s (ADR-0002): it becomes a manual (pending) case.
|
||||
api.MapPost("/applications/{id}/submit", (string id, SubmitApplicationRequest req, HttpContext ctx, IZaakSource zaken, IDocumentSource documents) =>
|
||||
api.MapPost("/aanvragen/{id}/submit", (string id, AanvraagIndienenRequest req, HttpContext ctx, IZaakSource zaken, IDocumentSource documents) =>
|
||||
{
|
||||
var existing = ApplicationStore.Get(id, ctx.Zorgverlener().Bsn);
|
||||
if (existing is null) return Results.NotFound();
|
||||
@@ -480,9 +480,9 @@ api.MapPost("/applications/{id}/submit", (string id, SubmitApplicationRequest re
|
||||
}
|
||||
}
|
||||
|
||||
return Results.Ok(new SubmitApplicationResponse(referentie, status));
|
||||
return Results.Ok(new AanvraagIndienenResponse(referentie, status));
|
||||
})
|
||||
.Produces<SubmitApplicationResponse>()
|
||||
.Produces<AanvraagIndienenResponse>()
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest)
|
||||
.ProducesProblem(StatusCodes.Status409Conflict)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
@@ -494,7 +494,7 @@ api.MapPost("/applications/{id}/submit", (string id, SubmitApplicationRequest re
|
||||
api.MapGet("/admin/cases", (HttpContext ctx, IZaakSource zaken) => CasesAdmin(ctx, () =>
|
||||
Results.Ok(zaken.ListCases(DateTimeOffset.UtcNow))))
|
||||
.Gate("CasesAdmin")
|
||||
.Produces<List<ApplicationSummaryDto>>()
|
||||
.Produces<List<AanvraagSummaryDto>>()
|
||||
.ProducesProblem(StatusCodes.Status403Forbidden);
|
||||
|
||||
// Queryable authz/PII-reveal audit trail (WP-41) — data-minimised, no PII. Admin-gated
|
||||
@@ -510,7 +510,7 @@ api.MapGet("/admin/audit", (HttpContext ctx) => CasesAdmin(ctx, () =>
|
||||
// --- writes ---
|
||||
|
||||
// Admin delete removes ANY case (any owner, submitted or not) — unlike the user-facing
|
||||
// DELETE /applications/{id}. A missing id is a 404.
|
||||
// DELETE /aanvragen/{id}. A missing id is a 404.
|
||||
api.MapDelete("/admin/cases/{id}", (string id, HttpContext ctx) => CasesAdmin(ctx, () =>
|
||||
{
|
||||
if (!ApplicationStore.DeleteAny(id)) return Results.NotFound();
|
||||
@@ -531,14 +531,14 @@ api.MapGet("/werkvoorraad", (HttpContext ctx, IZaakSource zaken) => Beoordelen(c
|
||||
.Where(c => c.Status.Tag is "Ingediend" or "InBehandeling")
|
||||
.ToList())))
|
||||
.Gate("Beoordelen")
|
||||
.Produces<List<ApplicationSummaryDto>>()
|
||||
.Produces<List<AanvraagSummaryDto>>()
|
||||
.ProducesProblem(StatusCodes.Status403Forbidden);
|
||||
|
||||
// --- Beoordeling (WP-65): one aanvraag's case-treatment detail — read side only (recording
|
||||
// a decision is WP-65's second half). Reads through IZaakSource.ListCases (no new seam method:
|
||||
// adding one now would force an OpenZaak get-by-id + mapper, which is WP-66's surface) — O(n)
|
||||
// over a POC-sized table. A Concept isn't a case a behandelaar can treat yet, so it 404s here
|
||||
// same as an unknown id (only /applications/{id}, citizen-scoped, shows a Concept).
|
||||
// same as an unknown id (only /aanvragen/{id}, citizen-scoped, shows a Concept).
|
||||
api.MapGet("/beoordeling/{id}", (string id, HttpContext ctx, IZaakSource zaken) =>
|
||||
Beoordelen(ctx, $"aanvraag/{id}", () =>
|
||||
{
|
||||
|
||||
@@ -18,9 +18,9 @@ public sealed record ZgwPage<T>(
|
||||
/// The <see cref="IZaakSource"/> backed by a real OpenZaak / ZGW Zaken API (WP-49 read, WP-50
|
||||
/// write). Reads zaken (following pagination), maps each zaak's zaaktype URL back to the
|
||||
/// internal aanvraag-type key via <c>Zgw:ZaaktypeUrls</c> (a local lookup — NOT OpenZaak's
|
||||
/// human zaaktype label, which isn't a value <see cref="ApplicationSummaryDto.Type"/>'s
|
||||
/// human zaaktype label, which isn't a value <see cref="AanvraagSummaryDto.Type"/>'s
|
||||
/// contract accepts; see <see cref="AanvraagTypeFor"/>), and maps into
|
||||
/// <see cref="ApplicationSummaryDto"/> via <see cref="ZgwZaakMapper"/>. Creates a zaak +
|
||||
/// <see cref="AanvraagSummaryDto"/> via <see cref="ZgwZaakMapper"/>. Creates a zaak +
|
||||
/// status + rol for a just-submitted aanvraag. Selected only when <c>Zgw:Enabled=true</c>;
|
||||
/// the default stays <see cref="LocalZaakSource"/>.
|
||||
///
|
||||
@@ -36,16 +36,16 @@ public sealed class OpenZaakZaakSource(HttpClient http, ZgwTokenProvider tokens,
|
||||
// sync /admin/cases endpoint, and ASP.NET Core has no sync-context to deadlock on. Make the
|
||||
// whole cases read path async (endpoint + CasesAdmin + interface) if OpenZaak becomes the
|
||||
// default and this blocking call shows up under load.
|
||||
public IReadOnlyList<ApplicationSummaryDto> ListCases(DateTimeOffset now) =>
|
||||
public IReadOnlyList<AanvraagSummaryDto> ListCases(DateTimeOffset now) =>
|
||||
ListCasesAsync(bsn: null, caller: null).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>WP-53: same read, filtered to one citizen's own zaken via ZGW's rol filter param
|
||||
/// (see <see cref="ListCasesAsync"/>) — and minted with that citizen's identity, not the
|
||||
/// system-level one <see cref="ListCases"/> uses.</summary>
|
||||
public IReadOnlyList<ApplicationSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
||||
public IReadOnlyList<AanvraagSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
||||
ListCasesAsync(caller.Bsn, caller).GetAwaiter().GetResult();
|
||||
|
||||
private async Task<IReadOnlyList<ApplicationSummaryDto>> ListCasesAsync(string? bsn, CallerIdentity? caller)
|
||||
private async Task<IReadOnlyList<AanvraagSummaryDto>> ListCasesAsync(string? bsn, CallerIdentity? caller)
|
||||
{
|
||||
var url = $"{options.ZrcBaseUrl}/zaken";
|
||||
if (bsn is not null)
|
||||
@@ -55,7 +55,7 @@ public sealed class OpenZaakZaakSource(HttpClient http, ZgwTokenProvider tokens,
|
||||
}
|
||||
|
||||
/// <summary>Real, live-repro'd bug (behandelportal's werkvoorraad always failed to parse):
|
||||
/// <c>ApplicationSummaryDto.Type</c>'s contract is the internal aanvraag-type key (e.g.
|
||||
/// <c>AanvraagSummaryDto.Type</c>'s contract is the internal aanvraag-type key (e.g.
|
||||
/// "herregistratie" — what <see cref="LocalZaakSource"/>/<c>Mappers.ToSummaryDto</c> send,
|
||||
/// and what the FE's <c>AANVRAAG_TYPES</c> trust boundary accepts), NOT OpenZaak's human
|
||||
/// zaaktype label ("Herregistratie arts") this used to resolve via an extra Catalogi round
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace BigRegister.Api.Zgw;
|
||||
|
||||
/// <summary>
|
||||
/// The subset of a ZGW Zaak (Zaken API / ZRC) the read slice needs. The full resource has
|
||||
/// dozens of fields; we bind only what maps to <see cref="ApplicationSummaryDto"/>. Note the
|
||||
/// dozens of fields; we bind only what maps to <see cref="AanvraagSummaryDto"/>. Note the
|
||||
/// two ZGW traits that force an anti-corruption layer: <see cref="Url"/> is the resource's
|
||||
/// identity (not a bare id), and <see cref="Zaaktype"/> is a URL <em>into another service</em>
|
||||
/// (Catalogi/ZTC) that must be resolved to a human label.
|
||||
@@ -20,7 +20,7 @@ public sealed record ZgwZaak(
|
||||
[property: JsonPropertyName("registratiedatum")] DateOnly? Registratiedatum);
|
||||
|
||||
/// <summary>
|
||||
/// Anti-corruption map: ZGW Zaak → the existing <see cref="ApplicationSummaryDto"/> the FE
|
||||
/// Anti-corruption map: ZGW Zaak → the existing <see cref="AanvraagSummaryDto"/> the FE
|
||||
/// already renders (WP-49). This is where "URL as identity" and the cross-service zaaktype
|
||||
/// join get flattened away, so nothing downstream (the FE) sees ZGW shapes.
|
||||
/// </summary>
|
||||
@@ -29,7 +29,7 @@ public static class ZgwZaakMapper
|
||||
/// <summary>Last path segment of a ZGW resource URL — the uuid that identifies it.</summary>
|
||||
public static string Uuid(string url) => url.TrimEnd('/').Split('/').Last();
|
||||
|
||||
public static ApplicationSummaryDto ToSummaryDto(ZgwZaak z, string zaaktypeLabel)
|
||||
public static AanvraagSummaryDto ToSummaryDto(ZgwZaak z, string zaaktypeLabel)
|
||||
{
|
||||
// ponytail: coarse status map — an open zaak (no einddatum) is In behandeling, a closed
|
||||
// one is Goedgekeurd. Real fidelity (statustype/resultaat lookups) is a later slice; the
|
||||
@@ -41,7 +41,7 @@ public static class ZgwZaakMapper
|
||||
var created = Iso(z.Registratiedatum ?? z.Startdatum);
|
||||
var updated = Iso(z.Einddatum ?? z.Registratiedatum ?? z.Startdatum);
|
||||
|
||||
return new ApplicationSummaryDto(
|
||||
return new AanvraagSummaryDto(
|
||||
Id: Uuid(z.Url),
|
||||
Type: zaaktypeLabel,
|
||||
Status: status,
|
||||
|
||||
+109
-109
@@ -425,7 +425,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/applications": {
|
||||
"/api/v1/aanvragen": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
@@ -438,7 +438,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ApplicationSummaryDto"
|
||||
"$ref": "#/components/schemas/AanvraagSummaryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,7 +454,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateApplicationRequest"
|
||||
"$ref": "#/components/schemas/CreateAanvraagRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -466,7 +466,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApplicationDetailDto"
|
||||
"$ref": "#/components/schemas/AanvraagDetailDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,7 +484,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/applications/{id}": {
|
||||
"/api/v1/aanvragen/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
@@ -505,7 +505,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApplicationDetailDto"
|
||||
"$ref": "#/components/schemas/AanvraagDetailDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -592,7 +592,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/applications/{id}/submit": {
|
||||
"/api/v1/aanvragen/{id}/submit": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
@@ -611,7 +611,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SubmitApplicationRequest"
|
||||
"$ref": "#/components/schemas/AanvraagIndienenRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -623,7 +623,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SubmitApplicationResponse"
|
||||
"$ref": "#/components/schemas/AanvraagIndienenResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -667,7 +667,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ApplicationSummaryDto"
|
||||
"$ref": "#/components/schemas/AanvraagSummaryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -766,7 +766,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ApplicationSummaryDto"
|
||||
"$ref": "#/components/schemas/AanvraagSummaryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1484,57 +1484,7 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AanvraagStatusDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"stepIndex": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"stepCount": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"referentie": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"manual": {
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
},
|
||||
"reden": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AdresDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"straat": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"postcode": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"woonplaats": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ApplicationDetailDto": {
|
||||
"AanvraagDetailDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1573,7 +1523,83 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ApplicationSummaryDto": {
|
||||
"AanvraagIndienenRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"diplomaHerkomst": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"uren": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DocumentRefDto"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"aanvullendeScholing": {
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
},
|
||||
"scholingPunten": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AanvraagIndienenResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"referentie": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/AanvraagStatusDto"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AanvraagStatusDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"stepIndex": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"stepCount": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"referentie": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"manual": {
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
},
|
||||
"reden": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AanvraagSummaryDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -1613,6 +1639,24 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AdresDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"straat": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"postcode": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"woonplaats": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AuthzAuditDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1674,7 +1718,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"aanvraag": {
|
||||
"$ref": "#/components/schemas/ApplicationSummaryDto"
|
||||
"$ref": "#/components/schemas/AanvraagSummaryDto"
|
||||
},
|
||||
"documenten": {
|
||||
"type": "array",
|
||||
@@ -1860,7 +1904,7 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"CreateApplicationRequest": {
|
||||
"CreateAanvraagRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -2678,50 +2722,6 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SubmitApplicationRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"diplomaHerkomst": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"uren": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DocumentRefDto"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"aanvullendeScholing": {
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
},
|
||||
"scholingPunten": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SubmitApplicationResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"referentie": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/AanvraagStatusDto"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"UploadCategoriesDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+35
-35
@@ -8,27 +8,27 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
||||
|
||||
namespace BigRegister.Tests;
|
||||
|
||||
public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||
public class AanvraagTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client = factory.CreateClient();
|
||||
|
||||
private async Task<ApplicationDetailDto> Create(string type = "registratie")
|
||||
private async Task<AanvraagDetailDto> Create(string type = "registratie")
|
||||
{
|
||||
// WP-35: one Concept per type is now server-enforced, and these tests share one DB
|
||||
// (IClassFixture). Clear any leftover Concept so each test starts from a clean slate.
|
||||
var existing = await List();
|
||||
Assert.NotNull(existing);
|
||||
foreach (var s in existing.Where(x => x.Status.Tag == "Concept"))
|
||||
await _client.DeleteAsync($"/api/v1/applications/{s.Id}");
|
||||
var res = await _client.PostAsJsonAsync("/api/v1/applications", new { type });
|
||||
await _client.DeleteAsync($"/api/v1/aanvragen/{s.Id}");
|
||||
var res = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type });
|
||||
Assert.Equal(HttpStatusCode.Created, res.StatusCode);
|
||||
var created = await res.Content.ReadFromJsonAsync<ApplicationDetailDto>();
|
||||
var created = await res.Content.ReadFromJsonAsync<AanvraagDetailDto>();
|
||||
Assert.NotNull(created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private Task<List<ApplicationSummaryDto>?> List() =>
|
||||
_client.GetFromJsonAsync<List<ApplicationSummaryDto>>("/api/v1/applications");
|
||||
private Task<List<AanvraagSummaryDto>?> List() =>
|
||||
_client.GetFromJsonAsync<List<AanvraagSummaryDto>>("/api/v1/aanvragen");
|
||||
|
||||
// --- Lifecycle over HTTP ---
|
||||
|
||||
@@ -36,7 +36,7 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
public async Task Create_then_list_shows_a_concept_with_step_progress()
|
||||
{
|
||||
var a = await Create();
|
||||
await _client.PutAsJsonAsync($"/api/v1/applications/{a.Id}",
|
||||
await _client.PutAsJsonAsync($"/api/v1/aanvragen/{a.Id}",
|
||||
new { draft = new { beroep = "arts" }, stepIndex = 1, stepCount = 4 });
|
||||
|
||||
var list = await List();
|
||||
@@ -51,10 +51,10 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
public async Task Draft_sync_is_readable_back_from_detail()
|
||||
{
|
||||
var a = await Create();
|
||||
await _client.PutAsJsonAsync($"/api/v1/applications/{a.Id}",
|
||||
await _client.PutAsJsonAsync($"/api/v1/aanvragen/{a.Id}",
|
||||
new { draft = new { beroep = "verpleegkundige" }, stepIndex = 2, stepCount = 4 });
|
||||
|
||||
var detail = await _client.GetFromJsonAsync<ApplicationDetailDto>($"/api/v1/applications/{a.Id}");
|
||||
var detail = await _client.GetFromJsonAsync<AanvraagDetailDto>($"/api/v1/aanvragen/{a.Id}");
|
||||
Assert.NotNull(detail);
|
||||
Assert.NotNull(detail.Draft);
|
||||
Assert.Equal("verpleegkundige", detail.Draft.Value.GetProperty("beroep").GetString());
|
||||
@@ -64,9 +64,9 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
public async Task Submit_duo_registratie_is_in_behandeling_and_auto()
|
||||
{
|
||||
var a = await Create("registratie");
|
||||
var res = await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "duo" });
|
||||
var res = await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit", new { diplomaHerkomst = "duo" });
|
||||
res.EnsureSuccessStatusCode();
|
||||
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
||||
var body = (await res.Content.ReadFromJsonAsync<AanvraagIndienenResponse>())!;
|
||||
Assert.StartsWith("BIG-2026-", body.Referentie);
|
||||
Assert.Equal("InBehandeling", body.Status.Tag);
|
||||
Assert.False(body.Status.Manual); // auto-approvable → not a manual case
|
||||
@@ -76,9 +76,9 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
public async Task Submit_handmatig_registratie_succeeds_as_manual_case()
|
||||
{
|
||||
var a = await Create("registratie");
|
||||
var res = await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "handmatig" });
|
||||
var res = await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit", new { diplomaHerkomst = "handmatig" });
|
||||
res.EnsureSuccessStatusCode(); // no longer a 422
|
||||
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
||||
var body = (await res.Content.ReadFromJsonAsync<AanvraagIndienenResponse>())!;
|
||||
Assert.Equal("InBehandeling", body.Status.Tag);
|
||||
Assert.True(body.Status.Manual);
|
||||
}
|
||||
@@ -87,9 +87,9 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
public async Task Submit_herregistratie_with_zero_uren_is_afgewezen()
|
||||
{
|
||||
var a = await Create("herregistratie");
|
||||
var res = await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { uren = 0 });
|
||||
var res = await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit", new { uren = 0 });
|
||||
res.EnsureSuccessStatusCode(); // the submission is accepted...
|
||||
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
||||
var body = (await res.Content.ReadFromJsonAsync<AanvraagIndienenResponse>())!;
|
||||
Assert.Equal("Afgewezen", body.Status.Tag); // ...but resolves to rejected
|
||||
Assert.NotNull(body.Status.Reden);
|
||||
}
|
||||
@@ -98,8 +98,8 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
public async Task Submitting_twice_conflicts()
|
||||
{
|
||||
var a = await Create("registratie");
|
||||
(await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "duo" })).EnsureSuccessStatusCode();
|
||||
var again = await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "duo" });
|
||||
(await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit", new { diplomaHerkomst = "duo" })).EnsureSuccessStatusCode();
|
||||
var again = await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit", new { diplomaHerkomst = "duo" });
|
||||
Assert.Equal(HttpStatusCode.Conflict, again.StatusCode);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
public async Task Creating_a_second_concept_of_the_same_type_conflicts()
|
||||
{
|
||||
await Create("herregistratie");
|
||||
var dup = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "herregistratie" });
|
||||
var dup = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "herregistratie" });
|
||||
Assert.Equal(HttpStatusCode.Conflict, dup.StatusCode);
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
public async Task A_concept_of_a_different_type_is_allowed()
|
||||
{
|
||||
await Create("registratie");
|
||||
var other = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "herregistratie" });
|
||||
var other = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "herregistratie" });
|
||||
Assert.Equal(HttpStatusCode.Created, other.StatusCode);
|
||||
}
|
||||
|
||||
@@ -125,8 +125,8 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
public async Task A_new_concept_is_allowed_once_the_previous_one_is_submitted()
|
||||
{
|
||||
var a = await Create("registratie");
|
||||
(await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "duo" })).EnsureSuccessStatusCode();
|
||||
var next = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "registratie" });
|
||||
(await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit", new { diplomaHerkomst = "duo" })).EnsureSuccessStatusCode();
|
||||
var next = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "registratie" });
|
||||
Assert.Equal(HttpStatusCode.Created, next.StatusCode);
|
||||
}
|
||||
|
||||
@@ -134,38 +134,38 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
public async Task Cancel_concept_removes_it()
|
||||
{
|
||||
var a = await Create();
|
||||
Assert.Equal(HttpStatusCode.NoContent, (await _client.DeleteAsync($"/api/v1/applications/{a.Id}")).StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, (await _client.GetAsync($"/api/v1/applications/{a.Id}")).StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NoContent, (await _client.DeleteAsync($"/api/v1/aanvragen/{a.Id}")).StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, (await _client.GetAsync($"/api/v1/aanvragen/{a.Id}")).StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancel_submitted_aanvraag_conflicts()
|
||||
{
|
||||
var a = await Create("registratie");
|
||||
(await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "duo" })).EnsureSuccessStatusCode();
|
||||
Assert.Equal(HttpStatusCode.Conflict, (await _client.DeleteAsync($"/api/v1/applications/{a.Id}")).StatusCode);
|
||||
(await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit", new { diplomaHerkomst = "duo" })).EnsureSuccessStatusCode();
|
||||
Assert.Equal(HttpStatusCode.Conflict, (await _client.DeleteAsync($"/api/v1/aanvragen/{a.Id}")).StatusCode);
|
||||
}
|
||||
|
||||
// --- WP-53: citizen-scoping — GET /applications must never leak across identities. ---
|
||||
// --- WP-53: citizen-scoping — GET /aanvragen must never leak across identities. ---
|
||||
|
||||
[Fact]
|
||||
public async Task Applications_are_scoped_to_the_caller_bsn()
|
||||
{
|
||||
var mine = await Create("intake");
|
||||
|
||||
var createOther = new HttpRequestMessage(HttpMethod.Post, "/api/v1/applications")
|
||||
var createOther = new HttpRequestMessage(HttpMethod.Post, "/api/v1/aanvragen")
|
||||
{
|
||||
Content = JsonContent.Create(new { type = "intake" }),
|
||||
Headers = { { "X-Subject", "999888777" } },
|
||||
};
|
||||
var otherRes = await _client.SendAsync(createOther);
|
||||
Assert.Equal(HttpStatusCode.Created, otherRes.StatusCode);
|
||||
var other = (await otherRes.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
||||
var other = (await otherRes.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
||||
|
||||
try
|
||||
{
|
||||
var listOther = new HttpRequestMessage(HttpMethod.Get, "/api/v1/applications") { Headers = { { "X-Subject", "999888777" } } };
|
||||
var theirCases = (await (await _client.SendAsync(listOther)).Content.ReadFromJsonAsync<List<ApplicationSummaryDto>>())!;
|
||||
var listOther = new HttpRequestMessage(HttpMethod.Get, "/api/v1/aanvragen") { Headers = { { "X-Subject", "999888777" } } };
|
||||
var theirCases = (await (await _client.SendAsync(listOther)).Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
|
||||
Assert.Contains(theirCases, c => c.Id == other.Id);
|
||||
Assert.DoesNotContain(theirCases, c => c.Id == mine.Id);
|
||||
|
||||
@@ -175,9 +175,9 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
}
|
||||
finally
|
||||
{
|
||||
var deleteOther = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/applications/{other.Id}") { Headers = { { "X-Subject", "999888777" } } };
|
||||
var deleteOther = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/aanvragen/{other.Id}") { Headers = { { "X-Subject", "999888777" } } };
|
||||
await _client.SendAsync(deleteOther);
|
||||
await _client.DeleteAsync($"/api/v1/applications/{mine.Id}");
|
||||
await _client.DeleteAsync($"/api/v1/aanvragen/{mine.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
var foreignDoc = await UploadAs(_client, "999888777", Guid.NewGuid().ToString());
|
||||
var a = await Create("registratie");
|
||||
|
||||
var res = await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit",
|
||||
var res = await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit",
|
||||
new { diplomaHerkomst = "duo", documents = new[] { new { categoryId = "diploma", channel = "digital", documentId = foreignDoc.DocumentId } } });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
||||
|
||||
@@ -221,7 +221,7 @@ public class ApplicationTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
var foreignDoc = await UploadAs(_client, "999888777", Guid.NewGuid().ToString());
|
||||
var a = await Create("registratie");
|
||||
|
||||
var res = await _client.PutAsJsonAsync($"/api/v1/applications/{a.Id}",
|
||||
var res = await _client.PutAsJsonAsync($"/api/v1/aanvragen/{a.Id}",
|
||||
new { draft = new { }, stepIndex = 0, stepCount = 1, documentIds = new[] { foreignDoc.DocumentId } });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace BigRegister.Tests.Acceptance;
|
||||
|
||||
/// <summary>
|
||||
/// Behaviour-level tests for the scholing-threshold enforcement (WP-69) over
|
||||
/// <c>POST /applications/{id}/submit</c> (the wizard's real path — WP-72 deleted the legacy
|
||||
/// <c>POST /aanvragen/{id}/submit</c> (the wizard's real path — WP-72 deleted the legacy
|
||||
/// <c>POST /intakes</c> endpoint this once also covered). Built through the <see
|
||||
/// cref="Given"/> type-state builder, mirroring <see cref="BesluitLifecycleTests"/> rather
|
||||
/// than the full wizard/upload dance — the builder's default owner IS <see
|
||||
@@ -28,7 +28,7 @@ public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFi
|
||||
}
|
||||
|
||||
private Task<HttpResponseMessage> Submit(string id, object body) =>
|
||||
_client.PostAsJsonAsync($"/api/v1/applications/{id}/submit", body);
|
||||
_client.PostAsJsonAsync($"/api/v1/aanvragen/{id}/submit", body);
|
||||
|
||||
[Fact]
|
||||
public async Task Below_threshold_without_an_answer_is_rejected_and_stays_a_concept()
|
||||
@@ -117,7 +117,7 @@ public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFi
|
||||
|
||||
// Then the submission is accepted and resolves to Afgewezen — not a 400.
|
||||
res.EnsureSuccessStatusCode();
|
||||
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
||||
var body = (await res.Content.ReadFromJsonAsync<AanvraagIndienenResponse>())!;
|
||||
Assert.Equal("Afgewezen", body.Status.Tag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ public class AdminCasesTests(TestWebApplicationFactory factory) : IClassFixture<
|
||||
return req;
|
||||
}
|
||||
|
||||
private async Task<ApplicationDetailDto> Create(string type)
|
||||
private async Task<AanvraagDetailDto> Create(string type)
|
||||
{
|
||||
var res = await _client.PostAsJsonAsync("/api/v1/applications", new { type });
|
||||
var res = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type });
|
||||
Assert.Equal(HttpStatusCode.Created, res.StatusCode);
|
||||
return (await res.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
||||
return (await res.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -33,7 +33,7 @@ public class AdminCasesTests(TestWebApplicationFactory factory) : IClassFixture<
|
||||
{
|
||||
var list = await _client.SendAsync(Admin(HttpMethod.Get, "/api/v1/admin/cases"));
|
||||
list.EnsureSuccessStatusCode();
|
||||
var cases = (await list.Content.ReadFromJsonAsync<List<ApplicationSummaryDto>>())!;
|
||||
var cases = (await list.Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
|
||||
var mine = cases.Single(x => x.Id == a.Id);
|
||||
// RB-03/BIO-003: the owner is carried, but masked — it is a BSN, and this list is
|
||||
// read by someone who is not the subject.
|
||||
@@ -57,13 +57,13 @@ public class AdminCasesTests(TestWebApplicationFactory factory) : IClassFixture<
|
||||
public async Task Admin_can_delete_a_submitted_case()
|
||||
{
|
||||
var a = await Create("registratie");
|
||||
(await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { diplomaHerkomst = "duo" }))
|
||||
(await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit", new { diplomaHerkomst = "duo" }))
|
||||
.EnsureSuccessStatusCode();
|
||||
|
||||
// The user-facing DELETE refuses a submitted case (409); admin delete removes it.
|
||||
var del = await _client.SendAsync(Admin(HttpMethod.Delete, $"/api/v1/admin/cases/{a.Id}"));
|
||||
Assert.Equal(HttpStatusCode.NoContent, del.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, (await _client.GetAsync($"/api/v1/applications/{a.Id}")).StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, (await _client.GetAsync($"/api/v1/aanvragen/{a.Id}")).StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -17,12 +17,12 @@ namespace BigRegister.Tests;
|
||||
file sealed class IdMismatchZaakSource : IZaakSource
|
||||
{
|
||||
private readonly LocalZaakSource inner = new();
|
||||
private static ApplicationSummaryDto Rekey(ApplicationSummaryDto dto) => dto with { Id = $"zaak-{dto.Id}" };
|
||||
private static AanvraagSummaryDto Rekey(AanvraagSummaryDto dto) => dto with { Id = $"zaak-{dto.Id}" };
|
||||
|
||||
public IReadOnlyList<ApplicationSummaryDto> ListCases(DateTimeOffset now) =>
|
||||
public IReadOnlyList<AanvraagSummaryDto> ListCases(DateTimeOffset now) =>
|
||||
inner.ListCases(now).Select(Rekey).ToList();
|
||||
|
||||
public IReadOnlyList<ApplicationSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
||||
public IReadOnlyList<AanvraagSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
||||
inner.ListMyCases(caller, now).Select(Rekey).ToList();
|
||||
|
||||
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(
|
||||
@@ -67,13 +67,13 @@ public class BeoordelingIdMismatchTests
|
||||
using var factory = Factory();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var created = await client.PostAsJsonAsync("/api/v1/applications", new { type = "registratie" });
|
||||
var app = (await created.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
||||
var submit = await client.PostAsJsonAsync($"/api/v1/applications/{app.Id}/submit", new { diplomaHerkomst = "handmatig" });
|
||||
var created = await client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "registratie" });
|
||||
var app = (await created.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
||||
var submit = await client.PostAsJsonAsync($"/api/v1/aanvragen/{app.Id}/submit", new { diplomaHerkomst = "handmatig" });
|
||||
submit.EnsureSuccessStatusCode();
|
||||
|
||||
var werkvoorraad = await client.SendAsync(Behandelaar(HttpMethod.Get, "/api/v1/werkvoorraad"));
|
||||
var items = (await werkvoorraad.Content.ReadFromJsonAsync<List<ApplicationSummaryDto>>())!;
|
||||
var items = (await werkvoorraad.Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
|
||||
var caseId = Assert.Single(items).Id;
|
||||
// Sanity: the id divergence this test exists for is real, not accidentally absent.
|
||||
Assert.NotEqual(app.Id, caseId);
|
||||
|
||||
@@ -35,17 +35,17 @@ public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
/// A manual (never auto-approved) case with one linked document, so it stays
|
||||
/// InBehandeling/decidable regardless of test timing (the 8s auto-approval window
|
||||
/// would otherwise make a duo-registratie/herregistratie fixture flaky).
|
||||
private async Task<(ApplicationDetailDto App, string DocumentId)> CreateManualCaseWithDocument()
|
||||
private async Task<(AanvraagDetailDto App, string DocumentId)> CreateManualCaseWithDocument()
|
||||
{
|
||||
var created = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "registratie" });
|
||||
var a = (await created.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
||||
var created = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "registratie" });
|
||||
var a = (await created.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
||||
|
||||
var localId = Guid.NewGuid().ToString();
|
||||
var upload = await _client.PostAsync("/api/v1/uploads", UploadForm(localId, "diploma", "diploma.pdf"));
|
||||
upload.EnsureSuccessStatusCode();
|
||||
var doc = (await upload.Content.ReadFromJsonAsync<UploadResponse>())!;
|
||||
|
||||
var submit = await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new
|
||||
var submit = await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit", new
|
||||
{
|
||||
diplomaHerkomst = "handmatig",
|
||||
documents = new[] { new { categoryId = "diploma", channel = "digital", documentId = doc.DocumentId } },
|
||||
@@ -88,8 +88,8 @@ public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
[Fact]
|
||||
public async Task Concept_and_unknown_id_are_not_found()
|
||||
{
|
||||
var created = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "registratie" });
|
||||
var concept = (await created.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
||||
var created = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "registratie" });
|
||||
var concept = (await created.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
||||
try
|
||||
{
|
||||
var conceptRes = await _client.SendAsync(AsBehandelaar(HttpMethod.Get, $"/api/v1/beoordeling/{concept.Id}"));
|
||||
@@ -100,7 +100,7 @@ public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _client.DeleteAsync($"/api/v1/applications/{concept.Id}");
|
||||
await _client.DeleteAsync($"/api/v1/aanvragen/{concept.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,9 +202,9 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
|
||||
var doc = await Upload(Guid.NewGuid().ToString());
|
||||
// Through the real submit path (RB-06 deleted POST /registrations, which was the only
|
||||
// other caller of DocumentStore.Link and had no ownership guard on it).
|
||||
var created = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "registratie" });
|
||||
var aanvraag = (await created.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
||||
var submit = await _client.PostAsJsonAsync($"/api/v1/applications/{aanvraag.Id}/submit",
|
||||
var created = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "registratie" });
|
||||
var aanvraag = (await created.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
||||
var submit = await _client.PostAsJsonAsync($"/api/v1/aanvragen/{aanvraag.Id}/submit",
|
||||
new { diplomaHerkomst = "duo", documents = new[] { new DocumentRefDto("diploma", "digital", doc.DocumentId) } });
|
||||
submit.EnsureSuccessStatusCode();
|
||||
Assert.Equal(HttpStatusCode.Conflict, (await _client.DeleteAsync($"/api/v1/uploads/{doc.DocumentId}")).StatusCode);
|
||||
|
||||
@@ -51,16 +51,16 @@ public class FeatureFlagTests(TestWebApplicationFactory factory) : IClassFixture
|
||||
{
|
||||
try
|
||||
{
|
||||
// Off → POST /applications for a registratie is refused.
|
||||
// Off → POST /aanvragen for a registratie is refused.
|
||||
(await _client.SendAsync(Admin(HttpMethod.Put, $"/api/v1/admin/flags/{FeatureFlags.InschrijvingOpen}", new { enabled = false })))
|
||||
.EnsureSuccessStatusCode();
|
||||
var blocked = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "registratie" });
|
||||
var blocked = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "registratie" });
|
||||
Assert.Equal(HttpStatusCode.Forbidden, blocked.StatusCode);
|
||||
|
||||
// On → allowed again.
|
||||
(await _client.SendAsync(Admin(HttpMethod.Put, $"/api/v1/admin/flags/{FeatureFlags.InschrijvingOpen}", new { enabled = true })))
|
||||
.EnsureSuccessStatusCode();
|
||||
var ok = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "registratie" });
|
||||
var ok = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "registratie" });
|
||||
Assert.Equal(HttpStatusCode.Created, ok.StatusCode);
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -63,7 +63,7 @@ public class OpenZaakIntegrationTests
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Role", "admin"); // CasesAdmin gate (cases:manage)
|
||||
|
||||
var cases = await client.GetFromJsonAsync<List<ApplicationSummaryDto>>("/api/v1/admin/cases");
|
||||
var cases = await client.GetFromJsonAsync<List<AanvraagSummaryDto>>("/api/v1/admin/cases");
|
||||
|
||||
Assert.NotNull(cases);
|
||||
// bootstrap-catalogus.sh seeds exactly one zaak, identificatie BIG-2026-000123.
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace BigRegister.Tests;
|
||||
/// - it is named, with a reason, in <see cref="AllowList"/> below.
|
||||
///
|
||||
/// The allow-list is deliberately not "public routes" — most of its entries are NOT public.
|
||||
/// `GET /applications/{id}` requires a caller identity and is scoped to that caller's own BSN
|
||||
/// `GET /aanvragen/{id}` requires a caller identity and is scoped to that caller's own BSN
|
||||
/// inline (`ctx.Zorgverlener()`), not through one of the five wrappers, which only gate the
|
||||
/// coarse admin/behandelaar surfaces. Recording that here, with the actual reason, is the point
|
||||
/// of BIO-016's remediation ("makes 'this endpoint is public' a decision someone wrote down")
|
||||
@@ -58,12 +58,12 @@ public class RouteInventoryTests(TestWebApplicationFactory factory) : IClassFixt
|
||||
new("GET", "/api/v1/uploads/{documentId}/content", "Ownership-scoped inline (RB-01/BIO-004): owning citizen, or a behandelaar via Authz.CanBeoordelen."),
|
||||
new("GET", "/api/v1/uploads/status", "Ownership-scoped inline: DocumentStore.ByLocalIds filtered to ctx.Zorgverlener().Bsn."),
|
||||
new("DELETE", "/api/v1/uploads/{documentId}", "Ownership-scoped inline: DocumentStore.DeleteOwned keyed by ctx.Zorgverlener().Bsn."),
|
||||
new("GET", "/api/v1/applications", "Ownership-scoped inline: IZaakSource.ListMyCases(ctx.Zorgverlener(), ...)."),
|
||||
new("GET", "/api/v1/applications/{id}", "Ownership-scoped inline: ApplicationStore.Get(id, ctx.Zorgverlener().Bsn)."),
|
||||
new("POST", "/api/v1/applications", "Ownership-scoped inline: created under ctx.Zorgverlener().Bsn."),
|
||||
new("PUT", "/api/v1/applications/{id}", "Ownership-scoped inline: ApplicationStore.SyncDraft keyed by ctx.Zorgverlener().Bsn."),
|
||||
new("DELETE", "/api/v1/applications/{id}", "Ownership-scoped inline: ApplicationStore.Get/.Delete keyed by ctx.Zorgverlener().Bsn."),
|
||||
new("POST", "/api/v1/applications/{id}/submit", "Ownership-scoped inline: ApplicationStore.Submit keyed by ctx.Zorgverlener().Bsn."),
|
||||
new("GET", "/api/v1/aanvragen", "Ownership-scoped inline: IZaakSource.ListMyCases(ctx.Zorgverlener(), ...)."),
|
||||
new("GET", "/api/v1/aanvragen/{id}", "Ownership-scoped inline: ApplicationStore.Get(id, ctx.Zorgverlener().Bsn)."),
|
||||
new("POST", "/api/v1/aanvragen", "Ownership-scoped inline: created under ctx.Zorgverlener().Bsn."),
|
||||
new("PUT", "/api/v1/aanvragen/{id}", "Ownership-scoped inline: ApplicationStore.SyncDraft keyed by ctx.Zorgverlener().Bsn."),
|
||||
new("DELETE", "/api/v1/aanvragen/{id}", "Ownership-scoped inline: ApplicationStore.Get/.Delete keyed by ctx.Zorgverlener().Bsn."),
|
||||
new("POST", "/api/v1/aanvragen/{id}/submit", "Ownership-scoped inline: ApplicationStore.Submit keyed by ctx.Zorgverlener().Bsn."),
|
||||
|
||||
// --- External caller, not a Principal at all. ---
|
||||
new("POST", "/api/v1/zgw/notificaties", "OpenZaak's NRC, not a user: gated by a fixed-time shared-secret comparison, audited directly."),
|
||||
|
||||
@@ -18,11 +18,11 @@ public class WerkvoorraadTests(TestWebApplicationFactory factory) : IClassFixtur
|
||||
return req;
|
||||
}
|
||||
|
||||
private async Task<ApplicationDetailDto> CreateAndSubmitHerregistratie()
|
||||
private async Task<AanvraagDetailDto> CreateAndSubmitHerregistratie()
|
||||
{
|
||||
var created = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "herregistratie" });
|
||||
var a = (await created.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
||||
(await _client.PostAsJsonAsync($"/api/v1/applications/{a.Id}/submit", new { uren = 200 }))
|
||||
var created = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "herregistratie" });
|
||||
var a = (await created.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
||||
(await _client.PostAsJsonAsync($"/api/v1/aanvragen/{a.Id}/submit", new { uren = 200 }))
|
||||
.EnsureSuccessStatusCode();
|
||||
return a;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ public class WerkvoorraadTests(TestWebApplicationFactory factory) : IClassFixtur
|
||||
{
|
||||
var res = await _client.SendAsync(AsBehandelaar("/api/v1/werkvoorraad"));
|
||||
res.EnsureSuccessStatusCode();
|
||||
var queue = (await res.Content.ReadFromJsonAsync<List<ApplicationSummaryDto>>())!;
|
||||
var queue = (await res.Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
|
||||
var mine = queue.Single(x => x.Id == a.Id);
|
||||
Assert.Equal("InBehandeling", mine.Status.Tag);
|
||||
// RB-03/BIO-003: masked, like /admin/cases — both inherit ToAdminSummaryDto.
|
||||
@@ -53,18 +53,18 @@ public class WerkvoorraadTests(TestWebApplicationFactory factory) : IClassFixtur
|
||||
[Fact]
|
||||
public async Task Queue_excludes_concepts()
|
||||
{
|
||||
var created = await _client.PostAsJsonAsync("/api/v1/applications", new { type = "herregistratie" });
|
||||
var a = (await created.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
||||
var created = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "herregistratie" });
|
||||
var a = (await created.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
||||
try
|
||||
{
|
||||
var res = await _client.SendAsync(AsBehandelaar("/api/v1/werkvoorraad"));
|
||||
res.EnsureSuccessStatusCode();
|
||||
var queue = (await res.Content.ReadFromJsonAsync<List<ApplicationSummaryDto>>())!;
|
||||
var queue = (await res.Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
|
||||
Assert.DoesNotContain(queue, x => x.Id == a.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _client.DeleteAsync($"/api/v1/applications/{a.Id}");
|
||||
await _client.DeleteAsync($"/api/v1/aanvragen/{a.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,14 +39,14 @@ public class ZgwDivergenceTests
|
||||
b.ConfigurePrimaryHttpMessageHandler(() => stub))));
|
||||
}
|
||||
|
||||
/// <summary>Doesn't call GET /applications first (unlike ApplicationTests.Create) — under
|
||||
/// <summary>Doesn't call GET /aanvragen first (unlike ApplicationTests.Create) — under
|
||||
/// Zgw:Enabled=true that route goes through IZaakSource too, which this test's stub doesn't
|
||||
/// need to answer since every test here uses a fresh db and creates exactly one aanvraag.</summary>
|
||||
private static async Task<string> CreateConcept(HttpClient client, string type = "registratie")
|
||||
{
|
||||
var res = await client.PostAsJsonAsync("/api/v1/applications", new { type });
|
||||
var res = await client.PostAsJsonAsync("/api/v1/aanvragen", new { type });
|
||||
res.EnsureSuccessStatusCode();
|
||||
var body = (await res.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
||||
var body = (await res.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
||||
return body.Id;
|
||||
}
|
||||
|
||||
@@ -78,11 +78,11 @@ public class ZgwDivergenceTests
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var id = await CreateConcept(client);
|
||||
var res = await client.PostAsJsonAsync($"/api/v1/applications/{id}/submit", new { diplomaHerkomst = "duo" });
|
||||
var res = await client.PostAsJsonAsync($"/api/v1/aanvragen/{id}/submit", new { diplomaHerkomst = "duo" });
|
||||
|
||||
// The local write is still authoritative: 200 with a real reference, not a 500.
|
||||
res.EnsureSuccessStatusCode();
|
||||
var body = (await res.Content.ReadFromJsonAsync<SubmitApplicationResponse>())!;
|
||||
var body = (await res.Content.ReadFromJsonAsync<AanvraagIndienenResponse>())!;
|
||||
Assert.NotEmpty(body.Referentie);
|
||||
|
||||
var stored = ApplicationStore.ListAll().Single(a => a.Id == id);
|
||||
@@ -103,7 +103,7 @@ public class ZgwDivergenceTests
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var id = await CreateConcept(client);
|
||||
var res = await client.PostAsJsonAsync($"/api/v1/applications/{id}/submit", new { diplomaHerkomst = "duo" });
|
||||
var res = await client.PostAsJsonAsync($"/api/v1/aanvragen/{id}/submit", new { diplomaHerkomst = "duo" });
|
||||
res.EnsureSuccessStatusCode();
|
||||
|
||||
var stored = ApplicationStore.ListAll().Single(a => a.Id == id);
|
||||
@@ -126,7 +126,7 @@ public class ZgwDivergenceTests
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var id = await CreateConcept(client);
|
||||
(await client.PostAsJsonAsync($"/api/v1/applications/{id}/submit", new { diplomaHerkomst = "duo" }))
|
||||
(await client.PostAsJsonAsync($"/api/v1/aanvragen/{id}/submit", new { diplomaHerkomst = "duo" }))
|
||||
.EnsureSuccessStatusCode();
|
||||
|
||||
var error = ApplicationStore.ListAll().Single(a => a.Id == id).ZgwError;
|
||||
|
||||
@@ -20,7 +20,7 @@ tested where._
|
||||
|
||||
Every bullet below is a real test name from the suite — an `it()` title (frontend) or a test
|
||||
method name (backend), read as a sentence. Nothing here is hand-written prose: this page
|
||||
**is** the suite, reshaped for a business reader. 497 frontend behaviours across
|
||||
**is** the suite, reshaped for a business reader. 505 frontend behaviours across
|
||||
9 contexts; 261 backend behaviours across 42 test
|
||||
classes.
|
||||
|
||||
@@ -418,6 +418,13 @@ classes.
|
||||
|
||||
### registratie
|
||||
|
||||
#### AanvragenStore
|
||||
|
||||
- loads and parses the list
|
||||
- cancels optimistically and confirms via the DELETE endpoint
|
||||
- rolls back the removal and surfaces the error when the cancel fails
|
||||
- clears a stale error on the next cancel attempt
|
||||
|
||||
#### AdminCasesStore
|
||||
|
||||
- loads and parses the cross-owner list
|
||||
@@ -425,13 +432,6 @@ classes.
|
||||
- rolls back the removal and surfaces the error when the delete fails
|
||||
- clears a stale error on the next delete attempt
|
||||
|
||||
#### ApplicationsStore
|
||||
|
||||
- loads and parses the list
|
||||
- cancels optimistically and confirms via the DELETE endpoint
|
||||
- rolls back the removal and surfaces the error when the cancel fails
|
||||
- clears a stale error on the next cancel attempt
|
||||
|
||||
#### STEPS (fixed)
|
||||
|
||||
- always has the same three steps
|
||||
@@ -544,12 +544,12 @@ classes.
|
||||
- parses each tag with its required fields
|
||||
- rejects a missing status, unknown tag, and wrong-typed fields
|
||||
|
||||
#### parseApplicationSummary
|
||||
#### parseAanvraagSummary
|
||||
|
||||
- maps a valid DTO to domain
|
||||
- rejects a bad type and non-objects
|
||||
|
||||
#### parseApplications / parseApplicationDetail
|
||||
#### parseAanvragen / parseAanvraagDetail
|
||||
|
||||
- parses a list and fails fast on a bad element
|
||||
- carries the opaque draft through detail
|
||||
@@ -569,6 +569,9 @@ classes.
|
||||
|
||||
- maps a valid response into a DashboardView
|
||||
- rejects malformed responses instead of trusting them
|
||||
- rejects a status whose tag is present but its required fields are missing
|
||||
- rejects an unknown status tag
|
||||
- rejects a person with an incomplete adres
|
||||
|
||||
#### parseDuoLookup (trust boundary)
|
||||
|
||||
@@ -617,6 +620,12 @@ classes.
|
||||
- statusColor is total over the union
|
||||
- herregistratieDeadline is only set for an active registration
|
||||
|
||||
#### sortForDashboard / concepten / ingediend
|
||||
|
||||
- sorts Concept, then still-open, then resolved last
|
||||
- does not mutate the input array
|
||||
- concepten/ingediend split on the Concept tag
|
||||
|
||||
#### submit
|
||||
|
||||
- stays in Invullen when the draft is incomplete (no diploma)
|
||||
@@ -923,6 +932,11 @@ classes.
|
||||
- leaves a non-API request untouched even when a subject is known
|
||||
- sends no header at all when no subject has ever been seen
|
||||
|
||||
#### successOf
|
||||
|
||||
- unwraps a Success value
|
||||
- is undefined for every other state
|
||||
|
||||
#### upload lifecycle messages
|
||||
|
||||
- queued → progress → complete
|
||||
@@ -963,18 +977,7 @@ classes.
|
||||
|
||||
## Backend (by test class)
|
||||
|
||||
### AdminCasesTests
|
||||
|
||||
- Admin lists every case with its owner
|
||||
- Non admin is forbidden
|
||||
- Admin can delete a submitted case
|
||||
- Deleting a missing case is not found
|
||||
|
||||
### ApplicationRuleTests
|
||||
|
||||
- AanvraagStatusTag covers the published lifecycle
|
||||
|
||||
### ApplicationTests
|
||||
### AanvraagTests
|
||||
|
||||
- Create then list shows a concept with step progress
|
||||
- Draft sync is readable back from detail
|
||||
@@ -993,6 +996,17 @@ classes.
|
||||
- AutoApprovable flips to goedgekeurd after the window
|
||||
- Manual case never auto advances
|
||||
|
||||
### AdminCasesTests
|
||||
|
||||
- Admin lists every case with its owner
|
||||
- Non admin is forbidden
|
||||
- Admin can delete a submitted case
|
||||
- Deleting a missing case is not found
|
||||
|
||||
### ApplicationRuleTests
|
||||
|
||||
- AanvraagStatusTag covers the published lifecycle
|
||||
|
||||
### AuthzAuditTests
|
||||
|
||||
- A denied admin action is recorded
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RemoteData, map2, map } from './remote-data';
|
||||
import { loading, failure, success } from '../testing/remote-data';
|
||||
import { RemoteData, map2, map, successOf } from './remote-data';
|
||||
import { loading, failure, empty, success } from '../testing/remote-data';
|
||||
|
||||
const loadingRd: RemoteData<string, number> = loading();
|
||||
const failureRd: RemoteData<string, number> = failure('x');
|
||||
@@ -21,3 +21,15 @@ describe('RemoteData combinators', () => {
|
||||
expect(map2(ok(2), ok(3), add)).toEqual({ tag: 'Success', value: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('successOf', () => {
|
||||
it('unwraps a Success value', () => {
|
||||
expect(successOf(ok(2))).toBe(2);
|
||||
});
|
||||
|
||||
it('is undefined for every other state', () => {
|
||||
expect(successOf(loadingRd)).toBeUndefined();
|
||||
expect(successOf(failureRd)).toBeUndefined();
|
||||
expect(successOf(empty())).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,3 +80,12 @@ export function andThen<E, A, B>(
|
||||
): RemoteData<E, B> {
|
||||
return rd.tag === 'Success' ? f(rd.value) : rd;
|
||||
}
|
||||
|
||||
/** Unwrap a Success value, or `undefined` for every other state. Used to narrow
|
||||
an `<app-async>` loaded slot: `<ng-template>` can't inherit a generic from a
|
||||
sibling host input (Angular only infers a structural directive's type
|
||||
parameter from an input on that same node), so the caller unwraps here
|
||||
instead of through `let-`. */
|
||||
export function successOf<E, T>(rd: RemoteData<E, T>): T | undefined {
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
}
|
||||
|
||||
@@ -637,8 +637,8 @@ export class ApiClient {
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
applicationsAll(): Promise<ApplicationSummaryDto[]> {
|
||||
let url_ = this.baseUrl + "/api/v1/applications";
|
||||
aanvragenAll(): Promise<AanvraagSummaryDto[]> {
|
||||
let url_ = this.baseUrl + "/api/v1/aanvragen";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: RequestInit = {
|
||||
@@ -649,17 +649,17 @@ export class ApiClient {
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processApplicationsAll(_response);
|
||||
return this.processAanvragenAll(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processApplicationsAll(response: Response): Promise<ApplicationSummaryDto[]> {
|
||||
protected processAanvragenAll(response: Response): Promise<AanvraagSummaryDto[]> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ApplicationSummaryDto[];
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AanvraagSummaryDto[];
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
@@ -667,14 +667,14 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<ApplicationSummaryDto[]>(null as any);
|
||||
return Promise.resolve<AanvraagSummaryDto[]>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Created
|
||||
*/
|
||||
applicationsPOST(body: CreateApplicationRequest): Promise<ApplicationDetailDto> {
|
||||
let url_ = this.baseUrl + "/api/v1/applications";
|
||||
aanvragenPOST(body: CreateAanvraagRequest): Promise<AanvraagDetailDto> {
|
||||
let url_ = this.baseUrl + "/api/v1/aanvragen";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(body);
|
||||
@@ -689,17 +689,17 @@ export class ApiClient {
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processApplicationsPOST(_response);
|
||||
return this.processAanvragenPOST(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processApplicationsPOST(response: Response): Promise<ApplicationDetailDto> {
|
||||
protected processAanvragenPOST(response: Response): Promise<AanvraagDetailDto> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 201) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result201: any = null;
|
||||
result201 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ApplicationDetailDto;
|
||||
result201 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AanvraagDetailDto;
|
||||
return result201;
|
||||
});
|
||||
} else if (status === 409) {
|
||||
@@ -713,14 +713,14 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<ApplicationDetailDto>(null as any);
|
||||
return Promise.resolve<AanvraagDetailDto>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
applicationsGET(id: string): Promise<ApplicationDetailDto> {
|
||||
let url_ = this.baseUrl + "/api/v1/applications/{id}";
|
||||
aanvragenGET(id: string): Promise<AanvraagDetailDto> {
|
||||
let url_ = this.baseUrl + "/api/v1/aanvragen/{id}";
|
||||
if (id === undefined || id === null)
|
||||
throw new globalThis.Error("The parameter 'id' must be defined.");
|
||||
url_ = url_.replace("{id}", encodeURIComponent("" + id));
|
||||
@@ -734,17 +734,17 @@ export class ApiClient {
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processApplicationsGET(_response);
|
||||
return this.processAanvragenGET(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processApplicationsGET(response: Response): Promise<ApplicationDetailDto> {
|
||||
protected processAanvragenGET(response: Response): Promise<AanvraagDetailDto> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ApplicationDetailDto;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AanvraagDetailDto;
|
||||
return result200;
|
||||
});
|
||||
} else if (status === 404) {
|
||||
@@ -756,14 +756,14 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<ApplicationDetailDto>(null as any);
|
||||
return Promise.resolve<AanvraagDetailDto>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return No Content
|
||||
*/
|
||||
applicationsPUT(id: string, body: DraftSyncRequest): Promise<void> {
|
||||
let url_ = this.baseUrl + "/api/v1/applications/{id}";
|
||||
aanvragenPUT(id: string, body: DraftSyncRequest): Promise<void> {
|
||||
let url_ = this.baseUrl + "/api/v1/aanvragen/{id}";
|
||||
if (id === undefined || id === null)
|
||||
throw new globalThis.Error("The parameter 'id' must be defined.");
|
||||
url_ = url_.replace("{id}", encodeURIComponent("" + id));
|
||||
@@ -780,11 +780,11 @@ export class ApiClient {
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processApplicationsPUT(_response);
|
||||
return this.processAanvragenPUT(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processApplicationsPUT(response: Response): Promise<void> {
|
||||
protected processAanvragenPUT(response: Response): Promise<void> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 204) {
|
||||
@@ -812,8 +812,8 @@ export class ApiClient {
|
||||
/**
|
||||
* @return No Content
|
||||
*/
|
||||
applicationsDELETE(id: string): Promise<void> {
|
||||
let url_ = this.baseUrl + "/api/v1/applications/{id}";
|
||||
aanvragenDELETE(id: string): Promise<void> {
|
||||
let url_ = this.baseUrl + "/api/v1/aanvragen/{id}";
|
||||
if (id === undefined || id === null)
|
||||
throw new globalThis.Error("The parameter 'id' must be defined.");
|
||||
url_ = url_.replace("{id}", encodeURIComponent("" + id));
|
||||
@@ -826,11 +826,11 @@ export class ApiClient {
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processApplicationsDELETE(_response);
|
||||
return this.processAanvragenDELETE(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processApplicationsDELETE(response: Response): Promise<void> {
|
||||
protected processAanvragenDELETE(response: Response): Promise<void> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 204) {
|
||||
@@ -858,8 +858,8 @@ export class ApiClient {
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
submit(id: string, body: SubmitApplicationRequest): Promise<SubmitApplicationResponse> {
|
||||
let url_ = this.baseUrl + "/api/v1/applications/{id}/submit";
|
||||
submit(id: string, body: AanvraagIndienenRequest): Promise<AanvraagIndienenResponse> {
|
||||
let url_ = this.baseUrl + "/api/v1/aanvragen/{id}/submit";
|
||||
if (id === undefined || id === null)
|
||||
throw new globalThis.Error("The parameter 'id' must be defined.");
|
||||
url_ = url_.replace("{id}", encodeURIComponent("" + id));
|
||||
@@ -881,13 +881,13 @@ export class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
protected processSubmit(response: Response): Promise<SubmitApplicationResponse> {
|
||||
protected processSubmit(response: Response): Promise<AanvraagIndienenResponse> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as SubmitApplicationResponse;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AanvraagIndienenResponse;
|
||||
return result200;
|
||||
});
|
||||
} else if (status === 400) {
|
||||
@@ -911,13 +911,13 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<SubmitApplicationResponse>(null as any);
|
||||
return Promise.resolve<AanvraagIndienenResponse>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
casesAll(): Promise<ApplicationSummaryDto[]> {
|
||||
casesAll(): Promise<AanvraagSummaryDto[]> {
|
||||
let url_ = this.baseUrl + "/api/v1/admin/cases";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
@@ -933,13 +933,13 @@ export class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
protected processCasesAll(response: Response): Promise<ApplicationSummaryDto[]> {
|
||||
protected processCasesAll(response: Response): Promise<AanvraagSummaryDto[]> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ApplicationSummaryDto[];
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AanvraagSummaryDto[];
|
||||
return result200;
|
||||
});
|
||||
} else if (status === 403) {
|
||||
@@ -953,7 +953,7 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<ApplicationSummaryDto[]>(null as any);
|
||||
return Promise.resolve<AanvraagSummaryDto[]>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1047,7 +1047,7 @@ export class ApiClient {
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
werkvoorraad(): Promise<ApplicationSummaryDto[]> {
|
||||
werkvoorraad(): Promise<AanvraagSummaryDto[]> {
|
||||
let url_ = this.baseUrl + "/api/v1/werkvoorraad";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
@@ -1063,13 +1063,13 @@ export class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
protected processWerkvoorraad(response: Response): Promise<ApplicationSummaryDto[]> {
|
||||
protected processWerkvoorraad(response: Response): Promise<AanvraagSummaryDto[]> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ApplicationSummaryDto[];
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AanvraagSummaryDto[];
|
||||
return result200;
|
||||
});
|
||||
} else if (status === 403) {
|
||||
@@ -1083,7 +1083,7 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<ApplicationSummaryDto[]>(null as any);
|
||||
return Promise.resolve<AanvraagSummaryDto[]>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1898,22 +1898,7 @@ export interface AantekeningDto {
|
||||
datum?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AanvraagStatusDto {
|
||||
tag?: string | undefined;
|
||||
stepIndex?: number | undefined;
|
||||
stepCount?: number | undefined;
|
||||
referentie?: string | undefined;
|
||||
manual?: boolean | undefined;
|
||||
reden?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AdresDto {
|
||||
straat?: string | undefined;
|
||||
postcode?: string | undefined;
|
||||
woonplaats?: string | undefined;
|
||||
}
|
||||
|
||||
export interface ApplicationDetailDto {
|
||||
export interface AanvraagDetailDto {
|
||||
id?: string | undefined;
|
||||
type?: string | undefined;
|
||||
status?: AanvraagStatusDto;
|
||||
@@ -1924,7 +1909,29 @@ export interface ApplicationDetailDto {
|
||||
submittedAt?: string | undefined;
|
||||
}
|
||||
|
||||
export interface ApplicationSummaryDto {
|
||||
export interface AanvraagIndienenRequest {
|
||||
diplomaHerkomst?: string | undefined;
|
||||
uren?: number | undefined;
|
||||
documents?: DocumentRefDto[] | undefined;
|
||||
aanvullendeScholing?: boolean | undefined;
|
||||
scholingPunten?: number | undefined;
|
||||
}
|
||||
|
||||
export interface AanvraagIndienenResponse {
|
||||
referentie?: string | undefined;
|
||||
status?: AanvraagStatusDto;
|
||||
}
|
||||
|
||||
export interface AanvraagStatusDto {
|
||||
tag?: string | undefined;
|
||||
stepIndex?: number | undefined;
|
||||
stepCount?: number | undefined;
|
||||
referentie?: string | undefined;
|
||||
manual?: boolean | undefined;
|
||||
reden?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AanvraagSummaryDto {
|
||||
id?: string | undefined;
|
||||
type?: string | undefined;
|
||||
status?: AanvraagStatusDto;
|
||||
@@ -1935,6 +1942,12 @@ export interface ApplicationSummaryDto {
|
||||
owner?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AdresDto {
|
||||
straat?: string | undefined;
|
||||
postcode?: string | undefined;
|
||||
woonplaats?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AuthzAuditDto {
|
||||
at?: string | undefined;
|
||||
action?: string | undefined;
|
||||
@@ -1955,7 +1968,7 @@ export interface BeoordelingDocumentDto {
|
||||
}
|
||||
|
||||
export interface BeoordelingViewDto {
|
||||
aanvraag?: ApplicationSummaryDto;
|
||||
aanvraag?: AanvraagSummaryDto;
|
||||
documenten?: BeoordelingDocumentDto[] | undefined;
|
||||
decisions?: BeoordelingDecisionsDto;
|
||||
}
|
||||
@@ -2014,7 +2027,7 @@ export interface ChangeRequestRequest {
|
||||
telefoon?: string | undefined;
|
||||
}
|
||||
|
||||
export interface CreateApplicationRequest {
|
||||
export interface CreateAanvraagRequest {
|
||||
type?: string | undefined;
|
||||
}
|
||||
|
||||
@@ -2275,19 +2288,6 @@ export interface SubOrgSummaryDto {
|
||||
publishedVersion?: number;
|
||||
}
|
||||
|
||||
export interface SubmitApplicationRequest {
|
||||
diplomaHerkomst?: string | undefined;
|
||||
uren?: number | undefined;
|
||||
documents?: DocumentRefDto[] | undefined;
|
||||
aanvullendeScholing?: boolean | undefined;
|
||||
scholingPunten?: number | undefined;
|
||||
}
|
||||
|
||||
export interface SubmitApplicationResponse {
|
||||
referentie?: string | undefined;
|
||||
status?: AanvraagStatusDto;
|
||||
}
|
||||
|
||||
export interface UploadCategoriesDto {
|
||||
categories?: DocumentCategoryDto[] | undefined;
|
||||
}
|
||||
|
||||
@@ -11,3 +11,5 @@ export const loading = <E = never, T = never>(): RemoteData<E, T> => ({ tag: 'Lo
|
||||
export const success = <T, E = never>(value: T): RemoteData<E, T> => ({ tag: 'Success', value });
|
||||
|
||||
export const failure = <E, T = never>(error: E): RemoteData<E, T> => ({ tag: 'Failure', error });
|
||||
|
||||
export const empty = <E = never, T = never>(): RemoteData<E, T> => ({ tag: 'Empty' });
|
||||
|
||||
Reference in New Issue
Block a user