feat(portal-frontend): implement edit-applicant-details form with write-through disclaimer

Shared form for writeThrough and owned modes. Shows the ADR-002
disclaimer only in writeThrough mode (no client-side validation
stricter than legacy provides). Maps 400 ErrorsResponse to per-field
inline errors, 422/409 to a banner with the server's own text -
never a client-side switch over invariant names. Embedded in
case-detail with reload() on save so the actions block refreshes.
This commit is contained in:
eho
2026-07-31 08:59:55 +02:00
parent cebbf9f57a
commit 9e031da724
7 changed files with 317 additions and 2 deletions
@@ -0,0 +1,92 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ErrorsResponse, InvariantViolationResponse, MessageResponse } from '../../shared/api-error.types';
import { CaseDetailService } from '../case-detail.service';
import { ActionLink, ApplicantDetailsRequest } from '../case-detail.types';
@Component({
selector: 'app-edit-applicant-details',
imports: [FormsModule],
templateUrl: './edit-applicant-details.html',
styleUrl: './edit-applicant-details.css',
})
export class EditApplicantDetails implements OnInit {
private readonly caseDetailService = inject(CaseDetailService);
@Input({ required: true }) actionLink!: ActionLink;
@Input({ required: true }) initial!: ApplicantDetailsRequest;
@Output() readonly saved = new EventEmitter<void>();
surname = '';
initials = '';
street = '';
number = '';
postalCode = '';
city = '';
email = '';
phone = '';
preferredChannel = '';
fieldErrors: Record<string, string> = {};
banner: string | null = null;
get isWriteThrough(): boolean {
return this.actionLink.mode === 'writeThrough';
}
ngOnInit(): void {
this.surname = this.initial.surname;
this.initials = this.initial.initials;
this.street = this.initial.address?.street ?? '';
this.number = this.initial.address?.number ?? '';
this.postalCode = this.initial.address?.postalCode ?? '';
this.city = this.initial.address?.city ?? '';
this.email = this.initial.email ?? '';
this.phone = this.initial.phone ?? '';
this.preferredChannel = this.initial.preferredChannel;
}
submit(): void {
this.fieldErrors = {};
this.banner = null;
const body: ApplicantDetailsRequest = {
surname: this.surname,
initials: this.initials,
address: this.street ? { street: this.street, number: this.number, postalCode: this.postalCode, city: this.city } : null,
email: this.email || null,
phone: this.phone || null,
preferredChannel: this.preferredChannel,
};
this.caseDetailService.updateDetails(this.actionLink.href, body).subscribe({
next: () => this.saved.emit(),
error: (error: HttpErrorResponse) => this.handleError(error),
});
}
private handleError(error: HttpErrorResponse): void {
switch (error.status) {
case 400: {
const body = error.error as ErrorsResponse;
this.fieldErrors = Object.fromEntries(body.errors.map((fieldError) => [fieldError.field, fieldError.message]));
break;
}
case 422: {
const body = error.error as InvariantViolationResponse;
this.banner = `${body.invariant}: ${body.message}`;
break;
}
case 409: {
const body = error.error as MessageResponse;
this.banner = body.message;
break;
}
default:
this.banner = 'Onbekende fout bij opslaan.';
}
}
}