Upload index.html
Browse files- index.html +25 -10
index.html
CHANGED
|
@@ -124,13 +124,13 @@ function stepDescriptions(agent, result, fake){
|
|
| 124 |
const sites=result?.sites??0, rows=result?.rows??0, repeat=result?.duplicates??0;
|
| 125 |
const provisional=result?.visits??0, unique=result?.unique??0;
|
| 126 |
if(agent==='data')return fake?['Demobron geladen','4 voorbeeldlocaties','Fictieve scanregels','Democontrole afgerond','Overdracht aan analyse']:[
|
| 127 |
-
'Excel-bestand ingelezen',`${sites} locaties herkend`,`${n(rows)} scanregels bekeken`,`${n(repeat)} herhaalde scans gevonden`,
|
| 128 |
if(agent==='analysis')return fake?['Voorbeeld-KPI’s geladen','Locaties vergeleken','Trends weergegeven','Groei samengevat','Overdracht aan inzicht']:[
|
| 129 |
'Dubbele scans uitgesloten',`${n(provisional)} bezoeken voorlopig`,`${n(unique)} unieke cliëntnummers`,'Maandtrends weergegeven','Overdracht aan Insight Agent'];
|
| 130 |
if(agent==='insight')return fake?['Voorbeeldtrend gelezen','Locatieverschillen bekeken','Fictieve inzichten verzameld','Conceptduiding opgesteld','Overdracht aan verificatie']:[
|
| 131 |
'Maandgegevens beschikbaar','Locaties in grafiek gezet','Signalen zonder causaliteit','AI-duiding niet aangesloten','Overdracht aan controle'];
|
| 132 |
if(agent==='verify')return fake?['Demovergelijking geopend','5 indicatoren bekeken','1 fictieve afwijking','Rapport niet gevalideerd','Overdracht met waarschuwing']:[
|
| 133 |
-
'Interne telling beschikbaar','
|
| 134 |
return fake?['Concept gestart','Voorbeeldcijfers verwerkt','Illustratie bijgewerkt','Niet gevalideerd gemarkeerd','Concept gereed']:[
|
| 135 |
'Concept gestart','KPI-preview bijgewerkt','Instroom nog niet bepaald','Niet gevalideerd gemarkeerd','Concept gereed'];
|
| 136 |
}
|
|
@@ -236,6 +236,7 @@ async function readExcelLocal(file){
|
|
| 236 |
const header=cellsToDict(rows[0],shared),lookup={};
|
| 237 |
for(const [col,headerText] of Object.entries(header))lookup[String(headerText).toLowerCase().trim()]=col;
|
| 238 |
for(const mandatory of ['scantijd','locatie','clientnummer'])if(!lookup[mandatory])throw Error('Verplichte kolom ontbreekt: '+mandatory);
|
|
|
|
| 239 |
const all=[],invalid={datum:0,locatie:0,client:0};
|
| 240 |
for(let i=1;i<rows.length;i++){
|
| 241 |
const c=cellsToDict(rows[i],shared),site=String(c[lookup.locatie]??'').trim(),id=String(c[lookup.clientnummer]??'').trim(),date=scanDate(c[lookup.scantijd]);
|
|
@@ -243,24 +244,35 @@ async function readExcelLocal(file){
|
|
| 243 |
if(!site)invalid.locatie++;
|
| 244 |
if(!id)invalid.client++;
|
| 245 |
if(!site||!id||!date)continue;
|
| 246 |
-
all.push({date,site,id});
|
| 247 |
}
|
| 248 |
if(!all.length)throw Error('Geen bruikbare scanregels');
|
| 249 |
const months=[...new Set(all.map(r=>r.date.slice(0,7)))].sort();
|
| 250 |
const sites=[...new Set(all.map(r=>r.site))].sort((a,b)=>a.localeCompare(b,'nl'));
|
| 251 |
-
return {all,sites,months,rawCount:rows.length-1,invalid};
|
| 252 |
}
|
| 253 |
function calculateImported(site='*'){
|
| 254 |
const all=importedData.all.filter(r=>site==='*'||r.site===site),keyset=new Set(),ids=new Set(),monthSite=new Map();
|
|
|
|
| 255 |
for(const r of all){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
const k=r.site+'|'+r.date+'|'+r.id;
|
| 257 |
if(keyset.has(k))continue;
|
| 258 |
keyset.add(k);ids.add(r.id);
|
| 259 |
const mk=r.site+'|'+r.date.slice(0,7);monthSite.set(mk,(monthSite.get(mk)||0)+1);
|
| 260 |
}
|
|
|
|
| 261 |
const ordered=[...new Set(all.map(x=>x.date.slice(0,7)))].sort(),chosen=ordered.slice(-3);
|
| 262 |
const locations=(site==='*'?importedData.sites:[site]).map(name=>({name,visits:chosen.map(m=>monthSite.get(name+'|'+m)||0)}));
|
| 263 |
-
return {site,rows:all.length,visits:keyset.size,unique:ids.size,duplicates:all.length-keyset.size,months:chosen,locations,invalid:importedData.invalid};
|
| 264 |
}
|
| 265 |
function drawImportedChart(result){
|
| 266 |
const cvs=$('chart'),ctx=cvs.getContext('2d'),w=cvs.width,h=cvs.height;
|
|
@@ -283,8 +295,11 @@ function renderImported(result){
|
|
| 283 |
const num=n=>n.toLocaleString('nl-NL');
|
| 284 |
const kpis=[{label:'Bezoeken (voorlopig)',value:num(result.visits),change:'Unieke cliënt / dag / locatie'},{label:'Unieke cliëntnummers',value:num(result.unique),change:'Binnen geselecteerde periode'},{label:'Scanregels',value:num(result.rows),change:`${num(result.duplicates)} herhaalde scans`},{label:'Verificatie',value:'—',change:'Extern niet uitgevoerd'}];
|
| 285 |
renderKPIs({kpis});drawImportedChart(result);
|
| 286 |
-
$('checks').innerHTML='<div class="check-row"><span>
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
| 288 |
document.querySelector('.paper-watermark').textContent='FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD';
|
| 289 |
document.querySelector('.paper p').textContent='Lokale Excel-proef · niet voor publicatie';
|
| 290 |
const pk=document.querySelectorAll('.paper-kpis strong');
|
|
@@ -300,7 +315,7 @@ async function startImported(){
|
|
| 300 |
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%'}
|
| 301 |
const result=calculateImported($('siteSelect').value);importedData.last=result;
|
| 302 |
const steps=[
|
| 303 |
-
{agent:'data',text:`${result.rows.toLocaleString('nl-NL')} scanregels gecontroleerd; ${result.duplicates} herhaalde scans
|
| 304 |
{agent:'analysis',text:`${result.visits} bezoeken (voorlopig) en ${result.unique} unieke cliëntnummers berekend`},
|
| 305 |
{agent:'insight',text:'Maandtrends berekend; AI-duiding nog niet verbonden'},
|
| 306 |
{agent:'verify',text:'Interne telling gecontroleerd; geen externe schaduwberekening beschikbaar'},
|
|
@@ -322,7 +337,7 @@ $('scanFile').addEventListener('change',async e=>{
|
|
| 322 |
const sel=$('siteSelect');sel.innerHTML='<option value="*">Alle locaties</option>';
|
| 323 |
importedData.sites.forEach(loc=>{const opt=document.createElement('option');opt.value=loc;opt.textContent=loc;sel.appendChild(opt)});
|
| 324 |
sel.disabled=false;
|
| 325 |
-
state.textContent=`${importedData.rawCount} testregels · ${importedData.sites.length} locaties`;
|
| 326 |
$('startBtn').textContent='▶ Analyseer testbestand';
|
| 327 |
$('flowState').textContent='Testbestand lokaal ingelezen · start de analyse';
|
| 328 |
log('Fictief Excel-testbestand is lokaal ingelezen; nog geen berekeningen gepubliceerd');
|
|
@@ -335,7 +350,7 @@ reply=function(q){
|
|
| 335 |
const r=importedData.last;
|
| 336 |
if(!r)return 'Klik op Start demo-analyse om de lokaal ingelezen testgegevens te berekenen.';
|
| 337 |
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.';
|
| 338 |
-
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. Dit is een lokale proef zonder AI-API en zonder externe verificatie.`;
|
| 339 |
return 'De AI-API is nog niet gekoppeld. Ik kan voorlopig alleen de gecontroleerde basisgetallen van het fictieve Excel-testbestand toelichten.';
|
| 340 |
};
|
| 341 |
|
|
|
|
| 124 |
const sites=result?.sites??0, rows=result?.rows??0, repeat=result?.duplicates??0;
|
| 125 |
const provisional=result?.visits??0, unique=result?.unique??0;
|
| 126 |
if(agent==='data')return fake?['Demobron geladen','4 voorbeeldlocaties','Fictieve scanregels','Democontrole afgerond','Overdracht aan analyse']:[
|
| 127 |
+
'Excel-bestand ingelezen',`${sites} locaties herkend`,`${n(rows)} scanregels bekeken`,`${n(repeat)} herhaalde scans gevonden`,result?.passChecked?`${n(result.multiPassClients)} cliënten met meerdere passen`:'Pascontrole niet beschikbaar'];
|
| 128 |
if(agent==='analysis')return fake?['Voorbeeld-KPI’s geladen','Locaties vergeleken','Trends weergegeven','Groei samengevat','Overdracht aan inzicht']:[
|
| 129 |
'Dubbele scans uitgesloten',`${n(provisional)} bezoeken voorlopig`,`${n(unique)} unieke cliëntnummers`,'Maandtrends weergegeven','Overdracht aan Insight Agent'];
|
| 130 |
if(agent==='insight')return fake?['Voorbeeldtrend gelezen','Locatieverschillen bekeken','Fictieve inzichten verzameld','Conceptduiding opgesteld','Overdracht aan verificatie']:[
|
| 131 |
'Maandgegevens beschikbaar','Locaties in grafiek gezet','Signalen zonder causaliteit','AI-duiding niet aangesloten','Overdracht aan controle'];
|
| 132 |
if(agent==='verify')return fake?['Demovergelijking geopend','5 indicatoren bekeken','1 fictieve afwijking','Rapport niet gevalideerd','Overdracht met waarschuwing']:[
|
| 133 |
+
'Interne telling beschikbaar',result?.passChecked?'Pascontrole vastgelegd':'Geen pasnummerkolom','Businesscontroller: geen bestand','Externe controle overgeslagen','Status: niet gevalideerd'];
|
| 134 |
return fake?['Concept gestart','Voorbeeldcijfers verwerkt','Illustratie bijgewerkt','Niet gevalideerd gemarkeerd','Concept gereed']:[
|
| 135 |
'Concept gestart','KPI-preview bijgewerkt','Instroom nog niet bepaald','Niet gevalideerd gemarkeerd','Concept gereed'];
|
| 136 |
}
|
|
|
|
| 236 |
const header=cellsToDict(rows[0],shared),lookup={};
|
| 237 |
for(const [col,headerText] of Object.entries(header))lookup[String(headerText).toLowerCase().trim()]=col;
|
| 238 |
for(const mandatory of ['scantijd','locatie','clientnummer'])if(!lookup[mandatory])throw Error('Verplichte kolom ontbreekt: '+mandatory);
|
| 239 |
+
const passColumn=lookup['pasnummer']||lookup['scanpasnummer']||lookup['pas nummer']||lookup['pasnr']||null;
|
| 240 |
const all=[],invalid={datum:0,locatie:0,client:0};
|
| 241 |
for(let i=1;i<rows.length;i++){
|
| 242 |
const c=cellsToDict(rows[i],shared),site=String(c[lookup.locatie]??'').trim(),id=String(c[lookup.clientnummer]??'').trim(),date=scanDate(c[lookup.scantijd]);
|
|
|
|
| 244 |
if(!site)invalid.locatie++;
|
| 245 |
if(!id)invalid.client++;
|
| 246 |
if(!site||!id||!date)continue;
|
| 247 |
+
all.push({date,site,id,pass:passColumn?String(c[passColumn]??'').trim():''});
|
| 248 |
}
|
| 249 |
if(!all.length)throw Error('Geen bruikbare scanregels');
|
| 250 |
const months=[...new Set(all.map(r=>r.date.slice(0,7)))].sort();
|
| 251 |
const sites=[...new Set(all.map(r=>r.site))].sort((a,b)=>a.localeCompare(b,'nl'));
|
| 252 |
+
return {all,sites,months,rawCount:rows.length-1,invalid,hasPassColumn:!!passColumn};
|
| 253 |
}
|
| 254 |
function calculateImported(site='*'){
|
| 255 |
const all=importedData.all.filter(r=>site==='*'||r.site===site),keyset=new Set(),ids=new Set(),monthSite=new Map();
|
| 256 |
+
const passByClient=new Map(), clientByPass=new Map();let missingPass=0,passConflicts=0;
|
| 257 |
for(const r of all){
|
| 258 |
+
if(importedData.hasPassColumn){
|
| 259 |
+
if(!r.pass)missingPass++;
|
| 260 |
+
else {
|
| 261 |
+
if(!passByClient.has(r.id))passByClient.set(r.id,new Set());
|
| 262 |
+
passByClient.get(r.id).add(r.pass);
|
| 263 |
+
if(clientByPass.has(r.pass)&&clientByPass.get(r.pass)!==r.id)passConflicts++;
|
| 264 |
+
else clientByPass.set(r.pass,r.id);
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
const k=r.site+'|'+r.date+'|'+r.id;
|
| 268 |
if(keyset.has(k))continue;
|
| 269 |
keyset.add(k);ids.add(r.id);
|
| 270 |
const mk=r.site+'|'+r.date.slice(0,7);monthSite.set(mk,(monthSite.get(mk)||0)+1);
|
| 271 |
}
|
| 272 |
+
const multiPassClients=[...passByClient.values()].filter(p=>p.size>1).length;
|
| 273 |
const ordered=[...new Set(all.map(x=>x.date.slice(0,7)))].sort(),chosen=ordered.slice(-3);
|
| 274 |
const locations=(site==='*'?importedData.sites:[site]).map(name=>({name,visits:chosen.map(m=>monthSite.get(name+'|'+m)||0)}));
|
| 275 |
+
return {site,rows:all.length,visits:keyset.size,unique:ids.size,duplicates:all.length-keyset.size,months:chosen,locations,invalid:importedData.invalid,passChecked:importedData.hasPassColumn,multiPassClients,passConflicts,missingPass};
|
| 276 |
}
|
| 277 |
function drawImportedChart(result){
|
| 278 |
const cvs=$('chart'),ctx=cvs.getContext('2d'),w=cvs.width,h=cvs.height;
|
|
|
|
| 295 |
const num=n=>n.toLocaleString('nl-NL');
|
| 296 |
const kpis=[{label:'Bezoeken (voorlopig)',value:num(result.visits),change:'Unieke cliënt / dag / locatie'},{label:'Unieke cliëntnummers',value:num(result.unique),change:'Binnen geselecteerde periode'},{label:'Scanregels',value:num(result.rows),change:`${num(result.duplicates)} herhaalde scans`},{label:'Verificatie',value:'—',change:'Extern niet uitgevoerd'}];
|
| 297 |
renderKPIs({kpis});drawImportedChart(result);
|
| 298 |
+
$('checks').innerHTML='<div class="check-row"><span>Herhaalde scans</span><span>'+num(result.duplicates)+'</span></div><div class="check-row"><span>Unieke cliëntnummers</span><span>'+num(result.unique)+'</span></div>'+
|
| 299 |
+
'<div class="check-row"><span>Meerdere passen per cliënt</span><span>'+(result.passChecked?num(result.multiPassClients):'NIET TE CONTROLEREN')+'</span></div>'+
|
| 300 |
+
(result.passChecked?'<div class="check-row"><span>Pasnummer gekoppeld aan meerdere cliënten</span><span>'+num(result.passConflicts)+'</span></div>':'')+
|
| 301 |
+
'<div class="check-row"><span>Verificatie Businesscontroller</span><span>NIET AANGELEVERD</span></div>';
|
| 302 |
+
document.querySelector('.verification .warning').textContent=result.passChecked?(result.passConflicts?'⚠ Mogelijk conflicterende pas-cliëntkoppelingen: nader onderzoeken. Niet gevalideerd.':'⚠ Pascontrole uitgevoerd op basis van aangeleverde kolommen. Externe verificatie ontbreekt.'): '⚠ Geen pasnummerkolom: meerdere passen per cliënt niet afzonderlijk controleerbaar. Niet gevalideerd.';
|
| 303 |
document.querySelector('.paper-watermark').textContent='FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD';
|
| 304 |
document.querySelector('.paper p').textContent='Lokale Excel-proef · niet voor publicatie';
|
| 305 |
const pk=document.querySelectorAll('.paper-kpis strong');
|
|
|
|
| 315 |
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%'}
|
| 316 |
const result=calculateImported($('siteSelect').value);importedData.last=result;
|
| 317 |
const steps=[
|
| 318 |
+
{agent:'data',text:`${result.rows.toLocaleString('nl-NL')} scanregels gecontroleerd; ${result.duplicates} herhaalde scans; pascontrole: ${result.passChecked?'beschikbaar':'niet beschikbaar'}`},
|
| 319 |
{agent:'analysis',text:`${result.visits} bezoeken (voorlopig) en ${result.unique} unieke cliëntnummers berekend`},
|
| 320 |
{agent:'insight',text:'Maandtrends berekend; AI-duiding nog niet verbonden'},
|
| 321 |
{agent:'verify',text:'Interne telling gecontroleerd; geen externe schaduwberekening beschikbaar'},
|
|
|
|
| 337 |
const sel=$('siteSelect');sel.innerHTML='<option value="*">Alle locaties</option>';
|
| 338 |
importedData.sites.forEach(loc=>{const opt=document.createElement('option');opt.value=loc;opt.textContent=loc;sel.appendChild(opt)});
|
| 339 |
sel.disabled=false;
|
| 340 |
+
state.textContent=`${importedData.rawCount} testregels · ${importedData.sites.length} locaties · pascontrole ${importedData.hasPassColumn?'beschikbaar':'niet beschikbaar'}`;
|
| 341 |
$('startBtn').textContent='▶ Analyseer testbestand';
|
| 342 |
$('flowState').textContent='Testbestand lokaal ingelezen · start de analyse';
|
| 343 |
log('Fictief Excel-testbestand is lokaal ingelezen; nog geen berekeningen gepubliceerd');
|
|
|
|
| 350 |
const r=importedData.last;
|
| 351 |
if(!r)return 'Klik op Start demo-analyse om de lokaal ingelezen testgegevens te berekenen.';
|
| 352 |
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.';
|
| 353 |
+
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. Pascontrole: ${r.passChecked?r.multiPassClients+' cliënten met meerdere passen, '+r.passConflicts+' pasconflicten':'niet beschikbaar (geen pasnummerkolom)'}. Dit is een lokale proef zonder AI-API en zonder externe verificatie.`;
|
| 354 |
return 'De AI-API is nog niet gekoppeld. Ik kan voorlopig alleen de gecontroleerde basisgetallen van het fictieve Excel-testbestand toelichten.';
|
| 355 |
};
|
| 356 |
|