Upload 2 files
Browse files- REAKT_TEST_PASDATUMS_FICTIEF.xlsx +0 -0
- index.html +28 -12
REAKT_TEST_PASDATUMS_FICTIEF.xlsx
ADDED
|
Binary file (11.9 kB). View file
|
|
|
index.html
CHANGED
|
@@ -178,7 +178,7 @@
|
|
| 178 |
</main>
|
| 179 |
</div>
|
| 180 |
<div id="reportOverlay" role="dialog" aria-modal="true" aria-label="Reakt Monitor rapport" aria-hidden="true">
|
| 181 |
-
<div class="report-actions"><span><strong>REAKT · Rapportvoorvertoning v0.
|
| 182 |
<section id="reportSheet" aria-label="Witte Inloop Monitor"></section>
|
| 183 |
</div>
|
| 184 |
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
|
@@ -231,9 +231,9 @@ function stepDescriptions(agent, result, fake){
|
|
| 231 |
if(agent==='insight')return fake?['Voorbeeldtrend gelezen','Locatieverschillen bekeken','Fictieve inzichten verzameld','Conceptduiding opgesteld','Overdracht aan verificatie']:[
|
| 232 |
'Maandgegevens beschikbaar','Locaties in grafiek gezet','Signalen zonder causaliteit','AI-duiding niet aangesloten','Overdracht aan controle'];
|
| 233 |
if(agent==='verify')return fake?['Demovergelijking geopend','5 indicatoren bekeken','1 fictieve afwijking','Rapport niet gevalideerd','Overdracht met waarschuwing']:[
|
| 234 |
-
'Interne telling beschikbaar',result?.passSheetFound?'
|
| 235 |
return fake?['Concept gestart','Voorbeeldcijfers verwerkt','Illustratie bijgewerkt','Niet gevalideerd gemarkeerd','Concept gereed']:[
|
| 236 |
-
'Concept gestart','KPI-preview bijgewerkt','Instroom
|
| 237 |
}
|
| 238 |
function showStages(agent,steps){
|
| 239 |
const box=$('agent-'+agent);box.classList.add('has-steps');const wrap=box.querySelector('.agent-steps');wrap.replaceChildren();
|
|
@@ -339,15 +339,23 @@ async function readExcelLocal(file){
|
|
| 339 |
// Werkblad 2 bevat een onafhankelijke pas-cliëntregistratie. Nooit identificeren op naam.
|
| 340 |
const passXML=await xmlEntry(zip,'xl/worksheets/sheet2.xml');
|
| 341 |
const passToClient=new Map(),clientToPasses=new Map(),passConflicts=new Set();let passRows=0,passMissing=0;
|
|
|
|
|
|
|
| 342 |
if(passXML){
|
| 343 |
const pr=xmlDoc(passXML).getElementsByTagNameNS(excelNS,'row');
|
| 344 |
if(pr.length>20000)throw Error('Voor deze proef maximaal 20.000 scanpassen');
|
| 345 |
if(pr.length){
|
| 346 |
const ph=cellsToDict(pr[0],shared),pl={};for(const [col,v] of Object.entries(ph))pl[String(v).toLowerCase().trim()]=col;
|
| 347 |
-
const pc=pick(pl,'pasnummer','scanpasnummer','pas nummer','pasnr'),ic=pick(pl,'clientnummer','cliëntnummer');
|
|
|
|
| 348 |
if(!pc||!ic)throw Error('Scanpassen mist kolommen Pasnummer en Clientnummer');
|
| 349 |
for(let i=1;i<pr.length;i++){
|
| 350 |
const r=cellsToDict(pr[i],shared),p=String(r[pc]??'').trim(),c=String(r[ic]??'').trim();passRows++;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
if(!p||!c){passMissing++;continue}
|
| 352 |
if(passToClient.has(p)&&passToClient.get(p)!==c)passConflicts.add(p);
|
| 353 |
else passToClient.set(p,c);
|
|
@@ -371,7 +379,10 @@ async function readExcelLocal(file){
|
|
| 371 |
}
|
| 372 |
if(!all.length)throw Error('Geen bruikbare scanregels');
|
| 373 |
const sites=[...new Set(all.map(r=>r.site))].sort((a,b)=>a.localeCompare(b,'nl'));
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
| 375 |
passSheetFound:!!passXML,passRows,passMissing,passToClient,clientToPasses,
|
| 376 |
passConflicts:passConflicts.size,identityConflicts:identityConflicts.size,
|
| 377 |
unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
|
|
@@ -393,7 +404,7 @@ function calculateImported(site='*'){
|
|
| 393 |
const locations=(site==='*'?importedData.sites:[site]).map(name=>({name,visits:chosen.map(m=>monthSite.get(name+'|'+m)||0)}));
|
| 394 |
return {site,rows:all.length,visits:keyset.size,unique:ids.size,duplicates:all.length-keyset.size,months:chosen,locations,
|
| 395 |
invalid:importedData.invalid,passChecked:importedData.hasPassColumn||importedData.passSheetFound,
|
| 396 |
-
passSheetFound:importedData.passSheetFound,multiPassClients,passConflicts:importedData.passConflicts,
|
| 397 |
identityConflicts:importedData.identityConflicts,unmatchedPasses:importedData.unmatchedPasses,
|
| 398 |
linkedByPass:importedData.linkedByPass,missingPass,passRows:importedData.passRows};
|
| 399 |
}
|
|
@@ -425,12 +436,17 @@ function renderImported(result){
|
|
| 425 |
['Scanpassen: apart werkblad',result.passSheetFound?num(result.passRows)+' registraties':'NIET AANWEZIG'],
|
| 426 |
['Cliënten met meerdere passen',result.passChecked?num(result.multiPassClients):'NIET TE CONTROLEREN'],
|
| 427 |
['Scanregels via pas gekoppeld',num(result.linkedByPass)],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
['Niet-herkende passen',num(result.unmatchedPasses)],['Tegenstrijdige pasregistraties',num(result.passConflicts)],
|
| 429 |
['Verschil scan/registratie',num(result.identityConflicts)],['Businesscontroller','NIET AANGELEVERD']];
|
| 430 |
$('checks').innerHTML=lines.map(([name,val])=>'<div class="check-row"><span>'+escapeHTML(name)+'</span><span>'+escapeHTML(val)+'</span></div>').join('');
|
| 431 |
-
const issue=result.passConflicts||result.identityConflicts||result.unmatchedPasses;
|
| 432 |
document.querySelector('.verification .warning').textContent=issue?'⚠ Inconsistente of ontbrekende pas-koppelingen. Eerst onderzoeken. NIET GEVALIDEERD.':
|
| 433 |
-
(result.passSheetFound?'⚠
|
| 434 |
document.querySelector('.paper-watermark').textContent='FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD';
|
| 435 |
document.querySelector('.paper p').textContent='Lokale Excel-proef · niet voor publicatie';
|
| 436 |
const pk=document.querySelectorAll('.paper-kpis strong');[num(result.visits),num(result.unique),'—'].forEach((x,i)=>{if(pk[i])pk[i].textContent=x});
|
|
@@ -533,10 +549,10 @@ function buildWhiteReport(){
|
|
| 533 |
const summary=[`In de geselecteerde periode registreerde de proefmonitor ${reportNL(d.unique)} unieke cliëntnummers en ${reportNL(d.visits)} unieke bezoekdagen voor ${escapeHTML(d.site)}.`,
|
| 534 |
`De gemiddelde bezoekintensiteit bedraagt ${avg} bezoekdagen per cliëntnummer; de mediaan is ${String(d.median).replace('.',',')}.`,
|
| 535 |
`De meeste bezoekdagen vallen op ${busiest} (${reportNL(weekdayMax)}) en de minste op ${quietest} (${reportNL(weekdayMin)}).`,
|
| 536 |
-
`Nieuwe instroom en actieve deelnemers
|
| 537 |
const kpis=[{klass:'purple',icon:'♟',label:'Unieke bezoekers',value:reportNL(d.unique),sub:'Unieke cliëntnummers binnen de geselecteerde periode'},
|
| 538 |
-
{klass:'green',icon:'+',label:'Nieuwe instroom',value:'—',sub:'
|
| 539 |
-
{klass:'purple',icon:'♙',label:'Actieve deelnemers',value:'—',sub:'
|
| 540 |
{klass:'orange',icon:'▦',label:'Unieke bezoekdagen',value:reportNL(d.visits),sub:'Eén cliëntnummer per locatie per kalenderdag'}];
|
| 541 |
const kpiHTML=kpis.map(k=>`<div class="report-kpi ${k.klass}"><div class="k-icon">${k.icon}</div><div><div class="k-label">${k.label}</div><div class="k-num">${k.value}</div><div class="k-sub">${k.sub}</div></div><div class="k-underline"></div></div>`).join('');
|
| 542 |
const maxI=Math.max(1,...d.intensities),ilabels=['1 – 5 dagen','6 – 20 dagen','21 – 50 dagen','51 – 100 dagen','101 dagen of meer'];
|
|
@@ -547,7 +563,7 @@ function buildWhiteReport(){
|
|
| 547 |
<div class="report-meta"><div><b class="meta-icon">▦</b><span><strong>Periode</strong>${escapeHTML(range)}</span></div><div><b class="meta-icon">⌖</b><span><strong>Locatie</strong>${escapeHTML(d.site)}</span></div><div><b class="meta-icon">↻</b><span><strong>Peildatum</strong>${escapeHTML(d.latest)}</span></div></div></div>
|
| 548 |
<div class="report-kpi-grid">${kpiHTML}</div>
|
| 549 |
<div class="report-chart-grid">
|
| 550 |
-
<section class="report-panel"><h2>Instroom per maand <small>(nieuwe deelnemers)</small></h2><div class="chart-box"><div class="report-no-data">${missing}<br>
|
| 551 |
<section class="report-panel"><h2>Unieke bezoekdagen per maand</h2><div class="chart-box">${reportChartLine(mon,values)}</div></section>
|
| 552 |
<section class="report-panel"><h2>Bezoekdagen per weekdag</h2><div class="chart-box">${reportChartBars(week,d.byWeek,'#ff6410',true)}</div></section>
|
| 553 |
</div>
|
|
|
|
| 178 |
</main>
|
| 179 |
</div>
|
| 180 |
<div id="reportOverlay" role="dialog" aria-modal="true" aria-label="Reakt Monitor rapport" aria-hidden="true">
|
| 181 |
+
<div class="report-actions"><span><strong>REAKT · Rapportvoorvertoning v0.9</strong> — uitsluitend fictieve testgegevens</span><div><button id="reportClose" type="button">✕ Sluiten</button> <button id="reportPng" class="report-png" type="button">⬇ Download PNG</button> <button id="reportJpg" class="report-jpg" type="button">⬇ Download JPG</button> <button id="reportPrint" class="report-print" type="button">↓ Opslaan als PDF / Afdrukken</button></div></div>
|
| 182 |
<section id="reportSheet" aria-label="Witte Inloop Monitor"></section>
|
| 183 |
</div>
|
| 184 |
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
|
|
|
| 231 |
if(agent==='insight')return fake?['Voorbeeldtrend gelezen','Locatieverschillen bekeken','Fictieve inzichten verzameld','Conceptduiding opgesteld','Overdracht aan verificatie']:[
|
| 232 |
'Maandgegevens beschikbaar','Locaties in grafiek gezet','Signalen zonder causaliteit','AI-duiding niet aangesloten','Overdracht aan controle'];
|
| 233 |
if(agent==='verify')return fake?['Demovergelijking geopend','5 indicatoren bekeken','1 fictieve afwijking','Rapport niet gevalideerd','Overdracht met waarschuwing']:[
|
| 234 |
+
'Interne telling beschikbaar',result?.passSheetFound?'Pasregistraties en datums gelezen':result?.passChecked?'Paskolom bekeken':'Pascontrole niet mogelijk','Businesscontroller: geen bestand','Externe controle overgeslagen','Status: niet gevalideerd'];
|
| 235 |
return fake?['Concept gestart','Voorbeeldcijfers verwerkt','Illustratie bijgewerkt','Niet gevalideerd gemarkeerd','Concept gereed']:[
|
| 236 |
+
'Concept gestart','KPI-preview bijgewerkt','Instroom: pasdatum is geen bewijs','Niet gevalideerd gemarkeerd','Concept gereed'];
|
| 237 |
}
|
| 238 |
function showStages(agent,steps){
|
| 239 |
const box=$('agent-'+agent);box.classList.add('has-steps');const wrap=box.querySelector('.agent-steps');wrap.replaceChildren();
|
|
|
|
| 339 |
// Werkblad 2 bevat een onafhankelijke pas-cliëntregistratie. Nooit identificeren op naam.
|
| 340 |
const passXML=await xmlEntry(zip,'xl/worksheets/sheet2.xml');
|
| 341 |
const passToClient=new Map(),clientToPasses=new Map(),passConflicts=new Set();let passRows=0,passMissing=0;
|
| 342 |
+
const pasDates={startPresent:0,endPresent:0,migration:0,invalidDates:0,notAvailable:false,activeAtPeildatum:0};
|
| 343 |
+
const passDateRecords=[];
|
| 344 |
if(passXML){
|
| 345 |
const pr=xmlDoc(passXML).getElementsByTagNameNS(excelNS,'row');
|
| 346 |
if(pr.length>20000)throw Error('Voor deze proef maximaal 20.000 scanpassen');
|
| 347 |
if(pr.length){
|
| 348 |
const ph=cellsToDict(pr[0],shared),pl={};for(const [col,v] of Object.entries(ph))pl[String(v).toLowerCase().trim()]=col;
|
| 349 |
+
const pc=pick(pl,'pasnummer','scanpasnummer','pas nummer','pasnr'),ic=pick(pl,'clientnummer','cliëntnummer'),startCol=pick(pl,'startdatum'),endCol=pick(pl,'einddatum');
|
| 350 |
+
pasDates.notAvailable=!(startCol&&endCol);
|
| 351 |
if(!pc||!ic)throw Error('Scanpassen mist kolommen Pasnummer en Clientnummer');
|
| 352 |
for(let i=1;i<pr.length;i++){
|
| 353 |
const r=cellsToDict(pr[i],shared),p=String(r[pc]??'').trim(),c=String(r[ic]??'').trim();passRows++;
|
| 354 |
+
const start=startCol?scanDate(r[startCol]):'',end=endCol?scanDate(r[endCol]):'';
|
| 355 |
+
if(start)pasDates.startPresent++;if(end)pasDates.endPresent++;
|
| 356 |
+
if(start==='2022-10-28')pasDates.migration++;
|
| 357 |
+
if(start&&end&&end<start)pasDates.invalidDates++;
|
| 358 |
+
if(c&&p)passDateRecords.push({client:c,start,end});
|
| 359 |
if(!p||!c){passMissing++;continue}
|
| 360 |
if(passToClient.has(p)&&passToClient.get(p)!==c)passConflicts.add(p);
|
| 361 |
else passToClient.set(p,c);
|
|
|
|
| 379 |
}
|
| 380 |
if(!all.length)throw Error('Geen bruikbare scanregels');
|
| 381 |
const sites=[...new Set(all.map(r=>r.site))].sort((a,b)=>a.localeCompare(b,'nl'));
|
| 382 |
+
const peildatum=all.reduce((m,r)=>r.date>m?r.date:m,'');
|
| 383 |
+
const activeIds=new Set(passDateRecords.filter(r=>r.start&&r.end&&r.start<=peildatum&&r.end>=peildatum).map(r=>r.client));
|
| 384 |
+
pasDates.activeAtPeildatum=activeIds.size;
|
| 385 |
+
return {all,sites,peildatum,pasDates,rawCount:rows.length-1,invalid,hasPassColumn:!!passColumn,
|
| 386 |
passSheetFound:!!passXML,passRows,passMissing,passToClient,clientToPasses,
|
| 387 |
passConflicts:passConflicts.size,identityConflicts:identityConflicts.size,
|
| 388 |
unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
|
|
|
|
| 404 |
const locations=(site==='*'?importedData.sites:[site]).map(name=>({name,visits:chosen.map(m=>monthSite.get(name+'|'+m)||0)}));
|
| 405 |
return {site,rows:all.length,visits:keyset.size,unique:ids.size,duplicates:all.length-keyset.size,months:chosen,locations,
|
| 406 |
invalid:importedData.invalid,passChecked:importedData.hasPassColumn||importedData.passSheetFound,
|
| 407 |
+
passSheetFound:importedData.passSheetFound,pasDates:importedData.pasDates,peildatum:importedData.peildatum,multiPassClients,passConflicts:importedData.passConflicts,
|
| 408 |
identityConflicts:importedData.identityConflicts,unmatchedPasses:importedData.unmatchedPasses,
|
| 409 |
linkedByPass:importedData.linkedByPass,missingPass,passRows:importedData.passRows};
|
| 410 |
}
|
|
|
|
| 436 |
['Scanpassen: apart werkblad',result.passSheetFound?num(result.passRows)+' registraties':'NIET AANWEZIG'],
|
| 437 |
['Cliënten met meerdere passen',result.passChecked?num(result.multiPassClients):'NIET TE CONTROLEREN'],
|
| 438 |
['Scanregels via pas gekoppeld',num(result.linkedByPass)],
|
| 439 |
+
['Startdatums aanwezig (pasregistraties)',result.pasDates?.notAvailable?'NIET BESCHIKBAAR':num(result.pasDates?.startPresent??0)],
|
| 440 |
+
['Einddatums aanwezig (pasregistraties)',result.pasDates?.notAvailable?'NIET BESCHIKBAAR':num(result.pasDates?.endPresent??0)],
|
| 441 |
+
['Migratiedatum 28-10-2022 (passen)',num(result.pasDates?.migration??0)],
|
| 442 |
+
['Actieve pashouders op peildatum (alle locaties)',result.pasDates?.notAvailable?'NIET BESCHIKBAAR':num(result.pasDates?.activeAtPeildatum??0)],
|
| 443 |
+
['Ongeldige pasdatums (eind vóór start)',num(result.pasDates?.invalidDates??0)],
|
| 444 |
['Niet-herkende passen',num(result.unmatchedPasses)],['Tegenstrijdige pasregistraties',num(result.passConflicts)],
|
| 445 |
['Verschil scan/registratie',num(result.identityConflicts)],['Businesscontroller','NIET AANGELEVERD']];
|
| 446 |
$('checks').innerHTML=lines.map(([name,val])=>'<div class="check-row"><span>'+escapeHTML(name)+'</span><span>'+escapeHTML(val)+'</span></div>').join('');
|
| 447 |
+
const issue=result.passConflicts||result.identityConflicts||result.unmatchedPasses||result.pasDates?.invalidDates;
|
| 448 |
document.querySelector('.verification .warning').textContent=issue?'⚠ Inconsistente of ontbrekende pas-koppelingen. Eerst onderzoeken. NIET GEVALIDEERD.':
|
| 449 |
+
(result.passSheetFound?'⚠ Pasdatums zijn controle-informatie, geen bewezen instroom of actieve locatiebezoekers. Externe verificatie ontbreekt.':'⚠ Geen apart Scanpassen-werkblad; externe verificatie ontbreekt. NIET GEVALIDEERD.');
|
| 450 |
document.querySelector('.paper-watermark').textContent='FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD';
|
| 451 |
document.querySelector('.paper p').textContent='Lokale Excel-proef · niet voor publicatie';
|
| 452 |
const pk=document.querySelectorAll('.paper-kpis strong');[num(result.visits),num(result.unique),'—'].forEach((x,i)=>{if(pk[i])pk[i].textContent=x});
|
|
|
|
| 549 |
const summary=[`In de geselecteerde periode registreerde de proefmonitor ${reportNL(d.unique)} unieke cliëntnummers en ${reportNL(d.visits)} unieke bezoekdagen voor ${escapeHTML(d.site)}.`,
|
| 550 |
`De gemiddelde bezoekintensiteit bedraagt ${avg} bezoekdagen per cliëntnummer; de mediaan is ${String(d.median).replace('.',',')}.`,
|
| 551 |
`De meeste bezoekdagen vallen op ${busiest} (${reportNL(weekdayMax)}) en de minste op ${quietest} (${reportNL(weekdayMin)}).`,
|
| 552 |
+
`Nieuwe instroom en actieve deelnemers per locatie zijn niet vastgesteld. Pas-startdatums kunnen ook uitgifte- of migratiedatums zijn. De cijfers zijn niet onafhankelijk gevalideerd.`];
|
| 553 |
const kpis=[{klass:'purple',icon:'♟',label:'Unieke bezoekers',value:reportNL(d.unique),sub:'Unieke cliëntnummers binnen de geselecteerde periode'},
|
| 554 |
+
{klass:'green',icon:'+',label:'Nieuwe instroom',value:'—',sub:'Pas-startdatum bewijst geen instroom'},
|
| 555 |
+
{klass:'purple',icon:'♙',label:'Actieve deelnemers',value:'—',sub:'Pashouder is niet per se actieve locatiebezoeker'},
|
| 556 |
{klass:'orange',icon:'▦',label:'Unieke bezoekdagen',value:reportNL(d.visits),sub:'Eén cliëntnummer per locatie per kalenderdag'}];
|
| 557 |
const kpiHTML=kpis.map(k=>`<div class="report-kpi ${k.klass}"><div class="k-icon">${k.icon}</div><div><div class="k-label">${k.label}</div><div class="k-num">${k.value}</div><div class="k-sub">${k.sub}</div></div><div class="k-underline"></div></div>`).join('');
|
| 558 |
const maxI=Math.max(1,...d.intensities),ilabels=['1 – 5 dagen','6 – 20 dagen','21 – 50 dagen','51 – 100 dagen','101 dagen of meer'];
|
|
|
|
| 563 |
<div class="report-meta"><div><b class="meta-icon">▦</b><span><strong>Periode</strong>${escapeHTML(range)}</span></div><div><b class="meta-icon">⌖</b><span><strong>Locatie</strong>${escapeHTML(d.site)}</span></div><div><b class="meta-icon">↻</b><span><strong>Peildatum</strong>${escapeHTML(d.latest)}</span></div></div></div>
|
| 564 |
<div class="report-kpi-grid">${kpiHTML}</div>
|
| 565 |
<div class="report-chart-grid">
|
| 566 |
+
<section class="report-panel"><h2>Instroom per maand <small>(nieuwe deelnemers)</small></h2><div class="chart-box"><div class="report-no-data">${missing}<br>Instroomdefinitie moet eerst worden vastgesteld</div></div></section>
|
| 567 |
<section class="report-panel"><h2>Unieke bezoekdagen per maand</h2><div class="chart-box">${reportChartLine(mon,values)}</div></section>
|
| 568 |
<section class="report-panel"><h2>Bezoekdagen per weekdag</h2><div class="chart-box">${reportChartBars(week,d.byWeek,'#ff6410',true)}</div></section>
|
| 569 |
</div>
|