diff --git a/portal-frontend/src/app/app.config.ts b/portal-frontend/src/app/app.config.ts index cb1270e..0e86e19 100644 --- a/portal-frontend/src/app/app.config.ts +++ b/portal-frontend/src/app/app.config.ts @@ -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() ] }; diff --git a/portal-frontend/src/app/case-detail/case-detail.service.ts b/portal-frontend/src/app/case-detail/case-detail.service.ts new file mode 100644 index 0000000..ce4b2f5 --- /dev/null +++ b/portal-frontend/src/app/case-detail/case-detail.service.ts @@ -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 { + return this.http.get(`/api/worklist/legacy/${aanvraagId}`); + } + + getOwnedDetail(registrationApplicationId: string): Observable { + return this.http.get(`/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 { + return this.http.put(href, body); + } + + takeOwnership(href: string): Observable { + return this.http.post(href, null); + } + + releaseOwnership(href: string): Observable { + return this.http.delete(href); + } + + recordAssessment(href: string, body: RecordAssessmentRequest): Observable { + return this.http.post(href, body); + } +} diff --git a/portal-frontend/src/app/case-detail/case-detail.types.ts b/portal-frontend/src/app/case-detail/case-detail.types.ts new file mode 100644 index 0000000..970e54a --- /dev/null +++ b/portal-frontend/src/app/case-detail/case-detail.types.ts @@ -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; +} + +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; +} diff --git a/portal-frontend/src/app/shared/api-error.types.ts b/portal-frontend/src/app/shared/api-error.types.ts new file mode 100644 index 0000000..e2a0522 --- /dev/null +++ b/portal-frontend/src/app/shared/api-error.types.ts @@ -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; +} diff --git a/portal-frontend/src/app/worklist/worklist.service.ts b/portal-frontend/src/app/worklist/worklist.service.ts new file mode 100644 index 0000000..2ad4a1e --- /dev/null +++ b/portal-frontend/src/app/worklist/worklist.service.ts @@ -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 { + 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('/api/worklist', { params }); + } +} diff --git a/portal-frontend/src/app/worklist/worklist.types.ts b/portal-frontend/src/app/worklist/worklist.types.ts new file mode 100644 index 0000000..332c7da --- /dev/null +++ b/portal-frontend/src/app/worklist/worklist.types.ts @@ -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; +}