feat(portal-frontend): implement case-detail read view

Resolves legacy/:id vs owned/:id off the matched route's static path
segment and fetches accordingly. Renders applicant, diploma, and
assessment fields, a seams panel (which backend serves each section),
and embeds app-case-actions. Read-only - write forms land next.
This commit is contained in:
eho
2026-07-31 08:53:17 +02:00
parent 08e08a0070
commit cebbf9f57a
3 changed files with 160 additions and 13 deletions
@@ -1 +1,71 @@
<p>case-detail works!</p> @if (detail(); as d) {
<h2>{{ d.surname }}, {{ d.initials }} ({{ d.origin }})</h2>
<section>
<h3>Aanvrager</h3>
<dl>
<dt>BSN</dt>
<dd>{{ d.bsn }}</dd>
<dt>E-mail</dt>
<dd>{{ d.email ?? 'n.v.t.' }}</dd>
<dt>Telefoon</dt>
<dd>{{ d.phone ?? 'n.v.t.' }}</dd>
<dt>Voorkeurskanaal</dt>
<dd>{{ d.preferredChannel }}</dd>
@if (d.address; as address) {
<dt>Adres</dt>
<dd>{{ address.street }} {{ address.number }}, {{ address.postalCode }} {{ address.city }}</dd>
}
</dl>
</section>
<section>
<h3>Diploma</h3>
<dl>
<dt>Code</dt>
<dd>{{ d.diplomaCode }}</dd>
<dt>Land van uitgifte</dt>
<dd>{{ d.diplomaCountryOfIssue }}</dd>
<dt>Uitgegeven op</dt>
<dd>{{ d.diplomaIssuedOn }}</dd>
<dt>Ontvangen op</dt>
<dd>{{ d.receivedOn }}</dd>
<dt>Processtatus</dt>
<dd>{{ d.processStatus ?? 'n.v.t.' }}</dd>
</dl>
</section>
@if (d.assessment; as assessment) {
<section>
<h3>Beoordeling</h3>
<dl>
<dt>Uitkomst</dt>
<dd>{{ assessment.outcome }}</dd>
<dt>Motivatie</dt>
<dd>{{ assessment.motivation }}</dd>
<dt>Gecontroleerde stukken</dt>
<dd>{{ assessment.verifiedItems.join(', ') || 'geen' }}</dd>
<dt>Beslist op</dt>
<dd>{{ assessment.decidedOn }}</dd>
</dl>
</section>
}
<section>
<h3>Seams</h3>
<p class="seam-note">Toont welk backend elk onderdeel van deze pagina levert.</p>
<dl>
@for (seam of d.seams | keyvalue; track seam.key) {
<dt>{{ seam.key }}</dt>
<dd>{{ seam.value ?? 'n.v.t.' }}</dd>
}
</dl>
</section>
<section>
<h3>Acties</h3>
<app-case-actions [actions]="d.actions" />
</section>
} @else {
<p>Laden...</p>
}
@@ -1,22 +1,79 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { of } from 'rxjs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CaseDetailService } from '../case-detail.service';
import { CaseDetail } from '../case-detail.types';
import { CaseDetailPage } from './case-detail'; import { CaseDetailPage } from './case-detail';
const legacyDetail: CaseDetail = {
origin: 'Legacy',
legacyAanvraagId: 1001,
registrationApplicationId: null,
surname: 'de Vries',
initials: 'A.',
bsn: '123456782',
address: { street: 'Kerkweg', number: '12', postalCode: '3512JK', city: 'Utrecht' },
email: 'anna@example.nl',
phone: null,
preferredChannel: 'Post',
diplomaCode: 'X',
diplomaCountryOfIssue: 'NL',
diplomaIssuedOn: '2020-01-01',
receivedOn: '2026-01-01',
assessment: null,
processStatus: null,
lastModifiedAt: null,
actions: {
editApplicantDetails: { mode: 'writeThrough', href: '/api/worklist/legacy/1001/details' },
recordAssessment: { mode: 'redirect', href: '/legacy/aanvraag/1001/beoordeling' },
takeOwnership: { mode: 'transition', href: '/api/worklist/legacy/1001/take-ownership' },
},
seams: { aanvrager: 'legacy-backend', procestijdlijn: null },
};
function configure(routeConfigPath: string, paramMap: Record<string, string>, getLegacyDetail: unknown, getOwnedDetail: unknown) {
return TestBed.configureTestingModule({
imports: [CaseDetailPage],
providers: [
{ provide: CaseDetailService, useValue: { getLegacyDetail, getOwnedDetail } },
{
provide: ActivatedRoute,
useValue: {
paramMap: of(convertToParamMap(paramMap)),
snapshot: { routeConfig: { path: routeConfigPath } },
},
},
],
}).compileComponents();
}
describe('CaseDetailPage', () => { describe('CaseDetailPage', () => {
let component: CaseDetailPage;
let fixture: ComponentFixture<CaseDetailPage>; let fixture: ComponentFixture<CaseDetailPage>;
beforeEach(async () => { it('given a legacy/:id route, fetches via getLegacyDetail and renders the case', async () => {
await TestBed.configureTestingModule({ const getLegacyDetail = vi.fn().mockReturnValue(of(legacyDetail));
imports: [CaseDetailPage], const getOwnedDetail = vi.fn();
}).compileComponents(); await configure('legacy/:id', { id: '1001' }, getLegacyDetail, getOwnedDetail);
fixture = TestBed.createComponent(CaseDetailPage); fixture = TestBed.createComponent(CaseDetailPage);
component = fixture.componentInstance; fixture.detectChanges();
await fixture.whenStable();
expect(getLegacyDetail).toHaveBeenCalledWith('1001');
expect(getOwnedDetail).not.toHaveBeenCalled();
expect(fixture.nativeElement.textContent).toContain('de Vries');
}); });
it('should create', () => { it('given an owned/:id route, fetches via getOwnedDetail', async () => {
expect(component).toBeTruthy(); const getLegacyDetail = vi.fn();
const getOwnedDetail = vi.fn().mockReturnValue(of({ ...legacyDetail, origin: 'Owned', legacyAanvraagId: null, registrationApplicationId: 'abc' }));
await configure('owned/:id', { id: 'abc' }, getLegacyDetail, getOwnedDetail);
fixture = TestBed.createComponent(CaseDetailPage);
fixture.detectChanges();
expect(getOwnedDetail).toHaveBeenCalledWith('abc');
expect(getLegacyDetail).not.toHaveBeenCalled();
}); });
}); });
@@ -1,9 +1,29 @@
import { Component } from '@angular/core'; import { KeyValuePipe } from '@angular/common';
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CaseActions } from '../case-actions/case-actions';
import { CaseDetailService } from '../case-detail.service';
import { CaseDetail } from '../case-detail.types';
@Component({ @Component({
selector: 'app-case-detail', selector: 'app-case-detail',
imports: [], imports: [CaseActions, KeyValuePipe],
templateUrl: './case-detail.html', templateUrl: './case-detail.html',
styleUrl: './case-detail.css', styleUrl: './case-detail.css',
}) })
export class CaseDetailPage {} export class CaseDetailPage {
private readonly route = inject(ActivatedRoute);
private readonly caseDetailService = inject(CaseDetailService);
readonly detail = signal<CaseDetail | null>(null);
constructor() {
this.route.paramMap.subscribe((params) => {
const id = params.get('id')!;
const isLegacy = this.route.snapshot.routeConfig?.path?.startsWith('legacy') ?? false;
const source$ = isLegacy ? this.caseDetailService.getLegacyDetail(id) : this.caseDetailService.getOwnedDetail(id);
source$.subscribe((detail) => this.detail.set(detail));
});
}
}