Upload index.html
Browse files- index.html +65 -50
index.html
CHANGED
|
@@ -169,8 +169,8 @@
|
|
| 169 |
<header><div><div class="eyebrow">REAKT / AI ANALYTICS PLATFORM</div><h1>Inloop <span>Intelligence</span> <em>ORCHESTRA</em></h1><p>Een intelligente blik op ontwikkeling, kwaliteit en herstelondersteunende inloop</p></div><div class="header-actions"><span class="pill"><i></i> DEMO MODE</span><span class="clock" id="clock"></span></div></header>
|
| 170 |
<div class="topbar"><div><span class="section-kicker">CONTROL CENTER</span><h2>Multi-Agent Orchestration <span class="live-dot"></span></h2></div><div class="actions"><label class="pace-label" for="paceSelect">Weergave <select id="paceSelect" aria-label="Animatiesnelheid"><option value="presentation">Presentatie ± 30 sec</option><option value="fast">Snel ± 6 sec</option></select></label><button id="resetBtn" class="ghost">↺ Reset</button><button id="startBtn" class="primary">▶ Start demo-analyse</button></div></div>
|
| 171 |
<section class="import-panel panel" aria-label="Excel-proef met fictieve gegevens">
|
| 172 |
-
<div class="import-intro"><span class="section-kicker">V1.
|
| 173 |
-
<label class="import-file-label" for="scanFile"><span>▤</span> Kies
|
| 174 |
<label class="site-label" for="siteSelect">Locatie</label><select id="siteSelect" disabled><option value="*">Alle locaties</option></select>
|
| 175 |
<label class="site-label" for="periodSelect">Kwartaal</label><select id="periodSelect" disabled aria-label="Rapportagekwartaal"><option>—</option></select>
|
| 176 |
<span id="importState" class="import-state">Demo staat klaar</span>
|
|
@@ -307,7 +307,7 @@ function ask(q){if(!q.trim())return;addMessage('user',q);addMessage('assistant',
|
|
| 307 |
// v0.4: veilige browser-lokale Excel-proef, uitsluitend met fictieve / geanonimiseerde voorbeelden.
|
| 308 |
// Er vindt GEEN netwerkverzoek, bestandsoverdracht of externe AI-aanroep plaats.
|
| 309 |
let importedData=null;
|
| 310 |
-
const excelLimit=
|
| 311 |
const excelNS='http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
| 312 |
const decoder=new TextDecoder('utf-8');
|
| 313 |
function bytesText(a){return decoder.decode(a)}
|
|
@@ -321,15 +321,16 @@ function zipDirectory(buffer){
|
|
| 321 |
const entries=new Map();let pos=offset;
|
| 322 |
for(let i=0;i<count;i++){
|
| 323 |
if(view.getUint32(pos,true)!==0x02014b50)throw Error('ZIP-structuur ongeldig');
|
| 324 |
-
const method=view.getUint16(pos+10,true), size=view.getUint32(pos+20,true),len=view.getUint16(pos+28,true),extra=view.getUint16(pos+30,true),comment=view.getUint16(pos+32,true),local=view.getUint32(pos+42,true);
|
| 325 |
const name=bytesText(bytes.subarray(pos+46,pos+46+len));
|
| 326 |
-
if(name.startsWith('xl/'))entries.set(name,{method,size,local});
|
| 327 |
pos+=46+len+extra+comment;
|
| 328 |
}
|
| 329 |
return {view,bytes,entries};
|
| 330 |
}
|
| 331 |
async function xmlEntry(zip,name){
|
| 332 |
const entry=zip.entries.get(name);if(!entry)return '';
|
|
|
|
| 333 |
const {view,bytes}=zip, off=entry.local;
|
| 334 |
if(view.getUint32(off,true)!==0x04034b50)throw Error('ZIP-bestandskop ongeldig');
|
| 335 |
const start=off+30+view.getUint16(off+26,true)+view.getUint16(off+28,true);
|
|
@@ -343,15 +344,26 @@ function xmlDoc(str){
|
|
| 343 |
const doc=new DOMParser().parseFromString(str,'application/xml');
|
| 344 |
if(doc.getElementsByTagName('parsererror').length)throw Error('Excel-XML niet leesbaar');return doc;
|
| 345 |
}
|
| 346 |
-
|
| 347 |
-
const
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
}
|
| 356 |
function scanDate(v){
|
| 357 |
if(v===''||v===undefined||v===null)return '';
|
|
@@ -366,43 +378,34 @@ function scanDate(v){
|
|
| 366 |
if(nl)return `${nl[3]}-${nl[2].padStart(2,'0')}-${nl[1].padStart(2,'0')}`;
|
| 367 |
return '';
|
| 368 |
}
|
| 369 |
-
async function readExcelLocal(file){
|
| 370 |
-
if(!file.name.toLowerCase().endsWith('.xlsx'))throw Error('Kies een XLSX-
|
| 371 |
-
if(file.size>excelLimit)throw Error('
|
|
|
|
| 372 |
const zip=zipDirectory(await file.arrayBuffer()),shared=[];
|
|
|
|
|
|
|
|
|
|
| 373 |
const sharedXML=await xmlEntry(zip,'xl/sharedStrings.xml');
|
| 374 |
-
if(sharedXML){for(const
|
| 375 |
-
// Deze eerste proef verwacht het klassieke Reakt-testformat met Data dump op sheet1 en Scanpassen op sheet2.
|
| 376 |
-
const sheet=await xmlEntry(zip,'xl/worksheets/sheet1.xml');
|
| 377 |
-
if(!sheet)throw Error('Werkblad Data dump (sheet1) ontbreekt');
|
| 378 |
-
const rows=xmlDoc(sheet).getElementsByTagNameNS(excelNS,'row');
|
| 379 |
-
if(rows.length<2)throw Error('Geen scanregels gevonden');
|
| 380 |
-
if(rows.length>18000)throw Error('Voor deze eerste proef maximaal 18.000 scanregels');
|
| 381 |
-
const header=cellsToDict(rows[0],shared),lookup={};
|
| 382 |
-
for(const [col,name] of Object.entries(header))lookup[String(name).toLowerCase().trim()]=col;
|
| 383 |
const pick=(l,...names)=>names.map(n=>l[n]).find(Boolean)||null;
|
| 384 |
-
const dateCol=pick(lookup,'scantijd','scandatum'),siteCol=pick(lookup,'locatie'),idCol=pick(lookup,'clientnummer','cliëntnummer'),passColumn=pick(lookup,'pasnummer','scanpasnummer','pas nummer','pasnr');
|
| 385 |
-
if(!dateCol||!siteCol||(!idCol&&!passColumn))throw Error('Kolommen scantijd, locatie en cliëntnummer of pasnummer ontbreken');
|
| 386 |
-
// Werkblad 2 bevat een onafhankelijke pas-cliëntregistratie. Nooit identificeren op naam.
|
| 387 |
-
const passXML=await xmlEntry(zip,'xl/worksheets/sheet2.xml');
|
| 388 |
const passToClient=new Map(),clientToPasses=new Map(),passConflicts=new Set();let passRows=0,passMissing=0;
|
| 389 |
const pasDates={startPresent:0,endPresent:0,migration:0,invalidDates:0,notAvailable:false,activeAtPeildatum:0};
|
| 390 |
const passDateRecords=[];
|
|
|
|
|
|
|
| 391 |
if(passXML){
|
| 392 |
-
const
|
| 393 |
-
if(
|
| 394 |
-
if(pr.length){
|
| 395 |
-
const ph=cellsToDict(pr[0],shared),pl={};for(const [col,v] of Object.entries(ph))pl[String(v).toLowerCase().trim()]=col;
|
| 396 |
const pc=pick(pl,'pasnummer','scanpasnummer','pas nummer','pasnr'),ic=pick(pl,'clientnummer','cliëntnummer'),startCol=pick(pl,'startdatum'),endCol=pick(pl,'einddatum');
|
| 397 |
pasDates.notAvailable=!(startCol&&endCol);
|
| 398 |
if(!pc||!ic)throw Error('Scanpassen mist kolommen Pasnummer en Clientnummer');
|
| 399 |
-
for(
|
| 400 |
-
|
| 401 |
-
const
|
| 402 |
-
if(
|
| 403 |
-
if(
|
| 404 |
-
if(
|
| 405 |
-
if(c&&p)passDateRecords.push({client:c,start,end});
|
| 406 |
if(!p||!c){passMissing++;continue}
|
| 407 |
if(passToClient.has(p)&&passToClient.get(p)!==c)passConflicts.add(p);
|
| 408 |
else passToClient.set(p,c);
|
|
@@ -410,9 +413,18 @@ async function readExcelLocal(file){
|
|
| 410 |
}
|
| 411 |
}
|
| 412 |
}
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
const pass=passColumn?String(r[passColumn]??'').trim():'',directId=idCol?String(r[idCol]??'').trim():'';
|
| 417 |
if(pass)rowsWithPass++;
|
| 418 |
const resolved=pass&&!passConflicts.has(pass)?passToClient.get(pass):null;
|
|
@@ -429,7 +441,8 @@ async function readExcelLocal(file){
|
|
| 429 |
const peildatum=all.reduce((m,r)=>r.date>m?r.date:m,'');
|
| 430 |
const activeIds=new Set(passDateRecords.filter(r=>r.start&&r.end&&r.start<=peildatum&&r.end>=peildatum).map(r=>r.client));
|
| 431 |
pasDates.activeAtPeildatum=activeIds.size;
|
| 432 |
-
|
|
|
|
| 433 |
passSheetFound:!!passXML,passRows,passMissing,passToClient,clientToPasses,
|
| 434 |
passConflicts:passConflicts.size,identityConflicts:identityConflicts.size,
|
| 435 |
unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
|
|
@@ -638,7 +651,9 @@ $('scanFile').addEventListener('change',async e=>{
|
|
| 638 |
importedData=null;resetControllerComparison(true);updateControllerContext();const file=e.target.files?.[0];if(!file)return;
|
| 639 |
const state=$('importState');state.textContent='Bestand wordt lokaal gelezen...';$('startBtn').disabled=true;
|
| 640 |
try{
|
| 641 |
-
|
|
|
|
|
|
|
| 642 |
const sel=$('siteSelect');sel.innerHTML='<option value="*">Alle locaties</option>';
|
| 643 |
importedData.sites.forEach(loc=>{const opt=document.createElement('option');opt.value=loc;opt.textContent=loc;sel.appendChild(opt)});
|
| 644 |
sel.disabled=false;
|
|
@@ -646,10 +661,10 @@ $('scanFile').addEventListener('change',async e=>{
|
|
| 646 |
const quarter=$('periodSelect');quarter.replaceChildren();
|
| 647 |
periods.forEach(key=>{const opt=document.createElement('option');opt.value=key;opt.textContent=quarterDates(key).label;quarter.appendChild(opt)});
|
| 648 |
quarter.disabled=false;
|
| 649 |
-
state.textContent=`${importedData.rawCount}
|
| 650 |
-
$('startBtn').textContent='▶ Analyseer
|
| 651 |
-
$('flowState').textContent='
|
| 652 |
-
log('
|
| 653 |
}catch(err){$('startBtn').textContent='▶ Start demo-analyse';state.textContent='⚠ '+err.message;$('siteSelect').disabled=true;$('periodSelect').disabled=true;log('Excel-proef: '+err.message)}finally{$('startBtn').disabled=false}
|
| 654 |
});
|
| 655 |
$('siteSelect').addEventListener('change',()=>{if(importedData){importedData.last=null;resetControllerComparison(true);updateControllerContext();$('flowState').textContent='Locatie gewijzigd · analyse opnieuw starten';}});
|
|
|
|
| 169 |
<header><div><div class="eyebrow">REAKT / AI ANALYTICS PLATFORM</div><h1>Inloop <span>Intelligence</span> <em>ORCHESTRA</em></h1><p>Een intelligente blik op ontwikkeling, kwaliteit en herstelondersteunende inloop</p></div><div class="header-actions"><span class="pill"><i></i> DEMO MODE</span><span class="clock" id="clock"></span></div></header>
|
| 170 |
<div class="topbar"><div><span class="section-kicker">CONTROL CENTER</span><h2>Multi-Agent Orchestration <span class="live-dot"></span></h2></div><div class="actions"><label class="pace-label" for="paceSelect">Weergave <select id="paceSelect" aria-label="Animatiesnelheid"><option value="presentation">Presentatie ± 30 sec</option><option value="fast">Snel ± 6 sec</option></select></label><button id="resetBtn" class="ghost">↺ Reset</button><button id="startBtn" class="primary">▶ Start demo-analyse</button></div></div>
|
| 171 |
<section class="import-panel panel" aria-label="Excel-proef met fictieve gegevens">
|
| 172 |
+
<div class="import-intro"><span class="section-kicker">V1.3 · GROTE-BESTANDENPROEF</span><strong>Lokale scandata-test</strong><small>Test lokaal in browser · eerst fictieve gegevens · 25 MB testlimiet</small></div>
|
| 173 |
+
<label class="import-file-label" for="scanFile"><span>▤</span> Kies Excelbestand (.xlsx)</label><input type="file" id="scanFile" accept=".xlsx" />
|
| 174 |
<label class="site-label" for="siteSelect">Locatie</label><select id="siteSelect" disabled><option value="*">Alle locaties</option></select>
|
| 175 |
<label class="site-label" for="periodSelect">Kwartaal</label><select id="periodSelect" disabled aria-label="Rapportagekwartaal"><option>—</option></select>
|
| 176 |
<span id="importState" class="import-state">Demo staat klaar</span>
|
|
|
|
| 307 |
// v0.4: veilige browser-lokale Excel-proef, uitsluitend met fictieve / geanonimiseerde voorbeelden.
|
| 308 |
// Er vindt GEEN netwerkverzoek, bestandsoverdracht of externe AI-aanroep plaats.
|
| 309 |
let importedData=null;
|
| 310 |
+
const excelLimit=25*1024*1024; // v1.3: technische grens, geen privacytoestemming
|
| 311 |
const excelNS='http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
| 312 |
const decoder=new TextDecoder('utf-8');
|
| 313 |
function bytesText(a){return decoder.decode(a)}
|
|
|
|
| 321 |
const entries=new Map();let pos=offset;
|
| 322 |
for(let i=0;i<count;i++){
|
| 323 |
if(view.getUint32(pos,true)!==0x02014b50)throw Error('ZIP-structuur ongeldig');
|
| 324 |
+
const method=view.getUint16(pos+10,true), size=view.getUint32(pos+20,true),inflated=view.getUint32(pos+24,true),len=view.getUint16(pos+28,true),extra=view.getUint16(pos+30,true),comment=view.getUint16(pos+32,true),local=view.getUint32(pos+42,true);
|
| 325 |
const name=bytesText(bytes.subarray(pos+46,pos+46+len));
|
| 326 |
+
if(name.startsWith('xl/'))entries.set(name,{method,size,inflated,local});
|
| 327 |
pos+=46+len+extra+comment;
|
| 328 |
}
|
| 329 |
return {view,bytes,entries};
|
| 330 |
}
|
| 331 |
async function xmlEntry(zip,name){
|
| 332 |
const entry=zip.entries.get(name);if(!entry)return '';
|
| 333 |
+
if(entry.inflated>180*1024*1024)throw Error('Een Excel-onderdeel is uitgepakt groter dan 180 MB; bestand eerst opsplitsen');
|
| 334 |
const {view,bytes}=zip, off=entry.local;
|
| 335 |
if(view.getUint32(off,true)!==0x04034b50)throw Error('ZIP-bestandskop ongeldig');
|
| 336 |
const start=off+30+view.getUint16(off+26,true)+view.getUint16(off+28,true);
|
|
|
|
| 344 |
const doc=new DOMParser().parseFromString(str,'application/xml');
|
| 345 |
if(doc.getElementsByTagName('parsererror').length)throw Error('Excel-XML niet leesbaar');return doc;
|
| 346 |
}
|
| 347 |
+
// v1.3: rijgewijs XML uitlezen; geen DOM-boom van 100+ MB grote werkbladen.
|
| 348 |
+
function decodeXML(v){return String(v).replace(/&(?:amp|lt|gt|quot|apos|#(?:x[0-9a-fA-F]+|\d+));/g,m=>{const k=m.slice(1,-1);if(k[0]==='#'){const n=k[1]?.toLowerCase()==='x'?parseInt(k.slice(2),16):Number(k.slice(1));return Number.isInteger(n)&&n>=0&&n<=0x10ffff?String.fromCodePoint(n):m}return {amp:'&',lt:'<',gt:'>',quot:'"',apos:"'"}[k]||m})}
|
| 349 |
+
function parseSharedLite(xml){
|
| 350 |
+
const arr=[];const si=/<(?:[\w.-]+:)?si\b[^>]*>([\s\S]*?)<\/(?:[\w.-]+:)?si>/g;let m;
|
| 351 |
+
while((m=si.exec(xml))!==null){let t='',p;const re=/<(?:[\w.-]+:)?t(?:\s[^>]*)?>([\s\S]*?)<\/(?:[\w.-]+:)?t>/g;while((p=re.exec(m[1]))!==null)t+=decodeXML(p[1]);arr.push(t)}
|
| 352 |
+
return arr;
|
| 353 |
+
}
|
| 354 |
+
function *sheetRows(xml,shared){
|
| 355 |
+
const rowRe=/<(?:[\w.-]+:)?row\b[^>]*>([\s\S]*?)<\/(?:[\w.-]+:)?row>/g, cellRe=/<(?:[\w.-]+:)?c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/(?:[\w.-]+:)?c>)/g;let row;
|
| 356 |
+
while((row=rowRe.exec(xml))!==null){const values={};let c;cellRe.lastIndex=0;
|
| 357 |
+
while((c=cellRe.exec(row[1]))!==null){const col=/\br="([A-Z]+)\d+"/.exec(c[1]);if(!col)continue;
|
| 358 |
+
const type=/\bt="([^"]+)"/.exec(c[1])?.[1]||'';const payload=c[2]||'';
|
| 359 |
+
let v=/<(?:[\w.-]+:)?v(?:\s[^>]*)?>([\s\S]*?)<\/(?:[\w.-]+:)?v>/.exec(payload)?.[1]??'';
|
| 360 |
+
if(type==='s')v=shared[Number(v)]??'';
|
| 361 |
+
else if(type==='inlineStr'){v='';let t;const tr=/<(?:[\w.-]+:)?t(?:\s[^>]*)?>([\s\S]*?)<\/(?:[\w.-]+:)?t>/g;while((t=tr.exec(payload))!==null)v+=decodeXML(t[1]);}
|
| 362 |
+
else v=decodeXML(v);
|
| 363 |
+
values[col[1]]=v;
|
| 364 |
+
}
|
| 365 |
+
yield values;
|
| 366 |
+
}
|
| 367 |
}
|
| 368 |
function scanDate(v){
|
| 369 |
if(v===''||v===undefined||v===null)return '';
|
|
|
|
| 378 |
if(nl)return `${nl[3]}-${nl[2].padStart(2,'0')}-${nl[1].padStart(2,'0')}`;
|
| 379 |
return '';
|
| 380 |
}
|
| 381 |
+
async function readExcelLocal(file,onProgress=()=>{}){
|
| 382 |
+
if(!file.name.toLowerCase().endsWith('.xlsx'))throw Error('Kies een XLSX-bestand');
|
| 383 |
+
if(file.size>excelLimit)throw Error('Dit bestand is groter dan de v1.3-testgrens van 25 MB');
|
| 384 |
+
onProgress('Excel wordt geopend ('+(file.size/1024/1024).toFixed(1)+' MB)…');
|
| 385 |
const zip=zipDirectory(await file.arrayBuffer()),shared=[];
|
| 386 |
+
const scanMeta=zip.entries.get('xl/worksheets/sheet1.xml');
|
| 387 |
+
if(!scanMeta)throw Error('Werkblad Data dump (sheet1) ontbreekt');
|
| 388 |
+
onProgress('Excel-structuur gecontroleerd · werkbladen worden gelezen…');
|
| 389 |
const sharedXML=await xmlEntry(zip,'xl/sharedStrings.xml');
|
| 390 |
+
if(sharedXML){for(const item of parseSharedLite(sharedXML))shared.push(item);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
const pick=(l,...names)=>names.map(n=>l[n]).find(Boolean)||null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
const passToClient=new Map(),clientToPasses=new Map(),passConflicts=new Set();let passRows=0,passMissing=0;
|
| 393 |
const pasDates={startPresent:0,endPresent:0,migration:0,invalidDates:0,notAvailable:false,activeAtPeildatum:0};
|
| 394 |
const passDateRecords=[];
|
| 395 |
+
// Eerst de pasregistratie, zodat scans zonder direct cliëntnummer gekoppeld kunnen worden.
|
| 396 |
+
const passXML=await xmlEntry(zip,'xl/worksheets/sheet2.xml');
|
| 397 |
if(passXML){
|
| 398 |
+
const it=sheetRows(passXML,shared),first=it.next();
|
| 399 |
+
if(!first.done){const pl={};for(const [col,v] of Object.entries(first.value))pl[String(v).toLowerCase().trim()]=col;
|
|
|
|
|
|
|
| 400 |
const pc=pick(pl,'pasnummer','scanpasnummer','pas nummer','pasnr'),ic=pick(pl,'clientnummer','cliëntnummer'),startCol=pick(pl,'startdatum'),endCol=pick(pl,'einddatum');
|
| 401 |
pasDates.notAvailable=!(startCol&&endCol);
|
| 402 |
if(!pc||!ic)throw Error('Scanpassen mist kolommen Pasnummer en Clientnummer');
|
| 403 |
+
for(const r of it){const p=String(r[pc]??'').trim(),c=String(r[ic]??'').trim();passRows++;
|
| 404 |
+
if(passRows>350000)throw Error('Meer dan 350.000 pasregistraties; contacteer ontwikkelaar voor grotere import');
|
| 405 |
+
const st=startCol?scanDate(r[startCol]):'',en=endCol?scanDate(r[endCol]):'';
|
| 406 |
+
if(st)pasDates.startPresent++;if(en)pasDates.endPresent++;
|
| 407 |
+
if(st==='2022-10-28')pasDates.migration++;if(st&&en&&en<st)pasDates.invalidDates++;
|
| 408 |
+
if(c&&p)passDateRecords.push({client:c,start:st,end:en});
|
|
|
|
| 409 |
if(!p||!c){passMissing++;continue}
|
| 410 |
if(passToClient.has(p)&&passToClient.get(p)!==c)passConflicts.add(p);
|
| 411 |
else passToClient.set(p,c);
|
|
|
|
| 413 |
}
|
| 414 |
}
|
| 415 |
}
|
| 416 |
+
onProgress('Scanpassen verwerkt ('+passRows.toLocaleString('nl-NL')+') · scanhistorie wordt gelezen…');
|
| 417 |
+
await new Promise(resolve=>setTimeout(resolve,0));
|
| 418 |
+
const sheet=await xmlEntry(zip,'xl/worksheets/sheet1.xml');
|
| 419 |
+
if(!sheet)throw Error('Werkblad Data dump ontbreekt');
|
| 420 |
+
const it=sheetRows(sheet,shared),first=it.next();if(first.done)throw Error('Werkblad Data dump is leeg');
|
| 421 |
+
const lookup={};for(const [col,name] of Object.entries(first.value))lookup[String(name).toLowerCase().trim()]=col;
|
| 422 |
+
const dateCol=pick(lookup,'scantijd','scandatum'),siteCol=pick(lookup,'locatie'),idCol=pick(lookup,'clientnummer','cliëntnummer'),passColumn=pick(lookup,'pasnummer','scanpasnummer','pas nummer','pasnr');
|
| 423 |
+
if(!dateCol||!siteCol||(!idCol&&!passColumn))throw Error('Kolommen scantijd, locatie en cliëntnummer of pasnummer ontbreken');
|
| 424 |
+
const all=[],invalid={datum:0,locatie:0,client:0},unmatchedPasses=new Set(),identityConflicts=new Set();let linkedByPass=0,rowsWithPass=0,rawCount=0;
|
| 425 |
+
for(const r of it){rawCount++;
|
| 426 |
+
if(rawCount>500000)throw Error('Meer dan 500.000 scans: importlimiet v1.3 bereikt; bestand opsplitsen');
|
| 427 |
+
const site=String(r[siteCol]??'').trim(),date=scanDate(r[dateCol]);
|
| 428 |
const pass=passColumn?String(r[passColumn]??'').trim():'',directId=idCol?String(r[idCol]??'').trim():'';
|
| 429 |
if(pass)rowsWithPass++;
|
| 430 |
const resolved=pass&&!passConflicts.has(pass)?passToClient.get(pass):null;
|
|
|
|
| 441 |
const peildatum=all.reduce((m,r)=>r.date>m?r.date:m,'');
|
| 442 |
const activeIds=new Set(passDateRecords.filter(r=>r.start&&r.end&&r.start<=peildatum&&r.end>=peildatum).map(r=>r.client));
|
| 443 |
pasDates.activeAtPeildatum=activeIds.size;
|
| 444 |
+
onProgress('Ingelezen: '+rawCount.toLocaleString('nl-NL')+' scanregels · '+sites.length+' locaties');
|
| 445 |
+
return {all,sites,peildatum,pasDates,rawCount,invalid,hasPassColumn:!!passColumn,
|
| 446 |
passSheetFound:!!passXML,passRows,passMissing,passToClient,clientToPasses,
|
| 447 |
passConflicts:passConflicts.size,identityConflicts:identityConflicts.size,
|
| 448 |
unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
|
|
|
|
| 651 |
importedData=null;resetControllerComparison(true);updateControllerContext();const file=e.target.files?.[0];if(!file)return;
|
| 652 |
const state=$('importState');state.textContent='Bestand wordt lokaal gelezen...';$('startBtn').disabled=true;
|
| 653 |
try{
|
| 654 |
+
const importStart=performance.now();
|
| 655 |
+
importedData=await readExcelLocal(file,msg=>{state.textContent=msg});
|
| 656 |
+
const importSeconds=((performance.now()-importStart)/1000).toFixed(1);
|
| 657 |
const sel=$('siteSelect');sel.innerHTML='<option value="*">Alle locaties</option>';
|
| 658 |
importedData.sites.forEach(loc=>{const opt=document.createElement('option');opt.value=loc;opt.textContent=loc;sel.appendChild(opt)});
|
| 659 |
sel.disabled=false;
|
|
|
|
| 661 |
const quarter=$('periodSelect');quarter.replaceChildren();
|
| 662 |
periods.forEach(key=>{const opt=document.createElement('option');opt.value=key;opt.textContent=quarterDates(key).label;quarter.appendChild(opt)});
|
| 663 |
quarter.disabled=false;
|
| 664 |
+
state.textContent=`${importedData.rawCount.toLocaleString('nl-NL')} scanregels · ${importedData.sites.length} locaties · ${importSeconds} sec inleestijd · Scanpassen ${importedData.passSheetFound?importedData.passRows+' regels':'ontbreekt'}`;
|
| 665 |
+
$('startBtn').textContent='▶ Analyseer Excelbestand';
|
| 666 |
+
$('flowState').textContent='Excelbestand lokaal ingelezen · start de analyse';
|
| 667 |
+
log('Excelbestand lokaal verwerkt; analyse moet nog starten. Voor echte data eerst interne toestemming controleren.');
|
| 668 |
}catch(err){$('startBtn').textContent='▶ Start demo-analyse';state.textContent='⚠ '+err.message;$('siteSelect').disabled=true;$('periodSelect').disabled=true;log('Excel-proef: '+err.message)}finally{$('startBtn').disabled=false}
|
| 669 |
});
|
| 670 |
$('siteSelect').addEventListener('change',()=>{if(importedData){importedData.last=null;resetControllerComparison(true);updateControllerContext();$('flowState').textContent='Locatie gewijzigd · analyse opnieuw starten';}});
|