feat(portal-frontend): implement worklist-list component

Table of WorklistService results with bucket/origin/search filters
(FormsModule ngModel); row click navigates to legacy/:id or owned/:id
based on origin, matching the placeholder's key scheme.
This commit is contained in:
eho
2026-07-31 08:50:56 +02:00
parent 97c0bea238
commit 08e08a0070
3 changed files with 164 additions and 11 deletions
@@ -1 +1,43 @@
<p>worklist-list works!</p>
<h2>Werkvoorraad</h2>
<div class="filters">
<label>
Bucket
<input type="text" name="bucket" [(ngModel)]="bucket" (ngModelChange)="onFilterChange()" />
</label>
<label>
Origin
<input type="text" name="origin" [(ngModel)]="origin" (ngModelChange)="onFilterChange()" />
</label>
<label>
Zoeken
<input type="text" name="search" [(ngModel)]="search" (ngModelChange)="onFilterChange()" />
</label>
</div>
<table>
<thead>
<tr>
<th>Origin</th>
<th>Referentie</th>
<th>Naam</th>
<th>BSN</th>
<th>Ontvangen</th>
<th>Uitkomst</th>
<th>Processtatus</th>
</tr>
</thead>
<tbody>
@for (item of items(); track keyOf(item).id) {
<tr data-testid="worklist-row" (click)="openDetail(item)">
<td>{{ item.origin }}</td>
<td>{{ keyOf(item).id }}</td>
<td>{{ item.surname }}, {{ item.initials }}</td>
<td>{{ item.bsn }}</td>
<td>{{ item.receivedOn }}</td>
<td>{{ item.assessmentOutcome ?? 'n.v.t.' }}</td>
<td>{{ item.processStatus ?? 'n.v.t.' }}</td>
</tr>
}
</tbody>
</table>
@@ -1,22 +1,91 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router, provideRouter } from '@angular/router';
import { of } from 'rxjs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { WorklistService } from '../worklist.service';
import { WorklistItem, WorklistPage, WorklistQuery } from '../worklist.types';
import { WorklistList } from './worklist-list';
const legacyItem: WorklistItem = {
origin: 'Legacy',
legacyAanvraagId: 1001,
registrationApplicationId: null,
surname: 'de Vries',
initials: 'A.',
bsn: '123456782',
receivedOn: '2026-01-01',
bucket: 'ToBeAssessed',
assessmentOutcome: null,
processStatus: null,
};
const ownedItem: WorklistItem = {
...legacyItem,
origin: 'Owned',
legacyAanvraagId: null,
registrationApplicationId: '00000000-0000-0000-0000-000000000002',
};
function pageOf(items: WorklistItem[]): WorklistPage {
return { items, page: 1, pageSize: 10, totalCount: items.length };
}
describe('WorklistList', () => {
let component: WorklistList;
let fixture: ComponentFixture<WorklistList>;
let getWorklist: ReturnType<typeof vi.fn>;
let router: Router;
function createComponent() {
fixture = TestBed.createComponent(WorklistList);
fixture.detectChanges();
router = TestBed.inject(Router);
}
beforeEach(async () => {
getWorklist = vi.fn().mockReturnValue(of(pageOf([legacyItem, ownedItem])));
await TestBed.configureTestingModule({
imports: [WorklistList],
providers: [provideRouter([]), { provide: WorklistService, useValue: { getWorklist } }],
}).compileComponents();
fixture = TestBed.createComponent(WorklistList);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
it('loads and renders the worklist on init', () => {
createComponent();
expect(getWorklist).toHaveBeenCalledWith({ bucket: undefined, origin: undefined, search: undefined });
const rows = fixture.nativeElement.querySelectorAll('[data-testid="worklist-row"]');
expect(rows.length).toBe(2);
});
it('given a legacy-origin row, navigates to legacy/:id on click', () => {
createComponent();
const navigate = vi.spyOn(router, 'navigate');
const rows = fixture.nativeElement.querySelectorAll('[data-testid="worklist-row"]');
rows[0].click();
expect(navigate).toHaveBeenCalledWith(['legacy', 1001]);
});
it('given an owned-origin row, navigates to owned/:id on click', () => {
createComponent();
const navigate = vi.spyOn(router, 'navigate');
const rows = fixture.nativeElement.querySelectorAll('[data-testid="worklist-row"]');
rows[1].click();
expect(navigate).toHaveBeenCalledWith(['owned', '00000000-0000-0000-0000-000000000002']);
});
it('reloads with the new query when a filter changes', () => {
createComponent();
getWorklist.mockClear();
fixture.componentInstance.bucket = 'ToBeAssessed';
fixture.componentInstance.onFilterChange();
expect(getWorklist).toHaveBeenCalledWith({ bucket: 'ToBeAssessed', origin: undefined, search: undefined } satisfies WorklistQuery);
});
});
@@ -1,9 +1,51 @@
import { Component } from '@angular/core';
import { Component, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { WorklistService } from '../worklist.service';
import { WorklistItem, WorklistQuery } from '../worklist.types';
@Component({
selector: 'app-worklist-list',
imports: [],
imports: [FormsModule],
templateUrl: './worklist-list.html',
styleUrl: './worklist-list.css',
})
export class WorklistList {}
export class WorklistList {
private readonly worklistService = inject(WorklistService);
private readonly router = inject(Router);
readonly items = signal<WorklistItem[]>([]);
bucket = '';
origin = '';
search = '';
constructor() {
this.load();
}
onFilterChange(): void {
this.load();
}
keyOf(item: WorklistItem): { segment: 'legacy' | 'owned'; id: number | string } {
return item.origin === 'Legacy'
? { segment: 'legacy', id: item.legacyAanvraagId! }
: { segment: 'owned', id: item.registrationApplicationId! };
}
openDetail(item: WorklistItem): void {
const { segment, id } = this.keyOf(item);
this.router.navigate([segment, id]);
}
private load(): void {
const query: WorklistQuery = {
bucket: this.bucket || undefined,
origin: this.origin || undefined,
search: this.search || undefined,
};
this.worklistService.getWorklist(query).subscribe((page) => this.items.set(page.items));
}
}