Marcel0123 commited on
Commit
6de5fe9
·
verified ·
1 Parent(s): 121973f

Upload 2 files

Browse files
REAKT_TEST_TWEE_WERKBLADEN_FICTIEF.xlsx ADDED
Binary file (11.3 kB). View file
 
index.html CHANGED
@@ -21,7 +21,7 @@
21
  @media (prefers-reduced-motion:reduce) { .conductor-icon::before,.conductor-icon::after,.conductor-icon .brain-symbol {animation:none!important} .connections .signal-layer {display:none} }
22
 
23
 
24
- /* v0.4 – compacte invoerbalk; bestaande dashboardstijl bewust ongewijzigd */
25
  .import-panel{display:flex;align-items:center;gap:13px;flex-wrap:wrap;padding:12px 17px;margin-bottom:13px;border-color:#305278;background:linear-gradient(135deg,#10263c,#0d192f)}
26
  .import-intro{display:flex;flex-direction:column;gap:4px;flex:1;min-width:250px}.import-intro strong{font-size:14px}.import-intro small{font-size:10px;color:#9bb0cd}
27
  .import-file-label{padding:10px 14px;border:1px solid #3666a4;border-radius:9px;background:#162c49;color:#dcf1ff;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap}
@@ -130,7 +130,7 @@ function stepDescriptions(agent, result, fake){
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
  }
@@ -220,59 +220,81 @@ function scanDate(v){
220
  }
221
  async function readExcelLocal(file){
222
  if(!file.name.toLowerCase().endsWith('.xlsx'))throw Error('Kies een XLSX-testbestand');
223
- if(file.size>excelLimit)throw Error('Dit is een grote productiedump. Gebruik voorlopig uitsluitend het bijgeleverde kleine fictieve testbestand.');
224
- const buf=await file.arrayBuffer(),zip=zipDirectory(buf);
225
- const shared=[];
226
  const sharedXML=await xmlEntry(zip,'xl/sharedStrings.xml');
227
- if(sharedXML){
228
- const doc=xmlDoc(sharedXML);
229
- for(const si of doc.getElementsByTagNameNS(excelNS,'si'))shared.push(si.textContent??'');
230
- }
231
- const sheetXML=await xmlEntry(zip,'xl/worksheets/sheet1.xml');
232
- if(!sheetXML)throw Error('Werkblad Data dump niet gevonden');
233
- const doc=xmlDoc(sheetXML),rows=doc.getElementsByTagNameNS(excelNS,'row');
234
  if(rows.length<2)throw Error('Geen scanregels gevonden');
235
- if(rows.length>18000)throw Error('Voor deze eerste proef maximaal 18.000 testregels');
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]);
243
- if(!date)invalid.datum++;
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;
@@ -293,17 +315,24 @@ function drawImportedChart(result){
293
  }
294
  function renderImported(result){
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');
306
- [num(result.visits),num(result.unique),'—'].forEach((x,i)=>{if(pk[i])pk[i].textContent=x});
307
  const pl=document.querySelectorAll('.paper-kpis small');if(pl[2])pl[2].textContent='Instroom niet bepaald';
308
  }
309
  async function startImported(){
@@ -315,7 +344,7 @@ async function startImported(){
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,7 +366,7 @@ $('scanFile').addEventListener('change',async e=>{
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,7 +379,7 @@ reply=function(q){
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
 
 
21
  @media (prefers-reduced-motion:reduce) { .conductor-icon::before,.conductor-icon::after,.conductor-icon .brain-symbol {animation:none!important} .connections .signal-layer {display:none} }
22
 
23
 
24
+ /* v0.7 – lokale Excel-proef met Scanpassen; bestaande dashboardstijl bewust ongewijzigd */
25
  .import-panel{display:flex;align-items:center;gap:13px;flex-wrap:wrap;padding:12px 17px;margin-bottom:13px;border-color:#305278;background:linear-gradient(135deg,#10263c,#0d192f)}
26
  .import-intro{display:flex;flex-direction:column;gap:4px;flex:1;min-width:250px}.import-intro strong{font-size:14px}.import-intro small{font-size:10px;color:#9bb0cd}
27
  .import-file-label{padding:10px 14px;border:1px solid #3666a4;border-radius:9px;background:#162c49;color:#dcf1ff;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap}
 
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?.passSheetFound?'Scanpassen-werkblad verwerkt':result?.passChecked?'Paskolom bekeken':'Pascontrole niet mogelijk','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
  }
 
220
  }
221
  async function readExcelLocal(file){
222
  if(!file.name.toLowerCase().endsWith('.xlsx'))throw Error('Kies een XLSX-testbestand');
223
+ if(file.size>excelLimit)throw Error('Gebruik voorlopig uitsluitend het kleine fictieve testbestand (maximaal 2 MB).');
224
+ const zip=zipDirectory(await file.arrayBuffer()),shared=[];
 
225
  const sharedXML=await xmlEntry(zip,'xl/sharedStrings.xml');
226
+ if(sharedXML){for(const si of xmlDoc(sharedXML).getElementsByTagNameNS(excelNS,'si'))shared.push(si.textContent??'')}
227
+ // Deze eerste proef verwacht het klassieke Reakt-testformat met Data dump op sheet1 en Scanpassen op sheet2.
228
+ const sheet=await xmlEntry(zip,'xl/worksheets/sheet1.xml');
229
+ if(!sheet)throw Error('Werkblad Data dump (sheet1) ontbreekt');
230
+ const rows=xmlDoc(sheet).getElementsByTagNameNS(excelNS,'row');
 
 
231
  if(rows.length<2)throw Error('Geen scanregels gevonden');
232
+ if(rows.length>18000)throw Error('Voor deze eerste proef maximaal 18.000 scanregels');
233
  const header=cellsToDict(rows[0],shared),lookup={};
234
+ for(const [col,name] of Object.entries(header))lookup[String(name).toLowerCase().trim()]=col;
235
+ const pick=(l,...names)=>names.map(n=>l[n]).find(Boolean)||null;
236
+ const dateCol=pick(lookup,'scantijd','scandatum'),siteCol=pick(lookup,'locatie'),idCol=pick(lookup,'clientnummer','cliëntnummer'),passColumn=pick(lookup,'pasnummer','scanpasnummer','pas nummer','pasnr');
237
+ if(!dateCol||!siteCol||(!idCol&&!passColumn))throw Error('Kolommen scantijd, locatie en cliëntnummer of pasnummer ontbreken');
238
+ // Werkblad 2 bevat een onafhankelijke pas-cliëntregistratie. Nooit identificeren op naam.
239
+ const passXML=await xmlEntry(zip,'xl/worksheets/sheet2.xml');
240
+ const passToClient=new Map(),clientToPasses=new Map(),passConflicts=new Set();let passRows=0,passMissing=0;
241
+ if(passXML){
242
+ const pr=xmlDoc(passXML).getElementsByTagNameNS(excelNS,'row');
243
+ if(pr.length>20000)throw Error('Voor deze proef maximaal 20.000 scanpassen');
244
+ if(pr.length){
245
+ const ph=cellsToDict(pr[0],shared),pl={};for(const [col,v] of Object.entries(ph))pl[String(v).toLowerCase().trim()]=col;
246
+ const pc=pick(pl,'pasnummer','scanpasnummer','pas nummer','pasnr'),ic=pick(pl,'clientnummer','cliëntnummer');
247
+ if(!pc||!ic)throw Error('Scanpassen mist kolommen Pasnummer en Clientnummer');
248
+ for(let i=1;i<pr.length;i++){
249
+ const r=cellsToDict(pr[i],shared),p=String(r[pc]??'').trim(),c=String(r[ic]??'').trim();passRows++;
250
+ if(!p||!c){passMissing++;continue}
251
+ if(passToClient.has(p)&&passToClient.get(p)!==c)passConflicts.add(p);
252
+ else passToClient.set(p,c);
253
+ if(!clientToPasses.has(c))clientToPasses.set(c,new Set());clientToPasses.get(c).add(p);
254
+ }
255
+ }
256
+ }
257
+ const all=[],invalid={datum:0,locatie:0,client:0},unmatchedPasses=new Set(),identityConflicts=new Set();let linkedByPass=0,rowsWithPass=0;
258
  for(let i=1;i<rows.length;i++){
259
+ const r=cellsToDict(rows[i],shared),site=String(r[siteCol]??'').trim(),date=scanDate(r[dateCol]);
260
+ const pass=passColumn?String(r[passColumn]??'').trim():'',directId=idCol?String(r[idCol]??'').trim():'';
261
+ if(pass)rowsWithPass++;
262
+ const resolved=pass&&!passConflicts.has(pass)?passToClient.get(pass):null;
263
+ if(pass&&passXML&&!resolved)unmatchedPasses.add(pass);
264
+ if(directId&&resolved&&directId!==resolved)identityConflicts.add(pass);
265
+ if(!directId&&resolved)linkedByPass++;
266
+ const id=directId||resolved||'';
267
+ if(!date)invalid.datum++;if(!site)invalid.locatie++;if(!id)invalid.client++;
268
  if(!site||!id||!date)continue;
269
+ all.push({date,site,id,pass});
270
  }
271
  if(!all.length)throw Error('Geen bruikbare scanregels');
 
272
  const sites=[...new Set(all.map(r=>r.site))].sort((a,b)=>a.localeCompare(b,'nl'));
273
+ return {all,sites,rawCount:rows.length-1,invalid,hasPassColumn:!!passColumn,
274
+ passSheetFound:!!passXML,passRows,passMissing,passToClient,clientToPasses,
275
+ passConflicts:passConflicts.size,identityConflicts:identityConflicts.size,
276
+ unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
277
  }
278
  function calculateImported(site='*'){
279
  const all=importedData.all.filter(r=>site==='*'||r.site===site),keyset=new Set(),ids=new Set(),monthSite=new Map();
280
+ const passByClient=new Map();let missingPass=0;
281
  for(const r of all){
282
+ if(r.pass){if(!passByClient.has(r.id))passByClient.set(r.id,new Set());passByClient.get(r.id).add(r.pass)}else missingPass++;
 
 
 
 
 
 
 
 
283
  const k=r.site+'|'+r.date+'|'+r.id;
284
  if(keyset.has(k))continue;
285
  keyset.add(k);ids.add(r.id);
286
  const mk=r.site+'|'+r.date.slice(0,7);monthSite.set(mk,(monthSite.get(mk)||0)+1);
287
  }
288
+ let multiPassClients=0;
289
+ if(importedData.passSheetFound){for(const [client,passes] of importedData.clientToPasses)if(ids.has(client)&&passes.size>1)multiPassClients++}
290
+ else multiPassClients=[...passByClient.values()].filter(p=>p.size>1).length;
291
  const ordered=[...new Set(all.map(x=>x.date.slice(0,7)))].sort(),chosen=ordered.slice(-3);
292
  const locations=(site==='*'?importedData.sites:[site]).map(name=>({name,visits:chosen.map(m=>monthSite.get(name+'|'+m)||0)}));
293
+ return {site,rows:all.length,visits:keyset.size,unique:ids.size,duplicates:all.length-keyset.size,months:chosen,locations,
294
+ invalid:importedData.invalid,passChecked:importedData.hasPassColumn||importedData.passSheetFound,
295
+ passSheetFound:importedData.passSheetFound,multiPassClients,passConflicts:importedData.passConflicts,
296
+ identityConflicts:importedData.identityConflicts,unmatchedPasses:importedData.unmatchedPasses,
297
+ linkedByPass:importedData.linkedByPass,missingPass,passRows:importedData.passRows};
298
  }
299
  function drawImportedChart(result){
300
  const cvs=$('chart'),ctx=cvs.getContext('2d'),w=cvs.width,h=cvs.height;
 
315
  }
316
  function renderImported(result){
317
  const num=n=>n.toLocaleString('nl-NL');
318
+ const kpis=[{label:'Bezoeken (voorlopig)',value:num(result.visits),change:'Unieke cliënt / dag / locatie'},
319
+ {label:'Unieke cliëntnummers',value:num(result.unique),change:'Binnen geselecteerde periode'},
320
+ {label:'Scanregels',value:num(result.rows),change:`${num(result.duplicates)} herhaalde scans`},
321
+ {label:'Verificatie',value:'—',change:'Extern niet uitgevoerd'}];
322
  renderKPIs({kpis});drawImportedChart(result);
323
+ const lines=[['Herhaalde scans',num(result.duplicates)],['Unieke cliëntnummers',num(result.unique)],
324
+ ['Scanpassen: apart werkblad',result.passSheetFound?num(result.passRows)+' registraties':'NIET AANWEZIG'],
325
+ ['Cliënten met meerdere passen',result.passChecked?num(result.multiPassClients):'NIET TE CONTROLEREN'],
326
+ ['Scanregels via pas gekoppeld',num(result.linkedByPass)],
327
+ ['Niet-herkende passen',num(result.unmatchedPasses)],['Tegenstrijdige pasregistraties',num(result.passConflicts)],
328
+ ['Verschil scan/registratie',num(result.identityConflicts)],['Businesscontroller','NIET AANGELEVERD']];
329
+ $('checks').innerHTML=lines.map(([name,val])=>'<div class="check-row"><span>'+escapeHTML(name)+'</span><span>'+escapeHTML(val)+'</span></div>').join('');
330
+ const issue=result.passConflicts||result.identityConflicts||result.unmatchedPasses;
331
+ document.querySelector('.verification .warning').textContent=issue?'⚠ Inconsistente of ontbrekende pas-koppelingen. Eerst onderzoeken. NIET GEVALIDEERD.':
332
+ (result.passSheetFound?'⚠ Scanpassen gecontroleerd; definities en externe verificatie ontbreken. NIET GEVALIDEERD.':'⚠ Geen apart Scanpassen-werkblad; externe verificatie ontbreekt. NIET GEVALIDEERD.');
333
  document.querySelector('.paper-watermark').textContent='FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD';
334
  document.querySelector('.paper p').textContent='Lokale Excel-proef · niet voor publicatie';
335
+ const pk=document.querySelectorAll('.paper-kpis strong');[num(result.visits),num(result.unique),'—'].forEach((x,i)=>{if(pk[i])pk[i].textContent=x});
 
336
  const pl=document.querySelectorAll('.paper-kpis small');if(pl[2])pl[2].textContent='Instroom niet bepaald';
337
  }
338
  async function startImported(){
 
344
  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%'}
345
  const result=calculateImported($('siteSelect').value);importedData.last=result;
346
  const steps=[
347
+ {agent:'data',text:`${result.rows.toLocaleString('nl-NL')} scanregels gecontroleerd; ${result.duplicates} herhaalde scans; Scanpassen: ${result.passSheetFound?result.passRows+' registraties':'geen apart werkblad'}`},
348
  {agent:'analysis',text:`${result.visits} bezoeken (voorlopig) en ${result.unique} unieke cliëntnummers berekend`},
349
  {agent:'insight',text:'Maandtrends berekend; AI-duiding nog niet verbonden'},
350
  {agent:'verify',text:'Interne telling gecontroleerd; geen externe schaduwberekening beschikbaar'},
 
366
  const sel=$('siteSelect');sel.innerHTML='<option value="*">Alle locaties</option>';
367
  importedData.sites.forEach(loc=>{const opt=document.createElement('option');opt.value=loc;opt.textContent=loc;sel.appendChild(opt)});
368
  sel.disabled=false;
369
+ state.textContent=`${importedData.rawCount} testregels · ${importedData.sites.length} locaties · Scanpassen ${importedData.passSheetFound?importedData.passRows+' regels':'ontbreekt'}`;
370
  $('startBtn').textContent='▶ Analyseer testbestand';
371
  $('flowState').textContent='Testbestand lokaal ingelezen · start de analyse';
372
  log('Fictief Excel-testbestand is lokaal ingelezen; nog geen berekeningen gepubliceerd');
 
379
  const r=importedData.last;
380
  if(!r)return 'Klik op Start demo-analyse om de lokaal ingelezen testgegevens te berekenen.';
381
  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.';
382
+ 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.`;
383
  return 'De AI-API is nog niet gekoppeld. Ik kan voorlopig alleen de gecontroleerde basisgetallen van het fictieve Excel-testbestand toelichten.';
384
  };
385