Marcel0123 commited on
Commit
522e438
·
verified ·
1 Parent(s): 5039b30

Upload index.html

Browse files
Files changed (1) hide show
  1. 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.0</strong> &nbsp; — 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,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
- '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'];
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.0 — 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,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
- $('checks').innerHTML=lines.map(([name,val])=>'<div class="check-row"><span>'+escapeHTML(name)+'</span><span>'+escapeHTML(val)+'</span></div>').join('');
537
- const issue=result.passConflicts||result.identityConflicts||result.unmatchedPasses||result.pasDates?.invalidDates;
538
- document.querySelector('.verification .warning').textContent=issue?'⚠ Ontbrekende of tegenstrijdige pas-koppelingen: controleer de brongegevens. Nieuwe instroom is alleen betrouwbaar bij complete scanhistorie. NIET GEVALIDEERD.':
539
- '⚠ Nieuwe instroom = eerste scan binnen de AANGELEVERDE historie; eerdere scans buiten het bestand kunnen ontbreken. Geen externe schaduwberekening. NIET GEVALIDEERD.';
 
 
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:'Interne telling gecontroleerd; geen externe schaduwberekening beschikbaar'},
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'?'NIET EXTERN':'GEREED';completed++;$('completed').textContent=String(completed);log(step.text);await sleep(200)
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> &nbsp; — 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)+' &nbsp;<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')}