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

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +52 -16
index.html CHANGED
@@ -140,6 +140,24 @@
140
  #reportSheet .report-analysis p { margin-bottom:5px!important; }
141
  #reportSheet .report-foot { margin-top:6px!important; padding-top:5px!important; font-size:8px!important; }
142
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  </style></head>
144
  <body>
145
  <div class="app">
@@ -167,6 +185,15 @@
167
  <div class="content-grid">
168
  <div class="left-content">
169
  <div class="kpi-grid" id="kpis"></div>
 
 
 
 
 
 
 
 
 
170
  <div class="charts-grid"><section class="panel chart-panel"><div class="panel-heading"><div><span class="section-kicker">TRENDANALYSE</span><h3>Bezoeken per locatie</h3></div><span class="subtle">Jul – Sep 2026</span></div><canvas id="chart" width="640" height="250" aria-label="Grafiek fictieve bezoeken per locatie"></canvas><div class="legend"><span><i style="background:#3ee1b6"></i> Soeda</span><span><i style="background:#ffc66a"></i> Rietdijk</span><span><i style="background:#5b9cff"></i> Vredehof</span><span><i style="background:#ad88ff"></i> Gouda</span></div></section>
171
  <section class="panel verification"><div class="panel-heading"><div><span class="section-kicker">KWALITEITSBORGING</span><h3>Verificatie Businesscontroller</h3></div><span class="warn-label">DEMO</span></div><div id="checks" class="checks"></div><div class="warning">⚠ 1 fictieve afwijking. Rapport niet gevalideerd.</div></section></div>
172
  <section class="panel logpanel"><div class="panel-heading"><div><span class="section-kicker">REAL-TIME PROCESS</span><h3>Agent activity log</h3></div><span class="subtle" id="logStatus">Stand-by</span></div><div class="logs" id="logs"><div class="logitem muted">◌ &nbsp; Klik op ‘Start demo-analyse’ om de workflow te bekijken.</div></div></section>
@@ -178,7 +205,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.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>
@@ -388,13 +415,13 @@ async function readExcelLocal(file){
388
  unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
389
  }
