Files
ehoandClaude Sonnet 5 a6a1abbe9c feat: implement strangler-fig-demo Session 1 (backend + smoke script)
Builds the four-seam, three-write-path reference demo backend: case-framework
(seam D stand-in), legacy-backend/frontend (SQL Server, seams A/B/C targets),
and new-backend (Domain/Application/Infrastructure.*/Api implementing the
source resolver, take/release-ownership, write-through translator, and owned
assessment flow), wired together via docker-compose with a plain placeholder
frontend standing in for the Angular portal until Session 2.

All 11 Architecture.Tests pass and scripts/smoke.sh passes end-to-end against
a fresh `docker compose up`, covering acceptance criteria 1-3 and 7-22.

Fixes two real domain bugs found only once the stack ran for real: the BSN
eleven-proof checksum trivially passes all-zero digits, and the adoption
mapper silently treated a partial legacy address as absent instead of failing
loudly. Also fixes several environment-specific integration issues (rootless
Podman/SELinux bind-mount permissions, a buildah NuGet layer-caching bug,
SqlClient's invariant-globalization incompatibility, and an nginx path-prefix
mismatch for the legacy frontend).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 07:57:26 +02:00

158 lines
6.3 KiB
HTML

<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<title>Behandel portaal (placeholder)</title>
</head>
<body>
<h1>Behandel portaal — werkvoorraad (placeholder, session 2 replaces this with Angular)</h1>
<p>
<label>Bucket: <select id="bucket"><option value="">Alles</option><option value="Open">Te beoordelen</option><option value="Beoordeeld">Beoordeeld</option><option value="Ingetrokken">Ingetrokken</option></select></label>
<label>Origin: <select id="origin"><option value="">alles</option><option value="Legacy">legacy</option><option value="Owned">nieuw proces</option></select></label>
<label>Zoek: <input id="search" type="text"></label>
<button onclick="loadWorklist()">Ververs</button>
</p>
<table border="1" cellpadding="4">
<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 id="rows"></tbody>
</table>
<h2>Detail</h2>
<pre id="detail">Kies een rij (klik erop) om details te laden.</pre>
<div id="actions"></div>
<script>
let currentKey = null;
async function loadWorklist() {
const bucket = document.getElementById('bucket').value;
const origin = document.getElementById('origin').value;
const search = document.getElementById('search').value;
const params = new URLSearchParams();
if (bucket) params.set('bucket', bucket);
if (origin) params.set('origin', origin);
if (search) params.set('search', search);
const res = await fetch('/api/worklist?' + params.toString());
const data = await res.json();
const rows = document.getElementById('rows');
rows.innerHTML = '';
for (const item of data.items) {
const tr = document.createElement('tr');
const key = item.origin === 'Legacy' ? `legacy/${item.legacyAanvraagId}` : `owned/${item.registrationApplicationId}`;
tr.innerHTML = `<td>${item.origin}</td><td>${item.legacyAanvraagId ?? item.registrationApplicationId}</td>` +
`<td>${item.surname} ${item.initials}</td><td>${item.bsn}</td><td>${item.receivedOn}</td>` +
`<td>${item.assessmentOutcome ?? ''}</td><td>${item.processStatus ?? 'n.v.t.'}</td>`;
tr.style.cursor = 'pointer';
tr.onclick = () => loadDetail(key);
rows.appendChild(tr);
}
}
async function loadDetail(key) {
currentKey = key;
const res = await fetch(`/api/worklist/${key}`);
if (!res.ok) {
document.getElementById('detail').textContent = `Fout: ${res.status}`;
document.getElementById('actions').innerHTML = '';
return;
}
const detail = await res.json();
document.getElementById('detail').textContent = JSON.stringify(detail, null, 2);
renderActions(detail);
}
function renderActions(detail) {
const el = document.getElementById('actions');
el.innerHTML = '';
const a = detail.actions;
addButton(el, `Gegevens wijzigen (${a.editApplicantDetails.mode})`, async () => {
const surname = prompt('Surname', detail.surname);
if (surname === null) return;
const initials = prompt('Initials', detail.initials);
const street = prompt('Street (leeg = geen adres)', detail.address?.street ?? '');
const number = street ? prompt('Number', detail.address?.number ?? '') : null;
const postalCode = street ? prompt('Postal code', detail.address?.postalCode ?? '') : null;
const city = street ? prompt('City', detail.address?.city ?? '') : null;
const email = prompt('Email', detail.email ?? '');
const phone = prompt('Phone', detail.phone ?? '');
const preferredChannel = prompt('Preferred channel (Post/Email)', detail.preferredChannel);
const body = {
surname, initials,
address: street ? { street, number, postalCode, city } : null,
email, phone, preferredChannel,
};
const res = await fetch(a.editApplicantDetails.href, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const text = await res.text();
alert(`${res.status}: ${text}`);
loadDetail(currentKey);
});
if (a.recordAssessment.mode === 'redirect') {
addLink(el, 'Beoordeling vastleggen (verlaat portaal — legacy valideert)', a.recordAssessment.href);
} else {
addButton(el, 'Beoordeling vastleggen', async () => {
const verified = prompt('Verified items (comma separated: document,land,datum)', 'document,land,datum');
const exceptionReason = verified ? null : prompt('Exception reason (verplicht als geen items geverifieerd)');
const outcome = prompt('Outcome (Approved/Rejected)', 'Approved');
const rejectionCategory = outcome === 'Rejected' ? prompt('Rejection category (onvolledig/niet erkend/niet bevoegd/anders)') : null;
const motivation = prompt('Motivation (min 20 chars, 50 if "anders")');
const body = {
verifiedItems: verified ? verified.split(',').map(s => s.trim()) : [],
exceptionReason, outcome, rejectionCategory, motivation,
};
const res = await fetch(a.recordAssessment.href, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const text = await res.text();
alert(`${res.status}: ${text}`);
loadDetail(currentKey);
});
}
if (a.takeOwnership) {
addButton(el, 'Overnemen in nieuw systeem', async () => {
if (!confirm('Dit maakt het nieuwe systeem eigenaar van deze zaak. Doorgaan?')) return;
const res = await fetch(a.takeOwnership.href, { method: 'POST' });
const text = await res.text();
alert(`${res.status}: ${text}`);
loadWorklist();
if (res.ok) {
const body = JSON.parse(text);
loadDetail(`owned/${body.registrationApplicationId}`);
}
});
}
if (a.releaseOwnership) {
addButton(el, 'Overname terugdraaien', async () => {
const res = await fetch(a.releaseOwnership.href, { method: 'DELETE' });
const text = await res.text();
alert(`${res.status}: ${text}`);
loadWorklist();
});
}
}
function addButton(container, label, onClick) {
const btn = document.createElement('button');
btn.textContent = label;
btn.onclick = onClick;
container.appendChild(btn);
container.appendChild(document.createElement('br'));
}
function addLink(container, label, href) {
const a = document.createElement('a');
a.textContent = label;
a.href = href;
container.appendChild(a);
container.appendChild(document.createElement('br'));
}
loadWorklist();
</script>
</body>
</html>