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>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user