Upload index.html
Browse files- index.html +59 -10
index.html
CHANGED
|
@@ -206,7 +206,7 @@
|
|
| 206 |
</main>
|
| 207 |
</div>
|
| 208 |
<div id="reportOverlay" role="dialog" aria-modal="true" aria-label="Reakt Monitor rapport" aria-hidden="true">
|
| 209 |
-
<div class="report-actions"><span><strong>REAKT · Rapportvoorvertoning v1.
|
| 210 |
<section id="reportSheet" aria-label="Witte Inloop Monitor"></section>
|
| 211 |
</div>
|
| 212 |
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
|
@@ -259,7 +259,7 @@ function stepDescriptions(agent, result, fake){
|
|
| 259 |
if(agent==='insight')return fake?['Voorbeeldtrend gelezen','Locatieverschillen bekeken','Fictieve inzichten verzameld','Conceptduiding opgesteld','Overdracht aan verificatie']:[
|
| 260 |
'Maandgegevens beschikbaar','Locaties in grafiek gezet','Signalen zonder causaliteit','AI-duiding niet aangesloten','Overdracht aan controle'];
|
| 261 |
if(agent==='verify')return fake?['Demovergelijking geopend','5 indicatoren bekeken','1 fictieve afwijking','Rapport niet gevalideerd','Overdracht met waarschuwing']:[
|
| 262 |
-
'
|
| 263 |
return fake?['Concept gestart','Voorbeeldcijfers verwerkt','Illustratie bijgewerkt','Niet gevalideerd gemarkeerd','Concept gereed']:[
|
| 264 |
'Concept gestart','KPI-preview bijgewerkt','Instroom op eerste scan berekend','Niet gevalideerd gemarkeerd','Concept gereed'];
|
| 265 |
}
|
|
@@ -415,7 +415,7 @@ async function readExcelLocal(file){
|
|
| 415 |
passConflicts:passConflicts.size,identityConflicts:identityConflicts.size,
|
| 416 |
unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
|
| 417 |
}
|
| 418 |
-
// v1.
|
| 419 |
function quarterKey(date){return date.slice(0,4)+'-Q'+(Math.floor((Number(date.slice(5,7))-1)/3)+1);}
|
| 420 |
function quarterDates(key){
|
| 421 |
const m=String(key).match(/^(\d{4})-Q([1-4])$/);if(!m)throw Error('Kies een geldig rapportagekwartaal');
|
|
@@ -491,6 +491,52 @@ function calculateImported(site='*'){
|
|
| 491 |
passConflicts:importedData.passConflicts,identityConflicts:importedData.identityConflicts,
|
| 492 |
unmatchedPasses:importedData.unmatchedPasses,linkedByPass:importedData.linkedByPass,missingPass,passRows:importedData.passRows};
|
| 493 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
function drawImportedChart(result){
|
| 495 |
const cvs=$('chart'),ctx=cvs.getContext('2d'),w=cvs.width,h=cvs.height;
|
| 496 |
ctx.clearRect(0,0,w,h);ctx.font='13px Segoe UI';
|
|
@@ -515,6 +561,7 @@ function renderImported(result){
|
|
| 515 |
{label:'Nieuwe instroom',value:num(result.newIntake),change:'Eerste scan in '+result.period.label},
|
| 516 |
{label:'Actieve cliënten',value:num(result.active),change:'Laatste 3 maanden t/m peildatum'}];
|
| 517 |
renderKPIs({kpis});drawImportedChart(result);
|
|
|
|
| 518 |
const lines=[
|
| 519 |
['Geselecteerde periode',result.period.label+' ('+readableDate(result.period.begin)+' t/m '+readableDate(result.period.peildatum)+')'],
|
| 520 |
['Actieve cliënten: meetperiode',readableDate(result.period.activeFrom)+' t/m '+readableDate(result.peildatum)],
|
|
@@ -533,10 +580,12 @@ function renderImported(result){
|
|
| 533 |
['Tegenstrijdige pasregistraties',num(result.passConflicts)],
|
| 534 |
['Verschil scan/registratie',num(result.identityConflicts)],
|
| 535 |
['Businesscontroller','NIET AANGELEVERD']];
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
'
|
|
|
|
|
|
|
| 540 |
document.querySelector('.paper-watermark').textContent='FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD';
|
| 541 |
document.querySelector('.paper p').textContent=result.period.label+' · lokale Excel-proef · niet voor publicatie';
|
| 542 |
const pk=document.querySelectorAll('.paper-kpis strong');[num(result.visits),num(result.unique),num(result.newIntake)].forEach((x,i)=>{if(pk[i])pk[i].textContent=x});
|
|
@@ -549,18 +598,18 @@ async function startImported(){
|
|
| 549 |
$('conductor').classList.add('running');document.querySelector('.orchestra').classList.add('running');
|
| 550 |
$('conductorStatus').textContent='Excel-berekeningen worden gepresenteerd';$('logStatus').textContent='Actief';
|
| 551 |
for(const id of ids){let el=$('agent-'+id);el.classList.remove('active','done');el.querySelector('.status').textContent='STANDBY';el.querySelector('.track i').style.width='0%';el.querySelector('.percent').textContent='0%'}
|
| 552 |
-
const result=calculateImported($('siteSelect').value);importedData.last=result;
|
| 553 |
const steps=[
|
| 554 |
{agent:'data',text:`${result.rows.toLocaleString('nl-NL')} scanregels gecontroleerd; ${result.duplicates} herhaalde scans; Scanpassen: ${result.passSheetFound?result.passRows+' registraties':'geen apart werkblad'}`},
|
| 555 |
{agent:'analysis',text:`${result.unique} unieke cliënten ooit; ${result.newIntake} nieuwe instromers in ${result.period.label}; ${result.active} actieve cliënten`},
|
| 556 |
{agent:'insight',text:'Maandtrends berekend; AI-duiding nog niet verbonden'},
|
| 557 |
-
{agent:'verify',text:
|
| 558 |
{agent:'report',text:'Conceptpreview bijgewerkt · NIET GEVALIDEERD'}];
|
| 559 |
try{
|
| 560 |
renderImported(result);
|
| 561 |
for(const step of steps){const el=$('agent-'+step.agent);signalFor(step.agent);el.classList.add('active');el.querySelector('.status').textContent='ACTIEF';$('flowState').textContent=step.text;log('Start: '+el.querySelector('h3').textContent);
|
| 562 |
const stageLabels=stepDescriptions(step.agent,{...result,sites:importedData.sites.length},false);showStages(step.agent,stageLabels);await animateStages(step.agent,stageLabels);
|
| 563 |
-
clearSignal();el.classList.remove('active');el.classList.add('done');el.querySelector('.status').textContent=step.agent==='verify'?'
|
| 564 |
}
|
| 565 |
$('conductor').classList.add('attention');$('flowState').textContent='Concept · NIET GEVALIDEERD';$('conductorStatus').textContent='Proefberekening voltooid';$('logStatus').textContent='Afgerond';
|
| 566 |
}catch(err){log('FOUT: '+err.message);$('flowState').textContent='Proefberekening mislukt';}finally{clearSignal();running=false;$('startBtn').disabled=false;$('resetBtn').disabled=false;$('conductor').classList.remove('running');document.querySelector('.orchestra').classList.remove('running')}
|
|
|
|
| 206 |
</main>
|
| 207 |
</div>
|
| 208 |
<div id="reportOverlay" role="dialog" aria-modal="true" aria-label="Reakt Monitor rapport" aria-hidden="true">
|
| 209 |
+
<div class="report-actions"><span><strong>REAKT · Rapportvoorvertoning v1.1</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>
|
| 210 |
<section id="reportSheet" aria-label="Witte Inloop Monitor"></section>
|
| 211 |
</div>
|
| 212 |
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
|
|
|
| 259 |
if(agent==='insight')return fake?['Voorbeeldtrend gelezen','Locatieverschillen bekeken','Fictieve inzichten verzameld','Conceptduiding opgesteld','Overdracht aan verificatie']:[
|
| 260 |
'Maandgegevens beschikbaar','Locaties in grafiek gezet','Signalen zonder causaliteit','AI-duiding niet aangesloten','Overdracht aan controle'];
|
| 261 |
if(agent==='verify')return fake?['Demovergelijking geopend','5 indicatoren bekeken','1 fictieve afwijking','Rapport niet gevalideerd','Overdracht met waarschuwing']:[
|
| 262 |
+
'Tweede telroute vanaf bronregels',`${result?.verification?.checks.length??0} onafhankelijke vergelijkingen`,`${result?.verification?.mismatches??0} interne telverschillen`,`${result?.verification?.sourceIssues.length??0} soorten bronproblemen`,'Extern niet gevalideerd'];
|
| 263 |
return fake?['Concept gestart','Voorbeeldcijfers verwerkt','Illustratie bijgewerkt','Niet gevalideerd gemarkeerd','Concept gereed']:[
|
| 264 |
'Concept gestart','KPI-preview bijgewerkt','Instroom op eerste scan berekend','Niet gevalideerd gemarkeerd','Concept gereed'];
|
| 265 |
}
|
|
|
|
| 415 |
passConflicts:passConflicts.size,identityConflicts:identityConflicts.size,
|
| 416 |
unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
|
| 417 |
}
|
| 418 |
+
// v1.1 — rapportagekwartaal; voor historische definitie tellen alleen scans t/m peildatum.
|
| 419 |
function quarterKey(date){return date.slice(0,4)+'-Q'+(Math.floor((Number(date.slice(5,7))-1)/3)+1);}
|
| 420 |
function quarterDates(key){
|
| 421 |
const m=String(key).match(/^(\d{4})-Q([1-4])$/);if(!m)throw Error('Kies een geldig rapportagekwartaal');
|
|
|
|
| 491 |
passConflicts:importedData.passConflicts,identityConflicts:importedData.identityConflicts,
|
| 492 |
unmatchedPasses:importedData.unmatchedPasses,linkedByPass:importedData.linkedByPass,missingPass,passRows:importedData.passRows};
|
| 493 |
}
|
| 494 |
+
|
| 495 |
+
// v1.1: onafhankelijke controletelling. Geen hergebruik van de KPI-sets uit calculateImported.
|
| 496 |
+
function independentlyVerify(result){
|
| 497 |
+
const {begin,peildatum,activeFrom,months}=result.period;
|
| 498 |
+
const rows=importedData.all.filter(r=>(result.site==='*'||r.site===result.site)&&r.date<=peildatum);
|
| 499 |
+
const people=new Map(),visits=new Map(),monthFirst=new Map(months.map(m=>[m,0]));
|
| 500 |
+
for(const r of rows){
|
| 501 |
+
const key=JSON.stringify([r.site,r.id]);
|
| 502 |
+
let p=people.get(key);
|
| 503 |
+
if(!p){p={first:r.date,recent:false,inQuarter:false};people.set(key,p);}
|
| 504 |
+
if(r.date<p.first)p.first=r.date;
|
| 505 |
+
if(r.date>=activeFrom&&r.date<=peildatum)p.recent=true;
|
| 506 |
+
if(r.date>=begin&&r.date<=peildatum){
|
| 507 |
+
p.inQuarter=true;
|
| 508 |
+
const vkey=JSON.stringify([r.site,r.id,r.date]);
|
| 509 |
+
visits.set(vkey,(visits.get(vkey)||0)+1);
|
| 510 |
+
}
|
| 511 |
+
}
|
| 512 |
+
let intake=0,active=0,quarterClients=0;
|
| 513 |
+
for(const p of people.values()){
|
| 514 |
+
if(p.recent)active++;
|
| 515 |
+
if(p.inQuarter)quarterClients++;
|
| 516 |
+
if(p.first>=begin&&p.first<=peildatum){intake++;const m=p.first.slice(0,7);monthFirst.set(m,monthFirst.get(m)+1);}
|
| 517 |
+
}
|
| 518 |
+
const inQuarterRows=[...visits.values()].reduce((s,n)=>s+n,0);
|
| 519 |
+
const shadow={unique:people.size,newIntake:intake,active,visits:visits.size,quarterClients,
|
| 520 |
+
duplicates:inQuarterRows-visits.size,monthNew:[...monthFirst.values()].reduce((a,b)=>a+b,0)};
|
| 521 |
+
const checks=[
|
| 522 |
+
{label:'Unieke cliënten · tweede telling',ours:result.unique,shadow:shadow.unique},
|
| 523 |
+
{label:'Nieuwe instroom · eerste scan',ours:result.newIntake,shadow:shadow.newIntake},
|
| 524 |
+
{label:'Actieve cliënten · laatste 3 maanden',ours:result.active,shadow:shadow.active},
|
| 525 |
+
{label:'Unieke bezoekdagen · cliënt/locatie/dag',ours:result.visits,shadow:shadow.visits},
|
| 526 |
+
{label:'Bezoekers rapportagekwartaal',ours:result.quarterClients,shadow:shadow.quarterClients},
|
| 527 |
+
{label:'Herhaalde scans in kwartaal',ours:result.duplicates,shadow:shadow.duplicates},
|
| 528 |
+
{label:'Maandinstroom versus kwartaalinstroom',ours:result.newIntake,shadow:shadow.monthNew}
|
| 529 |
+
];
|
| 530 |
+
const mismatches=checks.filter(c=>c.ours!==c.shadow).length;
|
| 531 |
+
const sourceIssues=[
|
| 532 |
+
['Tegenstrijdige pasregistraties',result.passConflicts],
|
| 533 |
+
['Cliëntnummer wijkt af van pasregistratie',result.identityConflicts],
|
| 534 |
+
['Ongekoppelde scanpassen',result.unmatchedPasses],
|
| 535 |
+
['Ongeldige pasdatums',result.pasDates?.invalidDates||0]
|
| 536 |
+
].filter(([,n])=>n>0);
|
| 537 |
+
return {checks,mismatches,sourceIssues,passed:mismatches===0&&sourceIssues.length===0};
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
function drawImportedChart(result){
|
| 541 |
const cvs=$('chart'),ctx=cvs.getContext('2d'),w=cvs.width,h=cvs.height;
|
| 542 |
ctx.clearRect(0,0,w,h);ctx.font='13px Segoe UI';
|
|
|
|
| 561 |
{label:'Nieuwe instroom',value:num(result.newIntake),change:'Eerste scan in '+result.period.label},
|
| 562 |
{label:'Actieve cliënten',value:num(result.active),change:'Laatste 3 maanden t/m peildatum'}];
|
| 563 |
renderKPIs({kpis});drawImportedChart(result);
|
| 564 |
+
const quality=result.verification||independentlyVerify(result);
|
| 565 |
const lines=[
|
| 566 |
['Geselecteerde periode',result.period.label+' ('+readableDate(result.period.begin)+' t/m '+readableDate(result.period.peildatum)+')'],
|
| 567 |
['Actieve cliënten: meetperiode',readableDate(result.period.activeFrom)+' t/m '+readableDate(result.peildatum)],
|
|
|
|
| 580 |
['Tegenstrijdige pasregistraties',num(result.passConflicts)],
|
| 581 |
['Verschil scan/registratie',num(result.identityConflicts)],
|
| 582 |
['Businesscontroller','NIET AANGELEVERD']];
|
| 583 |
+
const verifyLines=quality.checks.map(c=>'<div class="check-row"><span>'+escapeHTML(c.label)+'</span><span>'+num(c.ours)+' / '+num(c.shadow)+' <strong class="'+(c.ours===c.shadow?'':'bad')+'">'+(c.ours===c.shadow?'✓ gelijk':'⚠ verschil')+'</strong></span></div>').join('');
|
| 584 |
+
$('checks').innerHTML='<div style="font-size:10px;color:#a8c9ee;margin:5px 0 11px">TWEEDE CONTROLETELLING · DASHBOARD / CONTROLE</div>'+verifyLines+'<div style="font-size:10px;color:#a8c9ee;margin:12px 0 9px">BRON- EN PERIODECONTROLES</div>'+lines.map(([name,val])=>'<div class="check-row"><span>'+escapeHTML(name)+'</span><span>'+escapeHTML(val)+'</span></div>').join('');
|
| 585 |
+
const issue=quality.mismatches||quality.sourceIssues.length;
|
| 586 |
+
document.querySelector('.verification .warning').textContent=quality.mismatches?`⚠ ${quality.mismatches} interne telverschillen gevonden. Cijfers nader onderzoeken. NIET GEVALIDEERD.`:
|
| 587 |
+
quality.sourceIssues.length?`⚠ Tweede telling sluit aan, maar ${quality.sourceIssues.length} soorten bronproblemen gevonden. NIET GEVALIDEERD.`:
|
| 588 |
+
'✓ 7 interne telcontroles sluiten aan. Let op: volledigheid van historische scans onbekend; externe businesscontrollercontrole ontbreekt. NIET EXTERN GEVALIDEERD.';
|
| 589 |
document.querySelector('.paper-watermark').textContent='FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD';
|
| 590 |
document.querySelector('.paper p').textContent=result.period.label+' · lokale Excel-proef · niet voor publicatie';
|
| 591 |
const pk=document.querySelectorAll('.paper-kpis strong');[num(result.visits),num(result.unique),num(result.newIntake)].forEach((x,i)=>{if(pk[i])pk[i].textContent=x});
|
|
|
|
| 598 |
$('conductor').classList.add('running');document.querySelector('.orchestra').classList.add('running');
|
| 599 |
$('conductorStatus').textContent='Excel-berekeningen worden gepresenteerd';$('logStatus').textContent='Actief';
|
| 600 |
for(const id of ids){let el=$('agent-'+id);el.classList.remove('active','done');el.querySelector('.status').textContent='STANDBY';el.querySelector('.track i').style.width='0%';el.querySelector('.percent').textContent='0%'}
|
| 601 |
+
const result=calculateImported($('siteSelect').value);result.verification=independentlyVerify(result);importedData.last=result;
|
| 602 |
const steps=[
|
| 603 |
{agent:'data',text:`${result.rows.toLocaleString('nl-NL')} scanregels gecontroleerd; ${result.duplicates} herhaalde scans; Scanpassen: ${result.passSheetFound?result.passRows+' registraties':'geen apart werkblad'}`},
|
| 604 |
{agent:'analysis',text:`${result.unique} unieke cliënten ooit; ${result.newIntake} nieuwe instromers in ${result.period.label}; ${result.active} actieve cliënten`},
|
| 605 |
{agent:'insight',text:'Maandtrends berekend; AI-duiding nog niet verbonden'},
|
| 606 |
+
{agent:'verify',text:`${result.verification.checks.length} onafhankelijke interne telcontroles; ${result.verification.mismatches} afwijkingen; ${result.verification.sourceIssues.length} soorten bronproblemen. Extern niet gevalideerd`},
|
| 607 |
{agent:'report',text:'Conceptpreview bijgewerkt · NIET GEVALIDEERD'}];
|
| 608 |
try{
|
| 609 |
renderImported(result);
|
| 610 |
for(const step of steps){const el=$('agent-'+step.agent);signalFor(step.agent);el.classList.add('active');el.querySelector('.status').textContent='ACTIEF';$('flowState').textContent=step.text;log('Start: '+el.querySelector('h3').textContent);
|
| 611 |
const stageLabels=stepDescriptions(step.agent,{...result,sites:importedData.sites.length},false);showStages(step.agent,stageLabels);await animateStages(step.agent,stageLabels);
|
| 612 |
+
clearSignal();el.classList.remove('active');el.classList.add('done');el.querySelector('.status').textContent=step.agent==='verify'?(result.verification.passed?'INTERN OK':'LET OP'):'GEREED';completed++;$('completed').textContent=String(completed);log(step.text);await sleep(200)
|
| 613 |
}
|
| 614 |
$('conductor').classList.add('attention');$('flowState').textContent='Concept · NIET GEVALIDEERD';$('conductorStatus').textContent='Proefberekening voltooid';$('logStatus').textContent='Afgerond';
|
| 615 |
}catch(err){log('FOUT: '+err.message);$('flowState').textContent='Proefberekening mislukt';}finally{clearSignal();running=false;$('startBtn').disabled=false;$('resetBtn').disabled=false;$('conductor').classList.remove('running');document.querySelector('.orchestra').classList.remove('running')}
|