feat(portal-frontend): add worklist and case-detail typed interfaces and API services

Types mirror New.Api.Contracts verbatim; all write calls use the href
given in a case's actions block rather than a client-built URL.
This commit is contained in:
eho
2026-07-31 08:45:27 +02:00
parent 229ca9b7d9
commit b8a8d28e7b
6 changed files with 194 additions and 1 deletions
+3 -1
View File
@@ -1,4 +1,5 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
@@ -6,6 +7,7 @@ import { routes } from './app.routes';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
provideRouter(routes) provideRouter(routes),
provideHttpClient()
] ]
}; };
@@ -0,0 +1,43 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import {
ApplicantDetailsRequest,
CaseDetail,
RecordAssessmentRequest,
RecordAssessmentResult,
TakeOwnershipResult,
} from './case-detail.types';
@Injectable({ providedIn: 'root' })
export class CaseDetailService {
private readonly http = inject(HttpClient);
getLegacyDetail(aanvraagId: string): Observable<CaseDetail> {
return this.http.get<CaseDetail>(`/api/worklist/legacy/${aanvraagId}`);
}
getOwnedDetail(registrationApplicationId: string): Observable<CaseDetail> {
return this.http.get<CaseDetail>(`/api/worklist/owned/${registrationApplicationId}`);
}
// Every write below is issued against an `href` taken directly from the
// detail response's `actions` block, never a URL built from the id -
// that's the point of the HATEOAS-style contract (see ADR-002/003).
updateDetails(href: string, body: ApplicantDetailsRequest): Observable<void> {
return this.http.put<void>(href, body);
}
takeOwnership(href: string): Observable<TakeOwnershipResult> {
return this.http.post<TakeOwnershipResult>(href, null);
}
releaseOwnership(href: string): Observable<void> {
return this.http.delete<void>(href);
}
recordAssessment(href: string, body: RecordAssessmentRequest): Observable<RecordAssessmentResult> {
return this.http.post<RecordAssessmentResult>(href, body);
}
}
@@ -0,0 +1,78 @@
// Mirrors New.Api.Contracts.CaseDetailResponse and its request contracts
// (ApplicantDetailsRequest, RecordAssessmentRequest) verbatim. `actions`
// links (href) are always used as-is, never rebuilt client-side.
export interface ActionLink {
mode: string;
href: string;
}
export interface CaseDetailActions {
editApplicantDetails: ActionLink;
recordAssessment: ActionLink;
takeOwnership?: ActionLink;
releaseOwnership?: ActionLink;
}
export interface Address {
street: string;
number: string;
postalCode: string;
city: string;
}
export interface Assessment {
outcome: string;
motivation: string;
verifiedItems: string[];
exceptionReason: string | null;
rejectionCategory: string | null;
decidedOn: string;
}
export interface CaseDetail {
origin: 'Legacy' | 'Owned';
legacyAanvraagId: number | null;
registrationApplicationId: string | null;
surname: string;
initials: string;
bsn: string;
address: Address | null;
email: string | null;
phone: string | null;
preferredChannel: string;
diplomaCode: string;
diplomaCountryOfIssue: string;
diplomaIssuedOn: string;
receivedOn: string;
assessment: Assessment | null;
processStatus: string | null;
lastModifiedAt: string | null;
actions: CaseDetailActions;
seams: Record<string, string | null>;
}
export interface ApplicantDetailsRequest {
surname: string;
initials: string;
address: Address | null;
email: string | null;
phone: string | null;
preferredChannel: string;
}
export interface RecordAssessmentRequest {
verifiedItems: string[];
exceptionReason: string | null;
outcome: 'Approved' | 'Rejected';
rejectionCategory: string | null;
motivation: string;
}
export interface RecordAssessmentResult {
closurePending: boolean;
}
export interface TakeOwnershipResult {
registrationApplicationId: string;
}
@@ -0,0 +1,25 @@
// Mirrors New.Api.Contracts error shapes verbatim.
export interface FieldError {
field: string;
message: string;
detail?: string | null;
}
/** 400 - legacy write-through validation failures only (seam B). */
export interface ErrorsResponse {
errors: FieldError[];
}
/** 422 - a single named domain-invariant failure. Render `invariant` and
* `message` as given; the set of invariants isn't fixed client-side
* knowledge (ADR-003), so no switch/enum maps over it. */
export interface InvariantViolationResponse {
invariant: string;
message: string;
}
/** 409 - a conflict, e.g. release-ownership blocked by prior domain writes. */
export interface MessageResponse {
message: string;
}
@@ -0,0 +1,18 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { WorklistPage, WorklistQuery } from './worklist.types';
@Injectable({ providedIn: 'root' })
export class WorklistService {
private readonly http = inject(HttpClient);
getWorklist(query: WorklistQuery = {}): Observable<WorklistPage> {
let params = new HttpParams();
if (query.bucket) params = params.set('bucket', query.bucket);
if (query.origin) params = params.set('origin', query.origin);
if (query.search) params = params.set('search', query.search);
return this.http.get<WorklistPage>('/api/worklist', { params });
}
}
@@ -0,0 +1,27 @@
// Mirrors New.Api.Contracts.WorklistItemResponse / WorklistPageResponse verbatim.
export interface WorklistItem {
origin: 'Legacy' | 'Owned';
legacyAanvraagId: number | null;
registrationApplicationId: string | null;
surname: string;
initials: string;
bsn: string;
receivedOn: string;
bucket: string;
assessmentOutcome: string | null;
processStatus: string | null;
}
export interface WorklistPage {
items: WorklistItem[];
page: number;
pageSize: number;
totalCount: number;
}
export interface WorklistQuery {
bucket?: string;
origin?: string;
search?: string;
}