390
  function calculateImported(site='*'){
391
- const all=importedData.all.filter(r=>site==='*'||r.site===site),keyset=new Set(),ids=new Set(),monthSite=new Map();
392
  const passByClient=new Map();let missingPass=0;
393
  for(const r of all){
394
  if(r.pass){if(!passByClient.has(r.id))passByClient.set(r.id,new Set());passByClient.get(r.id).add(r.pass)}else missingPass++;
395
  const k=r.site+'|'+r.date+'|'+r.id;
396
  if(keyset.has(k))continue;
397
- keyset.add(k);ids.add(r.id);
398
  const mk=r.site+'|'+r.date.slice(0,7);monthSite.set(mk,(monthSite.get(mk)||0)+1);
399
  }
400
  let multiPassClients=0;
@@ -402,7 +429,7 @@ function calculateImported(site='*'){
402
  else multiPassClients=[...passByClient.values()].filter(p=>p.size>1).length;
403
  const ordered=[...new Set(all.map(x=>x.date.slice(0,7)))].sort(),chosen=ordered.slice(-3);
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,
@@ -428,11 +455,11 @@ function drawImportedChart(result){
428
  function renderImported(result){
429
  const num=n=>n.toLocaleString('nl-NL');
430
  const kpis=[{label:'Bezoeken (voorlopig)',value:num(result.visits),change:'Unieke cliënt / dag / locatie'},
431
- {label:'Unieke cliëntnummers',value:num(result.unique),change:'Binnen geselecteerde periode'},
432
  {label:'Scanregels',value:num(result.rows),change:`${num(result.duplicates)} herhaalde scans`},
433
  {label:'Verificatie',value:'—',change:'Extern niet uitgevoerd'}];
434
  renderKPIs({kpis});drawImportedChart(result);
435
- const lines=[['Herhaalde scans',num(result.duplicates)],['Unieke cliëntnummers',num(result.unique)],
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)],
@@ -462,7 +489,7 @@ async function startImported(){
462
  const result=calculateImported($('siteSelect').value);importedData.last=result;
463
  const steps=[
464
  {agent:'data',text:`${result.rows.toLocaleString('nl-NL')} scanregels gecontroleerd; ${result.duplicates} herhaalde scans; Scanpassen: ${result.passSheetFound?result.passRows+' registraties':'geen apart werkblad'}`},
465
- {agent:'analysis',text:`${result.visits} bezoeken (voorlopig) en ${result.unique} unieke cliëntnummers berekend`},
466
  {agent:'insight',text:'Maandtrends berekend; AI-duiding nog niet verbonden'},
467
  {agent:'verify',text:'Interne telling gecontroleerd; geen externe schaduwberekening beschikbaar'},
468
  {agent:'report',text:'Conceptpreview bijgewerkt · NIET GEVALIDEERD'}];
@@ -496,7 +523,7 @@ reply=function(q){
496
  const r=importedData.last;
497
  if(!r)return 'Klik op Start demo-analyse om de lokaal ingelezen testgegevens te berekenen.';
498
  if(/valid|controle|wouter|business|betrouw|verschil|akkoord/.test(q.toLowerCase()))return 'Deze Excel-proef is NIET extern gevalideerd. De schaduwberekening van de businesscontroller ontbreekt en de rekenregels moeten nog worden afgestemd.';
499
- if(/bezoek|uniek|scan|groei|locatie|samenvat|rapport|analyse|kwartaal|cijfer/.test(q.toLowerCase()))return `Testbestand (${r.site==='*'?'alle locaties':r.site}): ${r.rows} scanregels, ${r.visits} voorlopige bezoeken na ${r.duplicates} herhaalde scans, en ${r.unique} unieke cliëntnummers. Scanpassen: ${r.passSheetFound?r.passRows+' registraties': 'geen apart werkblad'}; cliënten met meerdere passen: ${r.multiPassClients}; tegenstrijdige koppelingen: ${r.passConflicts+r.identityConflicts}. Dit is een lokale proef zonder AI-API en zonder externe verificatie.`;
500
  return 'De AI-API is nog niet gekoppeld. Ik kan voorlopig alleen de gecontroleerde basisgetallen van het fictieve Excel-testbestand toelichten.';
501
  };
502
 
@@ -549,10 +576,10 @@ function buildWhiteReport(){
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'];
@@ -562,17 +589,26 @@ function buildWhiteReport(){
562
  <div class="report-heading"><div><h1>Reakt Inloop Monitor ${escapeHTML(d.site)}</h1><p>Inzicht in bereik, betrokkenheid en bezoekpatronen</p></div>
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>
570
  <div class="report-bottom-grid">
571
- <section class="report-panel"><h2>Kerncijfers locatie</h2><table class="report-mini-table"><thead><tr><th>Locatie</th><th>Unieke bezoekers</th><th>Unieke bezoekdagen</th><th>Gem. dagen / bezoeker</th></tr></thead><tbody><tr><td>⌖ ${escapeHTML(d.site)}</td><td>${reportNL(d.unique)}</td><td>${reportNL(d.visits)}</td><td>${avg}</td></tr><tr class="total"><td>Totaal / gemiddelde</td><td>${reportNL(d.unique)}</td><td>${reportNL(d.visits)}</td><td>${avg}</td></tr></tbody></table></section>
572
  <section class="report-panel"><h2>Bezoekintensiteit <small>(unieke bezoekdagen per cliëntnummer)</small></h2>${itHTML}<div class="report-intensity-summary"><span>Gemiddeld<br><strong>${avg}</strong></span><span>Mediaan<br><strong>${String(d.median).replace('.',',')}</strong></span><span>Hoogste aantal dagen<br><strong>${d.maxDays}</strong></span></div></section>
573
  <section class="report-panel"><h2>Managementanalyse</h2><div class="report-analysis">${summary.map(t=>`<p>${t}</p>`).join('')}</div></section>
574
  </div>
575
- <div class="report-foot"><span>Bron: Fictieve Reakt-scanregistratie · Definities: één cliëntnummer per locatie per dag telt als één bezoekdag</span><span class="report-provisional">FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD · NIET PUBLICEREN</span></div>`;
576
  }
577
  function openWhiteReport(){
578
  if(!importedData?.last){alert('Upload eerst het fictieve Excel-testbestand en rond de analyse af. Deze witte rapportage gebruikt geen verzonnen demo-KPI’s.');return;}
@@ -600,7 +636,7 @@ async function downloadReportImage(type='png'){
600
  const quality=type==='jpg'?0.94:1;
601
  const dataUrl=canvas.toDataURL(mime,quality);
602
  const link=document.createElement('a');
603
- const site=(currentData&&currentData.site?currentData.site:'monitor').toLowerCase().replace(/[^a-z0-9]+/gi,'_');
604
  link.href=dataUrl;
605
  link.download=`reakt_inloop_monitor_${site}.${type==='jpg'?'jpg':'png'}`;
606
  document.body.appendChild(link);
 
140
  #reportSheet .report-analysis p { margin-bottom:5px!important; }
141
  #reportSheet .report-foot { margin-top:6px!important; padding-top:5px!important; font-size:8px!important; }
142
  }
143
+ /* v0.9.1: transparante cliëntdefinities zonder aanpassing van de ORCHESTRA-huisstijl. */
144
+ .definition-guide{border:1px solid #2c4971;border-radius:11px;background:linear-gradient(110deg,#11243b,#111a33);padding:12px 16px;margin:11px 0 16px;color:#cadcf0}
145
+ .definition-guide-heading{display:flex;align-items:center;gap:10px;margin-bottom:9px;font-size:12px}
146
+ .definition-guide-heading strong{color:#f2f8ff}.definition-guide-heading>span:last-child{font-size:10px;color:#93abc9;margin-left:auto}
147
+ .definition-guide-icon{color:#57dcbc;font-size:17px}
148
+ .definition-guide-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}
149
+ .definition-guide-grid>div{border-left:2px solid #3d7091;padding:1px 0 1px 10px}
150
+ .definition-guide-grid b{font-size:11px;color:#e3f3ff}.definition-guide-grid p{font-size:10px;line-height:1.42;margin:4px 0 0;color:#adbed6}
151
+ .definition-guide-note{font-size:10px;color:#77dbb7;margin-top:10px;line-height:1.35}
152
+ @media(max-width:760px){.definition-guide-grid{grid-template-columns:1fr}.definition-guide-heading{flex-wrap:wrap}.definition-guide-heading>span:last-child{margin-left:0}}
153
+ .report-definitions{border:1px solid #e2e6f1;border-radius:7px;background:#f8faff;padding:8px 12px;margin:-3px 0 10px}
154
+ .report-definitions-title{font-weight:800;color:#181c65;font-size:11px;margin-bottom:5px}
155
+ .report-definitions-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
156
+ .report-definitions-grid>div{border-left:3px solid #7850b9;padding:2px 0 2px 8px;color:#324062;font-size:10px;line-height:1.32}
157
+ .report-definitions-grid b{display:block;font-size:10px;color:#161c64;margin-bottom:2px}
158
+ .report-definitions-note{font-size:9px;color:#52618d;margin-top:5px}
159
+ @media(max-width:1000px){.report-definitions-grid{grid-template-columns:1fr}}
160
+ @media print{#reportSheet .report-definitions-grid{grid-template-columns:repeat(3,minmax(0,1fr))!important}#reportSheet .report-definitions{padding:4px 7px!important;margin:1px 0 6px!important}#reportSheet .report-definitions-grid>div{font-size:7px!important;line-height:1.2!important}#reportSheet .report-definitions-grid b,#reportSheet .report-definitions-title{font-size:8px!important}#reportSheet .report-definitions-note{font-size:7px!important}}
161
  </style></head>
162
  <body>
163
  <div class="app">
 
185
  <div class="content-grid">
186
  <div class="left-content">
187
  <div class="kpi-grid" id="kpis"></div>
188
+ <section class="definition-guide" aria-label="Betekenis van de cliëntcijfers">
189
+ <div class="definition-guide-heading"><span class="definition-guide-icon">ⓘ</span><strong>Wat betekenen deze cliëntcijfers?</strong><span>Rekenregels per inlooplocatie</span></div>
190
+ <div class="definition-guide-grid">
191
+ <div><b>Unieke cliënten</b><p>Iedere cliënt die deze locatie ooit minimaal één keer heeft bezocht én gescand, binnen de beschikbare scanhistorie.</p></div>
192
+ <div><b>Nieuwe instroom</b><p>Cliënten met hun allereerste scan op deze locatie in de gekozen rapportageperiode.</p></div>
193
+ <div><b>Actieve cliënten</b><p>Cliënten die in de afgelopen drie maanden t/m de peildatum minimaal één keer op deze locatie hebben gescand.</p></div>
194
+ </div>
195
+ <div class="definition-guide-note">Een verloren of vervangen scanpas telt niet als een nieuwe cliënt: meerdere passen van dezelfde cliënt worden samengevoegd.</div>
196
+ </section>
197
  <div class="charts-grid"><section class="panel chart-panel"><div class="panel-heading"><div><span class="section-kicker">TRENDANALYSE</span><h3>Bezoeken per locatie</h3></div><span class="subtle">Jul – Sep 2026</span></div><canvas id="chart" width="640" height="250" aria-label="Grafiek fictieve bezoeken per locatie"></canvas><div class="legend"><span><i style="background:#3ee1b6"></i> Soeda</span><span><i style="background:#ffc66a"></i> Rietdijk</span><span><i style="background:#5b9cff"></i> Vredehof</span><span><i style="background:#ad88ff"></i> Gouda</span></div></section>
198
  <section class="panel verification"><div class="panel-heading"><div><span class="section-kicker">KWALITEITSBORGING</span><h3>Verificatie Businesscontroller</h3></div><span class="warn-label">DEMO</span></div><div id="checks" class="checks"></div><div class="warning">⚠ 1 fictieve afwijking. Rapport niet gevalideerd.</div></section></div>
199
  <section class="panel logpanel"><div class="panel-heading"><div><span class="section-kicker">REAL-TIME PROCESS</span><h3>Agent activity log</h3></div><span class="subtle" id="logStatus">Stand-by</span></div><div class="logs" id="logs"><div class="logitem muted">◌ &nbsp; Klik op ‘Start demo-analyse’ om de workflow te bekijken.</div></div></section>
 
205
  </main>
206
  </div>
207
  <div id="reportOverlay" role="dialog" aria-modal="true" aria-label="Reakt Monitor rapport" aria-hidden="true">
208
+ <div class="report-actions"><span><strong>REAKT · Rapportvoorvertoning v0.9.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>
209
  <section id="reportSheet" aria-label="Witte Inloop Monitor"></section>
210
  </div>
211
  <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
 
415
  unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
416
  }
417
  function calculateImported(site='*'){
418
+ const all=importedData.all.filter(r=>site==='*'||r.site===site),keyset=new Set(),ids=new Set(),siteClients=new Set(),monthSite=new Map();
419
  const passByClient=new Map();let missingPass=0;
420
  for(const r of all){
421
  if(r.pass){if(!passByClient.has(r.id))passByClient.set(r.id,new Set());passByClient.get(r.id).add(r.pass)}else missingPass++;
422
  const k=r.site+'|'+r.date+'|'+r.id;
423
  if(keyset.has(k))continue;
424
+ keyset.add(k);ids.add(r.id);siteClients.add(r.site+'|'+r.id);
425
  const mk=r.site+'|'+r.date.slice(0,7);monthSite.set(mk,(monthSite.get(mk)||0)+1);
426
  }
427
  let multiPassClients=0;
 
429
  else multiPassClients=[...passByClient.values()].filter(p=>p.size>1).length;
430
  const ordered=[...new Set(all.map(x=>x.date.slice(0,7)))].sort(),chosen=ordered.slice(-3);
431
  const locations=(site==='*'?importedData.sites:[site]).map(name=>({name,visits:chosen.map(m=>monthSite.get(name+'|'+m)||0)}));
432
+ return {site,rows:all.length,visits:keyset.size,unique:siteClients.size,duplicates:all.length-keyset.size,months:chosen,locations,
433
  invalid:importedData.invalid,passChecked:importedData.hasPassColumn||importedData.passSheetFound,
434
  passSheetFound:importedData.passSheetFound,pasDates:importedData.pasDates,peildatum:importedData.peildatum,multiPassClients,passConflicts:importedData.passConflicts,
435
  identityConflicts:importedData.identityConflicts,unmatchedPasses:importedData.unmatchedPasses,
 
455
  function renderImported(result){
456
  const num=n=>n.toLocaleString('nl-NL');
457
  const kpis=[{label:'Bezoeken (voorlopig)',value:num(result.visits),change:'Unieke cliënt / dag / locatie'},
458
+ {label:'Unieke cliënten',value:num(result.unique),change:result.site==='*'?'Som van de locatiecijfers':'Alle scans in beschikbare historie'},
459
  {label:'Scanregels',value:num(result.rows),change:`${num(result.duplicates)} herhaalde scans`},
460
  {label:'Verificatie',value:'—',change:'Extern niet uitgevoerd'}];
461
  renderKPIs({kpis});drawImportedChart(result);
462
+ const lines=[['Herhaalde scans',num(result.duplicates)],['Unieke cliënten (per locatie)',num(result.unique)],
463
  ['Scanpassen: apart werkblad',result.passSheetFound?num(result.passRows)+' registraties':'NIET AANWEZIG'],
464
  ['Cliënten met meerdere passen',result.passChecked?num(result.multiPassClients):'NIET TE CONTROLEREN'],
465
  ['Scanregels via pas gekoppeld',num(result.linkedByPass)],
 
489
  const result=calculateImported($('siteSelect').value);importedData.last=result;
490
  const steps=[
491
  {agent:'data',text:`${result.rows.toLocaleString('nl-NL')} scanregels gecontroleerd; ${result.duplicates} herhaalde scans; Scanpassen: ${result.passSheetFound?result.passRows+' registraties':'geen apart werkblad'}`},
492
+ {agent:'analysis',text:`${result.visits} bezoeken (voorlopig) en ${result.unique} unieke cliënten per locatie berekend`},
493
  {agent:'insight',text:'Maandtrends berekend; AI-duiding nog niet verbonden'},
494
  {agent:'verify',text:'Interne telling gecontroleerd; geen externe schaduwberekening beschikbaar'},
495
  {agent:'report',text:'Conceptpreview bijgewerkt · NIET GEVALIDEERD'}];
 
523
  const r=importedData.last;
524
  if(!r)return 'Klik op Start demo-analyse om de lokaal ingelezen testgegevens te berekenen.';
525
  if(/valid|controle|wouter|business|betrouw|verschil|akkoord/.test(q.toLowerCase()))return 'Deze Excel-proef is NIET extern gevalideerd. De schaduwberekening van de businesscontroller ontbreekt en de rekenregels moeten nog worden afgestemd.';
526
+ if(/bezoek|uniek|scan|groei|locatie|samenvat|rapport|analyse|kwartaal|cijfer/.test(q.toLowerCase()))return `Testbestand (${r.site==='*'?'alle locaties':r.site}): ${r.rows} scanregels, ${r.visits} voorlopige bezoeken na ${r.duplicates} herhaalde scans, en ${r.unique} unieke cliënten per locatie. Scanpassen: ${r.passSheetFound?r.passRows+' registraties': 'geen apart werkblad'}; cliënten met meerdere passen: ${r.multiPassClients}; tegenstrijdige koppelingen: ${r.passConflicts+r.identityConflicts}. Dit is een lokale proef zonder AI-API en zonder externe verificatie.`;
527
  return 'De AI-API is nog niet gekoppeld. Ik kan voorlopig alleen de gecontroleerde basisgetallen van het fictieve Excel-testbestand toelichten.';
528
  };
529
 
 
576
  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)}.`,
577
  `De gemiddelde bezoekintensiteit bedraagt ${avg} bezoekdagen per cliëntnummer; de mediaan is ${String(d.median).replace('.',',')}.`,
578
  `De meeste bezoekdagen vallen op ${busiest} (${reportNL(weekdayMax)}) en de minste op ${quietest} (${reportNL(weekdayMin)}).`,
579
+ `Nieuwe instroom en actieve cliënten zijn wel gedefinieerd, maar worden in deze testversie nog niet berekend. Pas-startdatums bewijzen geen instroom. Externe verificatie ontbreekt.`];
580
+ const kpis=[{klass:'purple',icon:'♟',label:'Unieke cliënten',value:reportNL(d.unique),sub:'Minimaal één scan ooit op deze locatie'},
581
+ {klass:'green',icon:'+',label:'Nieuwe instroom',value:'—',sub:'Eerste scan in rapportageperiode · nog niet berekend'},
582
+ {klass:'purple',icon:'♙',label:'Actieve cliënten',value:'—',sub:'Min. 1 scan in afgelopen 3 maanden · nog niet berekend'},
583
  {klass:'orange',icon:'▦',label:'Unieke bezoekdagen',value:reportNL(d.visits),sub:'Eén cliëntnummer per locatie per kalenderdag'}];
584
  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('');
585
  const maxI=Math.max(1,...d.intensities),ilabels=['1 – 5 dagen','6 – 20 dagen','21 – 50 dagen','51 – 100 dagen','101 dagen of meer'];
 
589
  <div class="report-heading"><div><h1>Reakt Inloop Monitor ${escapeHTML(d.site)}</h1><p>Inzicht in bereik, betrokkenheid en bezoekpatronen</p></div>
590
  <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>
591
  <div class="report-kpi-grid">${kpiHTML}</div>
592
+ <section class="report-definitions" aria-label="Definities cliëntindicatoren">
593
+ <div class="report-definitions-title">Zo leest u de cliëntcijfers</div>
594
+ <div class="report-definitions-grid">
595
+ <div><b>Unieke cliënten</b>Iedere cliënt die op deze locatie ooit minimaal één keer heeft gescand (binnen de beschikbare historie).</div>
596
+ <div><b>Nieuwe instroom</b>De eerste scan ooit van een cliënt op deze locatie valt in de rapportageperiode.</div>
597
+ <div><b>Actieve cliënten</b>Minimaal één scan op deze locatie in de afgelopen drie maanden tot de peildatum.</div>
598
+ </div>
599
+ <div class="report-definitions-note">Een vervangende scanpas telt niet als nieuwe cliënt. Alle passen van één cliënt worden gekoppeld.</div>
600
+ </section>
601
  <div class="report-chart-grid">
602
+ <section class="report-panel"><h2>Instroom per maand <small>(nieuwe deelnemers)</small></h2><div class="chart-box"><div class="report-no-data">${missing}<br>Definitie vastgesteld · berekening volgt</div></div></section>
603
  <section class="report-panel"><h2>Unieke bezoekdagen per maand</h2><div class="chart-box">${reportChartLine(mon,values)}</div></section>
604
  <section class="report-panel"><h2>Bezoekdagen per weekdag</h2><div class="chart-box">${reportChartBars(week,d.byWeek,'#ff6410',true)}</div></section>
605
  </div>
606
  <div class="report-bottom-grid">
607
+ <section class="report-panel"><h2>Kerncijfers locatie</h2><table class="report-mini-table"><thead><tr><th>Locatie</th><th>Unieke cliënten</th><th>Unieke bezoekdagen</th><th>Gem. dagen / bezoeker</th></tr></thead><tbody><tr><td>⌖ ${escapeHTML(d.site)}</td><td>${reportNL(d.unique)}</td><td>${reportNL(d.visits)}</td><td>${avg}</td></tr><tr class="total"><td>Totaal / gemiddelde</td><td>${reportNL(d.unique)}</td><td>${reportNL(d.visits)}</td><td>${avg}</td></tr></tbody></table></section>
608
  <section class="report-panel"><h2>Bezoekintensiteit <small>(unieke bezoekdagen per cliëntnummer)</small></h2>${itHTML}<div class="report-intensity-summary"><span>Gemiddeld<br><strong>${avg}</strong></span><span>Mediaan<br><strong>${String(d.median).replace('.',',')}</strong></span><span>Hoogste aantal dagen<br><strong>${d.maxDays}</strong></span></div></section>
609
  <section class="report-panel"><h2>Managementanalyse</h2><div class="report-analysis">${summary.map(t=>`<p>${t}</p>`).join('')}</div></section>
610
  </div>
611
+ <div class="report-foot"><span>Bron: Fictieve Reakt-scanregistratie · Eén cliënt per locatie per dag telt als één bezoekdag · Historie beperkt tot geladen bestand</span><span class="report-provisional">FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD · NIET PUBLICEREN</span></div>`;
612
  }
613
  function openWhiteReport(){
614
  if(!importedData?.last){alert('Upload eerst het fictieve Excel-testbestand en rond de analyse af. Deze witte rapportage gebruikt geen verzonnen demo-KPI’s.');return;}
 
636
  const quality=type==='jpg'?0.94:1;
637
  const dataUrl=canvas.toDataURL(mime,quality);
638
  const link=document.createElement('a');
639
+ const site=(importedData?.last?.site||'monitor').toLowerCase().replace(/[^a-z0-9]+/gi,'_');
640
  link.href=dataUrl;
641
  link.download=`reakt_inloop_monitor_${site}.${type==='jpg'?'jpg':'png'}`;
642
  document.body.appendChild(link);