Marcel0123 commited on
Commit
eb3eee0
·
verified ·
1 Parent(s): f1b1060

Upload 2 files

Browse files
Files changed (2) hide show
  1. REAKT_TEST_PASDATUMS_FICTIEF.xlsx +0 -0
  2. 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.8.3</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>
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?'Scanpassen-werkblad verwerkt':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 nog niet bepaald','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,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
- return {all,sites,rawCount:rows.length-1,invalid,hasPassColumn:!!passColumn,
 
 
 
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?'⚠ Scanpassen gecontroleerd; definities en externe verificatie ontbreken. NIET GEVALIDEERD.':'⚠ Geen apart Scanpassen-werkblad; externe verificatie ontbreekt. NIET GEVALIDEERD.');
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 zijn nog niet vast te stellen uit deze proefberekening. De cijfers zijn niet onafhankelijk gevalideerd.`];
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:'Startdatum nog niet verwerkt; niet beschikbaar'},
539
- {klass:'purple',icon:'♙',label:'Actieve deelnemers',value:'—',sub:'Einddatum / actieve status niet vastgesteld'},
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>Startdatums ontbreken in deze proef</div></div></section>
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> &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>
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>