From 6f250cd987a54817e25137145a8e637960cc8a54 Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Wed, 1 Jul 2026 11:41:57 +0200 Subject: [PATCH] Mijn aanvragen (C): FE contracts + applications adapter + parse boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Regenerate the NSwag client against the new backend endpoints (application + content methods, Aanvraag DTOs) — clears the API-client drift. - registratie/domain/aanvraag.ts: FE domain view — AanvraagType + AanvraagStatus discriminated union (illegal states unrepresentable) + Aanvraag/AanvraagDetail. Lives in registratie: the dashboard consumes it, downstream wizards produce it. - ApplicationsAdapter (infrastructure, the only new network surface): list resource + create/syncDraft/cancel/submit commands, with a hand-written parse* boundary (parseAanvraagStatus/parseApplicationSummary/parseApplications/parseApplicationDetail) mapping untrusted DTO -> domain, per ADR-0001. Spec covers each status tag + rejects. - UploadAdapter.contentUrl(id): direct href for preview/download (browser opens it). Gates green: dotnet test 56, vitest 122, lint, build. Co-Authored-By: Claude Opus 4.8 --- backend/swagger.json | 398 ++++++++++++++++++ src/app/registratie/domain/aanvraag.ts | 32 ++ .../applications.adapter.spec.ts | 47 +++ .../infrastructure/applications.adapter.ts | 115 +++++ src/app/shared/infrastructure/api-client.ts | 354 ++++++++++++++++ src/app/shared/upload/upload.adapter.ts | 9 + 6 files changed, 955 insertions(+) create mode 100644 src/app/registratie/domain/aanvraag.ts create mode 100644 src/app/registratie/infrastructure/applications.adapter.spec.ts create mode 100644 src/app/registratie/infrastructure/applications.adapter.ts diff --git a/backend/swagger.json b/backend/swagger.json index ed155cf..ff5f6b2 100644 --- a/backend/swagger.json +++ b/backend/swagger.json @@ -312,6 +312,31 @@ } } }, + "/api/v1/uploads/{documentId}/content": { + "get": { + "tags": [ + "BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + ], + "parameters": [ + { + "name": "documentId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + }, "/api/v1/uploads/status": { "get": { "tags": [ @@ -402,6 +427,205 @@ } } } + }, + "/api/v1/applications": { + "get": { + "tags": [ + "BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationSummaryDto" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApplicationRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationDetailDto" + } + } + } + } + } + } + }, + "/api/v1/applications/{id}": { + "get": { + "tags": [ + "BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationDetailDto" + } + } + } + }, + "404": { + "description": "Not Found" + } + } + }, + "put": { + "tags": [ + "BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DraftSyncRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found" + } + } + }, + "delete": { + "tags": [ + "BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "409": { + "description": "Conflict", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/v1/applications/{id}/submit": { + "post": { + "tags": [ + "BigRegister.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitApplicationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitApplicationResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found" + } + } + } } }, "components": { @@ -424,6 +648,38 @@ }, "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": { @@ -442,6 +698,81 @@ }, "additionalProperties": false }, + "ApplicationDetailDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/AanvraagStatusDto" + }, + "draft": { + "nullable": true + }, + "documentIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "createdAt": { + "type": "string", + "nullable": true + }, + "updatedAt": { + "type": "string", + "nullable": true + }, + "submittedAt": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "ApplicationSummaryDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/AanvraagStatusDto" + }, + "documentIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "createdAt": { + "type": "string", + "nullable": true + }, + "updatedAt": { + "type": "string", + "nullable": true + }, + "submittedAt": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "BrpAddressDto": { "type": "object", "properties": { @@ -472,6 +803,16 @@ }, "additionalProperties": false }, + "CreateApplicationRequest": { + "type": "object", + "properties": { + "type": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "DashboardViewDto": { "type": "object", "properties": { @@ -543,6 +884,28 @@ }, "additionalProperties": false }, + "DraftSyncRequest": { + "type": "object", + "properties": { + "draft": { }, + "stepIndex": { + "type": "integer", + "format": "int32" + }, + "stepCount": { + "type": "integer", + "format": "int32" + }, + "documentIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, "DuoDiplomaDto": { "type": "object", "properties": { @@ -806,6 +1169,41 @@ }, "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 + } + }, + "additionalProperties": false + }, + "SubmitApplicationResponse": { + "type": "object", + "properties": { + "referentie": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/AanvraagStatusDto" + } + }, + "additionalProperties": false + }, "UploadCategoriesDto": { "type": "object", "properties": { diff --git a/src/app/registratie/domain/aanvraag.ts b/src/app/registratie/domain/aanvraag.ts new file mode 100644 index 0000000..d42e5fa --- /dev/null +++ b/src/app/registratie/domain/aanvraag.ts @@ -0,0 +1,32 @@ +/** + * An application (aanvraag) as the frontend sees it — the parsed, domain-side view + * of the backend-owned aggregate (see backend ApplicationStore + PRD 0001). Pure + * types, no Angular. Lives in `registratie` because the dashboard (here) is the + * consumer and the downstream wizards (`herregistratie → registratie`) produce them. + * + * The status is a discriminated union so illegal states are unrepresentable — same + * reflex as RemoteData. The server computes which tag applies (auto-approval on + * read); the FE renders it, it does not recompute the lifecycle. + */ +export type AanvraagType = 'registratie' | 'herregistratie' | 'intake'; + +export type AanvraagStatus = + | { tag: 'Concept'; stepIndex: number; stepCount: number } + | { tag: 'InBehandeling'; referentie: string; manual: boolean } // manual=true → "wordt handmatig beoordeeld" + | { tag: 'Goedgekeurd'; referentie: string } + | { tag: 'Afgewezen'; referentie: string; reden: string }; + +export interface Aanvraag { + id: string; + type: AanvraagType; + status: AanvraagStatus; + documentIds: string[]; + createdAt: string; + updatedAt: string; + submittedAt?: string; +} + +/** Detail adds the opaque wizard snapshot used to resume a Concept. */ +export interface AanvraagDetail extends Aanvraag { + draft: unknown; +} diff --git a/src/app/registratie/infrastructure/applications.adapter.spec.ts b/src/app/registratie/infrastructure/applications.adapter.spec.ts new file mode 100644 index 0000000..fc07a1c --- /dev/null +++ b/src/app/registratie/infrastructure/applications.adapter.spec.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest'; +import { parseAanvraagStatus, parseApplicationSummary, parseApplications, parseApplicationDetail } from './applications.adapter'; + +const concept = { id: 'a1', type: 'registratie', status: { tag: 'Concept', stepIndex: 1, stepCount: 4 }, documentIds: [], createdAt: '2026-07-01T10:00:00Z', updatedAt: '2026-07-01T10:05:00Z' }; + +describe('parseAanvraagStatus', () => { + it('parses each tag with its required fields', () => { + expect(parseAanvraagStatus({ tag: 'Concept', stepIndex: 2, stepCount: 4 })).toEqual({ ok: true, value: { tag: 'Concept', stepIndex: 2, stepCount: 4 } }); + expect(parseAanvraagStatus({ tag: 'InBehandeling', referentie: 'BIG-1', manual: true }).ok).toBe(true); + expect(parseAanvraagStatus({ tag: 'Goedgekeurd', referentie: 'BIG-1' }).ok).toBe(true); + expect(parseAanvraagStatus({ tag: 'Afgewezen', referentie: 'BIG-1', reden: 'geen uren' }).ok).toBe(true); + }); + + it('rejects a missing status, unknown tag, and wrong-typed fields', () => { + expect(parseAanvraagStatus(undefined).ok).toBe(false); + expect(parseAanvraagStatus({ tag: 'Onzin' }).ok).toBe(false); + expect(parseAanvraagStatus({ tag: 'InBehandeling', referentie: 'BIG-1' }).ok).toBe(false); // manual missing + expect(parseAanvraagStatus({ tag: 'Concept', stepIndex: 1 }).ok).toBe(false); // stepCount missing + }); +}); + +describe('parseApplicationSummary', () => { + it('maps a valid DTO to domain', () => { + const r = parseApplicationSummary(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); + }); +}); + +describe('parseApplications / parseApplicationDetail', () => { + 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); + }); + + it('carries the opaque draft through detail', () => { + const r = parseApplicationDetail({ ...concept, draft: { beroep: 'arts' } }); + expect(r.ok && (r.value.draft as { beroep: string }).beroep).toBe('arts'); + }); +}); diff --git a/src/app/registratie/infrastructure/applications.adapter.ts b/src/app/registratie/infrastructure/applications.adapter.ts new file mode 100644 index 0000000..6fc0c9c --- /dev/null +++ b/src/app/registratie/infrastructure/applications.adapter.ts @@ -0,0 +1,115 @@ +import { Injectable, inject, resource } from '@angular/core'; +import { Result, ok, err } from '@shared/kernel/fp'; +import { + ApiClient, + AanvraagStatusDto, + ApplicationSummaryDto, + ApplicationDetailDto, + DraftSyncRequest, + SubmitApplicationRequest, + SubmitApplicationResponse, +} from '@shared/infrastructure/api-client'; +import { Aanvraag, AanvraagDetail, AanvraagStatus, AanvraagType } from '@registratie/domain/aanvraag'; + +/** + * 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 + * orchestrates optimistically. The untrusted response is validated + mapped to + * domain by the hand-written parse* boundary below. + */ +@Injectable({ providedIn: 'root' }) +export class ApplicationsAdapter { + private client = inject(ApiClient); + + /** The dashboard's application list (raw DTOs; the store parses at the boundary). */ + applicationsResource() { + return resource({ loader: () => this.client.applicationsAll() }); + } + + detail(id: string): Promise { + return this.client.applicationsGET(id); + } + + /** Create a Concept for a wizard type; resolves to the new aanvraag id. */ + create(type: AanvraagType): Promise { + return this.client.applicationsPOST({ 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 { + return this.client.applicationsPUT(id, body); + } + + /** Cancel a Concept (cascades to its unlinked documents server-side). */ + cancel(id: string): Promise { + return this.client.applicationsDELETE(id); + } + + submit(id: string, body: SubmitApplicationRequest): Promise { + return this.client.submit(id, body); + } +} + +const AANVRAAG_TYPES: readonly string[] = ['registratie', 'herregistratie', 'intake']; + +/** Trust-boundary parse of the status union — the tag drives which fields must exist. */ +export function parseAanvraagStatus(s: AanvraagStatusDto | undefined): Result { + if (!s || typeof s.tag !== 'string') return err('aanvraag: missing status'); + switch (s.tag) { + case 'Concept': + if (typeof s.stepIndex !== 'number' || typeof s.stepCount !== 'number') return err('aanvraag: bad Concept status'); + return ok({ tag: 'Concept', stepIndex: s.stepIndex, stepCount: s.stepCount }); + case 'InBehandeling': + if (typeof s.referentie !== 'string' || typeof s.manual !== 'boolean') return err('aanvraag: bad InBehandeling status'); + return ok({ tag: 'InBehandeling', referentie: s.referentie, manual: s.manual }); + case 'Goedgekeurd': + if (typeof s.referentie !== 'string') return err('aanvraag: bad Goedgekeurd status'); + return ok({ tag: 'Goedgekeurd', referentie: s.referentie }); + case 'Afgewezen': + if (typeof s.referentie !== 'string' || typeof s.reden !== 'string') return err('aanvraag: bad Afgewezen status'); + return ok({ tag: 'Afgewezen', referentie: s.referentie, reden: s.reden }); + default: + return err(`aanvraag: unknown status tag ${s.tag}`); + } +} + +function parseCommon(dto: ApplicationSummaryDto): Result { + 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}`); + if (typeof dto.createdAt !== 'string' || typeof dto.updatedAt !== 'string') return err('aanvraag: missing timestamps'); + const status = parseAanvraagStatus(dto.status); + if (!status.ok) return status; + return ok({ + id: dto.id, + type: dto.type as AanvraagType, + status: status.value, + documentIds: dto.documentIds ?? [], + createdAt: dto.createdAt, + updatedAt: dto.updatedAt, + submittedAt: dto.submittedAt, + }); +} + +export function parseApplicationSummary(json: unknown): Result { + if (typeof json !== 'object' || json === null) return err('aanvraag: not an object'); + return parseCommon(json as ApplicationSummaryDto); +} + +export function parseApplications(json: unknown): Result { + if (!Array.isArray(json)) return err('aanvragen: not an array'); + const out: Aanvraag[] = []; + for (const item of json) { + const parsed = parseApplicationSummary(item); + if (!parsed.ok) return parsed; + out.push(parsed.value); + } + return ok(out); +} + +export function parseApplicationDetail(json: unknown): Result { + if (typeof json !== 'object' || json === null) return err('aanvraag: not an object'); + const base = parseCommon(json as ApplicationDetailDto); + if (!base.ok) return base; + return ok({ ...base.value, draft: (json as ApplicationDetailDto).draft ?? null }); +} diff --git a/src/app/shared/infrastructure/api-client.ts b/src/app/shared/infrastructure/api-client.ts index 96af1ef..b3a703e 100644 --- a/src/app/shared/infrastructure/api-client.ts +++ b/src/app/shared/infrastructure/api-client.ts @@ -487,6 +487,46 @@ export class ApiClient { return Promise.resolve(null as any); } + /** + * @return OK + */ + content(documentId: string): Promise { + let url_ = this.baseUrl + "/api/v1/uploads/{documentId}/content"; + if (documentId === undefined || documentId === null) + throw new globalThis.Error("The parameter 'documentId' must be defined."); + url_ = url_.replace("{documentId}", encodeURIComponent("" + documentId)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processContent(_response); + }); + } + + protected processContent(response: Response): Promise { + 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) => { + return; + }); + } else if (status === 404) { + return response.text().then((_responseText) => { + return throwException("Not Found", status, _responseText, _headers); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + /** * @param localIds (optional) * @return OK @@ -617,6 +657,268 @@ export class ApiClient { } return Promise.resolve(null as any); } + + /** + * @return OK + */ + applicationsAll(): Promise { + let url_ = this.baseUrl + "/api/v1/applications"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processApplicationsAll(_response); + }); + } + + protected processApplicationsAll(response: Response): Promise { + 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[]; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * @return Created + */ + applicationsPOST(body: CreateApplicationRequest): Promise { + let url_ = this.baseUrl + "/api/v1/applications"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(body); + + let options_: RequestInit = { + body: content_, + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processApplicationsPOST(_response); + }); + } + + protected processApplicationsPOST(response: Response): Promise { + 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; + return result201; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * @return OK + */ + applicationsGET(id: string): Promise { + let url_ = this.baseUrl + "/api/v1/applications/{id}"; + if (id === undefined || id === null) + throw new globalThis.Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processApplicationsGET(_response); + }); + } + + protected processApplicationsGET(response: Response): Promise { + 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; + return result200; + }); + } else if (status === 404) { + return response.text().then((_responseText) => { + return throwException("Not Found", status, _responseText, _headers); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * @return No Content + */ + applicationsPUT(id: string, body: DraftSyncRequest): Promise { + let url_ = this.baseUrl + "/api/v1/applications/{id}"; + if (id === undefined || id === null) + throw new globalThis.Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(body); + + let options_: RequestInit = { + body: content_, + method: "PUT", + headers: { + "Content-Type": "application/json", + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processApplicationsPUT(_response); + }); + } + + protected processApplicationsPUT(response: Response): Promise { + 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) { + return response.text().then((_responseText) => { + return; + }); + } else if (status === 404) { + return response.text().then((_responseText) => { + return throwException("Not Found", status, _responseText, _headers); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * @return No Content + */ + applicationsDELETE(id: string): Promise { + let url_ = this.baseUrl + "/api/v1/applications/{id}"; + if (id === undefined || id === null) + throw new globalThis.Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "DELETE", + headers: { + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processApplicationsDELETE(_response); + }); + } + + protected processApplicationsDELETE(response: Response): Promise { + 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) { + return response.text().then((_responseText) => { + return; + }); + } else if (status === 404) { + return response.text().then((_responseText) => { + return throwException("Not Found", status, _responseText, _headers); + }); + } else if (status === 409) { + return response.text().then((_responseText) => { + let result409: any = null; + result409 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails; + return throwException("Conflict", status, _responseText, _headers, result409); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * @return OK + */ + submit(id: string, body: SubmitApplicationRequest): Promise { + let url_ = this.baseUrl + "/api/v1/applications/{id}/submit"; + if (id === undefined || id === null) + throw new globalThis.Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(body); + + let options_: RequestInit = { + body: content_, + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processSubmit(_response); + }); + } + + protected processSubmit(response: Response): Promise { + 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; + return result200; + }); + } else if (status === 404) { + return response.text().then((_responseText) => { + return throwException("Not Found", status, _responseText, _headers); + }); + } else if (status === 409) { + return response.text().then((_responseText) => { + let result409: any = null; + result409 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails; + return throwException("Conflict", status, _responseText, _headers, result409); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } } export interface AantekeningDto { @@ -625,12 +927,42 @@ 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 { + id?: string | undefined; + type?: string | undefined; + status?: AanvraagStatusDto; + draft?: any | undefined; + documentIds?: string[] | undefined; + createdAt?: string | undefined; + updatedAt?: string | undefined; + submittedAt?: string | undefined; +} + +export interface ApplicationSummaryDto { + id?: string | undefined; + type?: string | undefined; + status?: AanvraagStatusDto; + documentIds?: string[] | undefined; + createdAt?: string | undefined; + updatedAt?: string | undefined; + submittedAt?: string | undefined; +} + export interface BrpAddressDto { gevonden?: boolean; adres?: AdresDto; @@ -642,6 +974,10 @@ export interface ChangeRequestRequest { woonplaats?: string | undefined; } +export interface CreateApplicationRequest { + type?: string | undefined; +} + export interface DashboardViewDto { registration?: RegistrationDto; person?: PersonDto; @@ -665,6 +1001,13 @@ export interface DocumentRefDto { documentId?: string | undefined; } +export interface DraftSyncRequest { + draft?: any; + stepIndex?: number; + stepCount?: number; + documentIds?: string[] | undefined; +} + export interface DuoDiplomaDto { id?: string | undefined; naam?: string | undefined; @@ -750,6 +1093,17 @@ export interface RegistrationStatusDto { doorgehaaldOp?: string | undefined; } +export interface SubmitApplicationRequest { + diplomaHerkomst?: string | undefined; + uren?: number | undefined; + documents?: DocumentRefDto[] | undefined; +} + +export interface SubmitApplicationResponse { + referentie?: string | undefined; + status?: AanvraagStatusDto; +} + export interface UploadCategoriesDto { categories?: DocumentCategoryDto[] | undefined; } diff --git a/src/app/shared/upload/upload.adapter.ts b/src/app/shared/upload/upload.adapter.ts index 9946ceb..0c45ace 100644 --- a/src/app/shared/upload/upload.adapter.ts +++ b/src/app/shared/upload/upload.adapter.ts @@ -57,6 +57,15 @@ export class UploadAdapter { return this.client.uploads(documentId); } + /** + * Direct URL to the stored bytes, for a preview/download link (``) — the + * server serves it inline for pdf/image, attachment otherwise. Not a fetch: the + * browser opens it. demo-* ids (dev simulation) have no bytes and 404 here. + */ + contentUrl(documentId: string): string { + return `${environment.apiBaseUrl}/api/v1/uploads/${documentId}/content`; + } + /** * Multipart upload with progress + cancel. ponytail: XHR (not fetch keepalive) — * progress events need XHR; the keepalive/background gap is covered by