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:
@@ -1,4 +1,5 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
@@ -6,6 +7,7 @@ import { routes } from './app.routes';
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user