Upload index.html
Browse files- index.html +66 -31
index.html
CHANGED
|
@@ -27,7 +27,7 @@
|
|
| 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}
|
| 28 |
.import-file-label span{color:#53d9bf;font-size:16px;margin-right:7px}
|
| 29 |
#scanFile{display:none}.site-label{font-size:11px;color:#a6bad8}#siteSelect{background:#132947;border:1px solid #325172;border-radius:8px;color:#eaf2ff;max-width:185px;padding:9px;font:inherit;font-size:12px}
|
| 30 |
-
#siteSelect:disabled{opacity:.55}#periodSelect{background:#132947;border:1px solid #325172;border-radius:8px;color:#eaf2ff;padding:9px;font:inherit;font-size:12px;max-width:138px}#periodSelect:disabled{opacity:.55}.import-state{color:#78e0c3;font-size:11px;max-width:210px;line-height:1.4}
|
| 31 |
@media(max-width:760px){.import-panel{align-items:start}.import-intro{flex-basis:100%}.import-state{flex-basis:100%;max-width:none}}
|
| 32 |
|
| 33 |
/* v0.5: agent-workflow-inzicht, zonder de goedgekeurde basisvormgeving te wijzigen */
|
|
@@ -166,13 +166,13 @@
|
|
| 166 |
<body>
|
| 167 |
<div class="app">
|
| 168 |
<main class="main">
|
| 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>
|
| 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 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="
|
| 176 |
<span id="importState" class="import-state">Demo staat klaar</span>
|
| 177 |
</section>
|
| 178 |
<section class="orchestra panel">
|
|
@@ -246,7 +246,7 @@ function clock(){ $('clock').textContent=new Intl.DateTimeFormat('nl-NL',{hour:'
|
|
| 246 |
clock();setInterval(clock,60000);
|
| 247 |
function renderKPIs(data){$('kpis').innerHTML=data.kpis.map(k=>`<div class="kpi"><small>${escapeHTML(k.label)}</small><strong>${escapeHTML(k.value)}</strong><span>${escapeHTML(k.change)}</span></div>`).join('');}
|
| 248 |
function renderChecks(data){$('checks').innerHTML=data.checks.map(c=>{const delta=c.ours-c.shadow;return `<div class="check-row"><span>${escapeHTML(c.label)}</span><span>${c.ours.toLocaleString('nl-NL')} / ${c.shadow.toLocaleString('nl-NL')} <strong class="${delta?'bad':''}">${delta?'⚠ '+(delta>0?'+':'')+delta:'✓ 0'}</strong></span></div>`}).join('');}
|
| 249 |
-
function drawChart(data){const canvas=$('chart'),ctx=canvas.getContext('2d'),w=640,h=250;ctx.clearRect(0,0,w,h);ctx.font='13px Segoe UI';ctx.textAlign='left';const max=500;for(let i=0;i<=4;i++){let y=25+i*45;ctx.strokeStyle='#29405a';ctx.beginPath();ctx.moveTo(44,y);ctx.lineTo(w-15,y);ctx.stroke();ctx.fillStyle='#8298b4';ctx.fillText(String(Math.round(max*(4-i)/4)),5,y+4);}const cols=['#3ee1b6','#ffc66a','#5b9cff','#ad88ff'];data.locations.forEach((loc,index)=>{ctx.strokeStyle=cols[index];ctx.lineWidth=3;ctx.beginPath();loc.visits.forEach((v,j)=>{const x=
|
| 250 |
function log(text){const now=new Date().toLocaleTimeString('nl-NL',{hour:'2-digit',minute:'2-digit',second:'2-digit'});if($('logs').querySelector('.muted'))$('logs').innerHTML='';$('logs').insertAdjacentHTML('afterbegin',`<div class="logitem"><time>${now}</time>${escapeHTML(text)}</div>`)}
|
| 251 |
// De lichtpunten volgen de vijf bestaande SVG-verbindingen. Ze lopen alleen tijdens een actieve processtap.
|
| 252 |
const signalPaths={data:'M500 0 V30 H100 V90',analysis:'M500 0 V30 H300 V90',insight:'M500 0 V90',verify:'M500 0 V30 H700 V90',report:'M500 0 V30 H900 V90'};
|
|
@@ -447,7 +447,7 @@ async function readExcelLocal(file,onProgress=()=>{}){
|
|
| 447 |
passConflicts:passConflicts.size,identityConflicts:identityConflicts.size,
|
| 448 |
unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
|
| 449 |
}
|
| 450 |
-
// v1.
|
| 451 |
function quarterKey(date){return date.slice(0,4)+'-Q'+(Math.floor((Number(date.slice(5,7))-1)/3)+1);}
|
| 452 |
function quarterDates(key){
|
| 453 |
const m=String(key).match(/^(\d{4})-Q([1-4])$/);if(!m)throw Error('Kies een geldig rapportagekwartaal');
|
|
@@ -472,12 +472,46 @@ function firstScansBySite(all){
|
|
| 472 |
for(const r of all){const key=JSON.stringify([r.site,r.id]);if(!first.has(key)||r.date<first.get(key))first.set(key,r.date);}
|
| 473 |
return first;
|
| 474 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
function periodContext(key){
|
| 476 |
-
const p=
|
| 477 |
-
const
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
const peildatum=
|
|
|
|
| 481 |
return {...p,peildatum,activeFrom:precedingThreeMonths(peildatum)};
|
| 482 |
}
|
| 483 |
function calculateImported(site='*'){
|
|
@@ -555,9 +589,9 @@ function independentlyVerify(result){
|
|
| 555 |
{label:'Nieuwe instroom · eerste scan',ours:result.newIntake,shadow:shadow.newIntake},
|
| 556 |
{label:'Actieve cliënten · laatste 3 maanden',ours:result.active,shadow:shadow.active},
|
| 557 |
{label:'Unieke bezoekdagen · cliënt/locatie/dag',ours:result.visits,shadow:shadow.visits},
|
| 558 |
-
{label:'Bezoekers
|
| 559 |
-
{label:'Herhaalde scans in
|
| 560 |
-
{label:'Maandinstroom versus
|
| 561 |
];
|
| 562 |
const mismatches=checks.filter(c=>c.ours!==c.shadow).length;
|
| 563 |
const sourceIssues=[
|
|
@@ -578,10 +612,10 @@ function drawImportedChart(result){
|
|
| 578 |
const colors=['#3ee1b6','#ffc66a','#5b9cff','#ad88ff'];
|
| 579 |
series.forEach((loc,li)=>{
|
| 580 |
ctx.strokeStyle=colors[li];ctx.lineWidth=3;ctx.beginPath();
|
| 581 |
-
loc.visits.forEach((v,j)=>{const x=
|
| 582 |
-
loc.visits.forEach((v,j)=>{const x=
|
| 583 |
});ctx.fillStyle='#a6b9d0';ctx.textAlign='center';
|
| 584 |
-
result.months.forEach((m,i)=>ctx.fillText(m,
|
| 585 |
const legend=document.querySelector('.legend');
|
| 586 |
legend.innerHTML=series.map((loc,i)=>`<span><i style="background:${colors[i]}"></i> ${escapeHTML(loc.name)}</span>`).join('');
|
| 587 |
document.querySelector('.chart-panel .subtle').textContent=result.months.join(' · ')+(result.locations.length>4?' · 4 van '+result.locations.length+' locaties':'');
|
|
@@ -600,9 +634,9 @@ function renderImported(result){
|
|
| 600 |
['Cliënten ooit (per locatie)',num(result.unique)],
|
| 601 |
['Nieuwe instroom (per locatie)',num(result.newIntake)],
|
| 602 |
['Actieve cliënten (per locatie)',num(result.active)],
|
| 603 |
-
['Bezoekers binnen
|
| 604 |
-
['Unieke bezoekdagen
|
| 605 |
-
['Herhaalde scans
|
| 606 |
['Scanpassen: apart werkblad',result.passSheetFound?num(result.passRows)+' registraties':'NIET AANWEZIG'],
|
| 607 |
['Cliënten met meerdere passen',result.passChecked?num(result.multiPassClients):'NIET TE CONTROLEREN'],
|
| 608 |
['Scanregels via pas gekoppeld (hele bestand)',num(result.linkedByPass)],
|
|
@@ -618,7 +652,7 @@ function renderImported(result){
|
|
| 618 |
document.querySelector('.verification .warning').textContent=quality.mismatches?`⚠ ${quality.mismatches} interne telverschillen gevonden. Cijfers nader onderzoeken. NIET GEVALIDEERD.`:
|
| 619 |
quality.sourceIssues.length?`⚠ Tweede telling sluit aan, maar ${quality.sourceIssues.length} soorten bronproblemen gevonden. NIET GEVALIDEERD.`:
|
| 620 |
'✓ 7 interne telcontroles sluiten aan. Let op: volledigheid van historische scans onbekend; externe businesscontrollercontrole ontbreekt. NIET EXTERN GEVALIDEERD.';
|
| 621 |
-
document.querySelector('.paper-watermark').textContent='
|
| 622 |
document.querySelector('.paper p').textContent=result.period.label+' · lokale Excel-proef · niet voor publicatie';
|
| 623 |
const pk=document.querySelectorAll('.paper-kpis strong');[num(result.visits),num(result.unique),num(result.newIntake)].forEach((x,i)=>{if(pk[i])pk[i].textContent=x});
|
| 624 |
const pl=document.querySelectorAll('.paper-kpis small');if(pl[2])pl[2].textContent='Eerste scan in '+result.period.label;
|
|
@@ -657,18 +691,19 @@ $('scanFile').addEventListener('change',async e=>{
|
|
| 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;
|
| 660 |
-
const
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 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';}});
|
| 671 |
-
$('periodSelect').addEventListener('change',()=>{if(importedData){importedData.last=null;resetControllerComparison(true);updateControllerContext();$('flowState').textContent='
|
|
|
|
| 672 |
// Bestaande chat blijft demo-gericht. Voor echte Excel-proef antwoorden we uitsluitend met controleerbare berekende cijfers.
|
| 673 |
const originalReply=reply;
|
| 674 |
reply=function(q){
|
|
@@ -676,7 +711,7 @@ reply=function(q){
|
|
| 676 |
const r=importedData.last;
|
| 677 |
if(!r)return 'Klik op Start demo-analyse om de lokaal ingelezen testgegevens te berekenen.';
|
| 678 |
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.';
|
| 679 |
-
if(/bezoek|uniek|scan|groei|locatie|samenvat|rapport|analyse|kwartaal|cijfer|instroom|actief/.test(q.toLowerCase()))return `
|
| 680 |
return 'De AI-API is nog niet gekoppeld. Ik kan voorlopig alleen de gecontroleerde basisgetallen van het fictieve Excel-testbestand toelichten.';
|
| 681 |
};
|
| 682 |
|
|
@@ -768,10 +803,10 @@ function buildWhiteReport(){
|
|
| 768 |
<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>
|
| 769 |
<section class="report-panel"><h2>Managementanalyse</h2><div class="report-analysis">${summary.map(t=>`<p>${t}</p>`).join('')}</div></section>
|
| 770 |
</div>
|
| 771 |
-
<div class="report-foot"><span>Bron:
|
| 772 |
}
|
| 773 |
function openWhiteReport(){
|
| 774 |
-
if(!importedData?.last){alert('Upload eerst
|
| 775 |
if(importedData.last.site==='*'){alert('Kies eerst één locatie en voer daarna de analyse opnieuw uit. De witte monitor is in v0.8 een rapport per locatie.');return;}
|
| 776 |
buildWhiteReport();$('reportOverlay').classList.add('open');$('reportOverlay').setAttribute('aria-hidden','false');document.body.classList.add('report-open');$('reportOverlay').scrollTop=0;
|
| 777 |
}
|
|
@@ -831,7 +866,7 @@ function resetControllerComparison(clearInputs=false){
|
|
| 831 |
}
|
| 832 |
function updateControllerContext(){
|
| 833 |
const r=importedData?.last;
|
| 834 |
-
$('controllerContext').textContent=r?`ORCHESTRA: ${r.site==='*'?'Alle locaties (som van locaties)':r.site} · ${r.period.label} · peildatum ${readableDate(r.period.peildatum)}. Gebruik precies deze selectie voor de controle.`:'Analyseer eerst het
|
| 835 |
}
|
| 836 |
function compareController(){
|
| 837 |
const r=importedData?.last;
|
|
|
|
| 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}
|
| 28 |
.import-file-label span{color:#53d9bf;font-size:16px;margin-right:7px}
|
| 29 |
#scanFile{display:none}.site-label{font-size:11px;color:#a6bad8}#siteSelect{background:#132947;border:1px solid #325172;border-radius:8px;color:#eaf2ff;max-width:185px;padding:9px;font:inherit;font-size:12px}
|
| 30 |
+
#siteSelect:disabled{opacity:.55}#periodType,#periodSelect{background:#132947;border:1px solid #325172;border-radius:8px;color:#eaf2ff;padding:9px;font:inherit;font-size:12px;max-width:138px}#periodType:disabled,#periodSelect:disabled{opacity:.55}.import-state{color:#78e0c3;font-size:11px;max-width:210px;line-height:1.4}
|
| 31 |
@media(max-width:760px){.import-panel{align-items:start}.import-intro{flex-basis:100%}.import-state{flex-basis:100%;max-width:none}}
|
| 32 |
|
| 33 |
/* v0.5: agent-workflow-inzicht, zonder de goedgekeurde basisvormgeving te wijzigen */
|
|
|
|
| 166 |
<body>
|
| 167 |
<div class="app">
|
| 168 |
<main class="main">
|
| 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> LOKALE ANALYSE</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.4 · FLEXIBELE RAPPORTAGES</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="periodType">Rapportageperiode</label><select id="periodType" disabled aria-label="Soort rapportage"><option value="quarter">Kwartaalrapportage</option><option value="half">Halfjaarrapportage</option><option value="nine">Negenmaandsrapportage</option></select><select id="periodSelect" disabled aria-label="Geselecteerde rapportageperiode"><option>—</option></select>
|
| 176 |
<span id="importState" class="import-state">Demo staat klaar</span>
|
| 177 |
</section>
|
| 178 |
<section class="orchestra panel">
|
|
|
|
| 246 |
clock();setInterval(clock,60000);
|
| 247 |
function renderKPIs(data){$('kpis').innerHTML=data.kpis.map(k=>`<div class="kpi"><small>${escapeHTML(k.label)}</small><strong>${escapeHTML(k.value)}</strong><span>${escapeHTML(k.change)}</span></div>`).join('');}
|
| 248 |
function renderChecks(data){$('checks').innerHTML=data.checks.map(c=>{const delta=c.ours-c.shadow;return `<div class="check-row"><span>${escapeHTML(c.label)}</span><span>${c.ours.toLocaleString('nl-NL')} / ${c.shadow.toLocaleString('nl-NL')} <strong class="${delta?'bad':''}">${delta?'⚠ '+(delta>0?'+':'')+delta:'✓ 0'}</strong></span></div>`}).join('');}
|
| 249 |
+
function drawChart(data){const canvas=$('chart'),ctx=canvas.getContext('2d'),w=640,h=250;ctx.clearRect(0,0,w,h);ctx.font='13px Segoe UI';ctx.textAlign='left';const max=500;for(let i=0;i<=4;i++){let y=25+i*45;ctx.strokeStyle='#29405a';ctx.beginPath();ctx.moveTo(44,y);ctx.lineTo(w-15,y);ctx.stroke();ctx.fillStyle='#8298b4';ctx.fillText(String(Math.round(max*(4-i)/4)),5,y+4);}const cols=['#3ee1b6','#ffc66a','#5b9cff','#ad88ff'];data.locations.forEach((loc,index)=>{ctx.strokeStyle=cols[index];ctx.lineWidth=3;ctx.beginPath();loc.visits.forEach((v,j)=>{const x=72+(result.months.length===1?0:j*(w-105)/(result.months.length-1)),y=205-(v/max)*180;if(j===0)ctx.moveTo(x,y);else ctx.lineTo(x,y)});ctx.stroke();loc.visits.forEach((v,j)=>{const x=72+(result.months.length===1?0:j*(w-105)/(result.months.length-1)),y=205-(v/max)*180;ctx.fillStyle=cols[index];ctx.beginPath();ctx.arc(x,y,4,0,Math.PI*2);ctx.fill()})});ctx.textAlign='center';ctx.fillStyle='#a6b9d0';['JUL','AUG','SEP'].forEach((m,i)=>ctx.fillText(m,72+(result.months.length===1?0:i*(w-105)/(result.months.length-1)),239));}
|
| 250 |
function log(text){const now=new Date().toLocaleTimeString('nl-NL',{hour:'2-digit',minute:'2-digit',second:'2-digit'});if($('logs').querySelector('.muted'))$('logs').innerHTML='';$('logs').insertAdjacentHTML('afterbegin',`<div class="logitem"><time>${now}</time>${escapeHTML(text)}</div>`)}
|
| 251 |
// De lichtpunten volgen de vijf bestaande SVG-verbindingen. Ze lopen alleen tijdens een actieve processtap.
|
| 252 |
const signalPaths={data:'M500 0 V30 H100 V90',analysis:'M500 0 V30 H300 V90',insight:'M500 0 V90',verify:'M500 0 V30 H700 V90',report:'M500 0 V30 H900 V90'};
|
|
|
|
| 447 |
passConflicts:passConflicts.size,identityConflicts:identityConflicts.size,
|
| 448 |
unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass};
|
| 449 |
}
|
| 450 |
+
// v1.4 — kwartaal-, halfjaar- en negenmaandsrapportage; historie t/m peildatum.
|
| 451 |
function quarterKey(date){return date.slice(0,4)+'-Q'+(Math.floor((Number(date.slice(5,7))-1)/3)+1);}
|
| 452 |
function quarterDates(key){
|
| 453 |
const m=String(key).match(/^(\d{4})-Q([1-4])$/);if(!m)throw Error('Kies een geldig rapportagekwartaal');
|
|
|
|
| 472 |
for(const r of all){const key=JSON.stringify([r.site,r.id]);if(!first.has(key)||r.date<first.get(key))first.set(key,r.date);}
|
| 473 |
return first;
|
| 474 |
}
|
| 475 |
+
function reportPeriodDates(key){
|
| 476 |
+
const match=String(key).match(/^(\d{4})-(Q[1-4]|H[12]|M9)$/);
|
| 477 |
+
if(!match)throw Error('Ongeldige rapportageperiode');
|
| 478 |
+
const year=Number(match[1]),part=match[2];
|
| 479 |
+
const first=part.startsWith('Q')?(Number(part[1])-1)*3+1:part==='H2'?7:1;
|
| 480 |
+
const count=part.startsWith('Q')?3:part.startsWith('H')?6:9;
|
| 481 |
+
const begin=`${year}-${String(first).padStart(2,'0')}-01`;
|
| 482 |
+
const end=new Date(Date.UTC(year,first+count-1,0)).toISOString().slice(0,10);
|
| 483 |
+
const months=Array.from({length:count},(_,i)=>`${year}-${String(first+i).padStart(2,'0')}`);
|
| 484 |
+
const label=part.startsWith('Q')?`Q${part[1]} ${year}`:part.startsWith('H')?`H${part[1]} ${year}`:`Januari t/m september ${year}`;
|
| 485 |
+
return {key,begin,end,months,label,type:part.startsWith('Q')?'quarter':part.startsWith('H')?'half':'nine'};
|
| 486 |
+
}
|
| 487 |
+
function availablePeriods(type){
|
| 488 |
+
const dataMonths=[...new Set(importedData.all.map(r=>r.date.slice(0,7)))].sort();
|
| 489 |
+
const years=[...new Set(dataMonths.map(x=>x.slice(0,4)))].sort().reverse();
|
| 490 |
+
const keys=[];
|
| 491 |
+
for(const year of years){
|
| 492 |
+
const suffixes=type==='quarter'?['Q4','Q3','Q2','Q1']:type==='half'?['H2','H1']:['M9'];
|
| 493 |
+
for(const suffix of suffixes){
|
| 494 |
+
const key=year+'-'+suffix,p=reportPeriodDates(key);
|
| 495 |
+
if(dataMonths.some(month=>month>=p.months[0]&&month<=p.months.at(-1)))keys.push(key);
|
| 496 |
+
}
|
| 497 |
+
}
|
| 498 |
+
return keys;
|
| 499 |
+
}
|
| 500 |
+
function refreshPeriodChoices(prefer){
|
| 501 |
+
const select=$('periodSelect'),keys=availablePeriods($('periodType').value);
|
| 502 |
+
select.replaceChildren();
|
| 503 |
+
keys.forEach(key=>{const opt=document.createElement('option');opt.value=key;opt.textContent=reportPeriodDates(key).label;select.appendChild(opt)});
|
| 504 |
+
if(prefer&&keys.includes(prefer))select.value=prefer;
|
| 505 |
+
select.disabled=!keys.length;
|
| 506 |
+
if(!keys.length){const opt=document.createElement('option');opt.textContent='Geen perioden beschikbaar';select.appendChild(opt)}
|
| 507 |
+
}
|
| 508 |
function periodContext(key){
|
| 509 |
+
const p=reportPeriodDates(key);
|
| 510 |
+
const seen=importedData.all.reduce((max,r)=>r.date>max?r.date:max,'');
|
| 511 |
+
// Onvolledig nieuwste tijdvak: laatste beschikbare scandag is de peildatum.
|
| 512 |
+
const latestInPeriod=importedData.all.reduce((max,r)=>r.date>=p.begin&&r.date<=p.end&&r.date>max?r.date:max,'');
|
| 513 |
+
const peildatum=seen<p.end?latestInPeriod:p.end;
|
| 514 |
+
if(!peildatum)throw Error('Geen scans in de geselecteerde rapportageperiode');
|
| 515 |
return {...p,peildatum,activeFrom:precedingThreeMonths(peildatum)};
|
| 516 |
}
|
| 517 |
function calculateImported(site='*'){
|
|
|
|
| 589 |
{label:'Nieuwe instroom · eerste scan',ours:result.newIntake,shadow:shadow.newIntake},
|
| 590 |
{label:'Actieve cliënten · laatste 3 maanden',ours:result.active,shadow:shadow.active},
|
| 591 |
{label:'Unieke bezoekdagen · cliënt/locatie/dag',ours:result.visits,shadow:shadow.visits},
|
| 592 |
+
{label:'Bezoekers rapportageperiode',ours:result.quarterClients,shadow:shadow.quarterClients},
|
| 593 |
+
{label:'Herhaalde scans in periode',ours:result.duplicates,shadow:shadow.duplicates},
|
| 594 |
+
{label:'Maandinstroom versus periode-instroom',ours:result.newIntake,shadow:shadow.monthNew}
|
| 595 |
];
|
| 596 |
const mismatches=checks.filter(c=>c.ours!==c.shadow).length;
|
| 597 |
const sourceIssues=[
|
|
|
|
| 612 |
const colors=['#3ee1b6','#ffc66a','#5b9cff','#ad88ff'];
|
| 613 |
series.forEach((loc,li)=>{
|
| 614 |
ctx.strokeStyle=colors[li];ctx.lineWidth=3;ctx.beginPath();
|
| 615 |
+
loc.visits.forEach((v,j)=>{const x=72+(result.months.length===1?0:j*(w-105)/(result.months.length-1)),y=205-(v/max)*180;j===0?ctx.moveTo(x,y):ctx.lineTo(x,y)});ctx.stroke();
|
| 616 |
+
loc.visits.forEach((v,j)=>{const x=72+(result.months.length===1?0:j*(w-105)/(result.months.length-1)),y=205-(v/max)*180;ctx.fillStyle=colors[li];ctx.beginPath();ctx.arc(x,y,4,0,Math.PI*2);ctx.fill()});
|
| 617 |
});ctx.fillStyle='#a6b9d0';ctx.textAlign='center';
|
| 618 |
+
result.months.forEach((m,i)=>ctx.fillText(m,72+(result.months.length===1?0:i*(w-105)/(result.months.length-1)),239));
|
| 619 |
const legend=document.querySelector('.legend');
|
| 620 |
legend.innerHTML=series.map((loc,i)=>`<span><i style="background:${colors[i]}"></i> ${escapeHTML(loc.name)}</span>`).join('');
|
| 621 |
document.querySelector('.chart-panel .subtle').textContent=result.months.join(' · ')+(result.locations.length>4?' · 4 van '+result.locations.length+' locaties':'');
|
|
|
|
| 634 |
['Cliënten ooit (per locatie)',num(result.unique)],
|
| 635 |
['Nieuwe instroom (per locatie)',num(result.newIntake)],
|
| 636 |
['Actieve cliënten (per locatie)',num(result.active)],
|
| 637 |
+
['Bezoekers binnen periode',num(result.quarterClients)],
|
| 638 |
+
['Unieke bezoekdagen periode',num(result.visits)],
|
| 639 |
+
['Herhaalde scans periode',num(result.duplicates)],
|
| 640 |
['Scanpassen: apart werkblad',result.passSheetFound?num(result.passRows)+' registraties':'NIET AANWEZIG'],
|
| 641 |
['Cliënten met meerdere passen',result.passChecked?num(result.multiPassClients):'NIET TE CONTROLEREN'],
|
| 642 |
['Scanregels via pas gekoppeld (hele bestand)',num(result.linkedByPass)],
|
|
|
|
| 652 |
document.querySelector('.verification .warning').textContent=quality.mismatches?`⚠ ${quality.mismatches} interne telverschillen gevonden. Cijfers nader onderzoeken. NIET GEVALIDEERD.`:
|
| 653 |
quality.sourceIssues.length?`⚠ Tweede telling sluit aan, maar ${quality.sourceIssues.length} soorten bronproblemen gevonden. NIET GEVALIDEERD.`:
|
| 654 |
'✓ 7 interne telcontroles sluiten aan. Let op: volledigheid van historische scans onbekend; externe businesscontrollercontrole ontbreekt. NIET EXTERN GEVALIDEERD.';
|
| 655 |
+
document.querySelector('.paper-watermark').textContent='LOKALE EXCEL-ANALYSE · NIET EXTERN GEVALIDEERD';
|
| 656 |
document.querySelector('.paper p').textContent=result.period.label+' · lokale Excel-proef · niet voor publicatie';
|
| 657 |
const pk=document.querySelectorAll('.paper-kpis strong');[num(result.visits),num(result.unique),num(result.newIntake)].forEach((x,i)=>{if(pk[i])pk[i].textContent=x});
|
| 658 |
const pl=document.querySelectorAll('.paper-kpis small');if(pl[2])pl[2].textContent='Eerste scan in '+result.period.label;
|
|
|
|
| 691 |
const sel=$('siteSelect');sel.innerHTML='<option value="*">Alle locaties</option>';
|
| 692 |
importedData.sites.forEach(loc=>{const opt=document.createElement('option');opt.value=loc;opt.textContent=loc;sel.appendChild(opt)});
|
| 693 |
sel.disabled=false;
|
| 694 |
+
const type=$('periodType');
|
| 695 |
+
type.disabled=false;
|
| 696 |
+
type.value=availablePeriods('nine').length?'nine':'quarter';
|
| 697 |
+
refreshPeriodChoices();
|
| 698 |
state.textContent=`${importedData.rawCount.toLocaleString('nl-NL')} scanregels · ${importedData.sites.length} locaties · ${importSeconds} sec inleestijd · Scanpassen ${importedData.passSheetFound?importedData.passRows+' regels':'ontbreekt'}`;
|
| 699 |
$('startBtn').textContent='▶ Analyseer Excelbestand';
|
| 700 |
$('flowState').textContent='Excelbestand lokaal ingelezen · start de analyse';
|
| 701 |
log('Excelbestand lokaal verwerkt; analyse moet nog starten. Voor echte data eerst interne toestemming controleren.');
|
| 702 |
+
}catch(err){$('startBtn').textContent='▶ Start demo-analyse';state.textContent='⚠ '+err.message;$('siteSelect').disabled=true;$('periodSelect').disabled=true;$('periodType').disabled=true;log('Excel-proef: '+err.message)}finally{$('startBtn').disabled=false}
|
| 703 |
});
|
| 704 |
$('siteSelect').addEventListener('change',()=>{if(importedData){importedData.last=null;resetControllerComparison(true);updateControllerContext();$('flowState').textContent='Locatie gewijzigd · analyse opnieuw starten';}});
|
| 705 |
+
$('periodSelect').addEventListener('change',()=>{if(importedData){importedData.last=null;resetControllerComparison(true);updateControllerContext();$('flowState').textContent='Rapportageperiode gewijzigd · analyse opnieuw starten';}});
|
| 706 |
+
$('periodType').addEventListener('change',()=>{if(importedData){refreshPeriodChoices();importedData.last=null;resetControllerComparison(true);updateControllerContext();$('flowState').textContent='Rapportagesoort gewijzigd · analyse opnieuw starten';}});
|
| 707 |
// Bestaande chat blijft demo-gericht. Voor echte Excel-proef antwoorden we uitsluitend met controleerbare berekende cijfers.
|
| 708 |
const originalReply=reply;
|
| 709 |
reply=function(q){
|
|
|
|
| 711 |
const r=importedData.last;
|
| 712 |
if(!r)return 'Klik op Start demo-analyse om de lokaal ingelezen testgegevens te berekenen.';
|
| 713 |
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.';
|
| 714 |
+
if(/bezoek|uniek|scan|groei|locatie|samenvat|rapport|analyse|kwartaal|cijfer|instroom|actief/.test(q.toLowerCase()))return `Lokale Excel-analyse (${r.site==='*'?'som locaties':r.site}, ${r.period.label}, t/m ${readableDate(r.peildatum)}): ${r.unique} unieke cliënten in de beschikbare historie, ${r.newIntake} nieuwe instromers, ${r.active} actieve cliënten in de afgelopen drie maanden en ${r.visits} unieke bezoekdagen in de geselecteerde periode. ${r.duplicates} herhaalde scans zijn uitgesloten. Deze cijfers zijn nog niet extern gevalideerd en hangen af van een volledige beschikbare scanhistorie.`;
|
| 715 |
return 'De AI-API is nog niet gekoppeld. Ik kan voorlopig alleen de gecontroleerde basisgetallen van het fictieve Excel-testbestand toelichten.';
|
| 716 |
};
|
| 717 |
|
|
|
|
| 803 |
<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>
|
| 804 |
<section class="report-panel"><h2>Managementanalyse</h2><div class="report-analysis">${summary.map(t=>`<p>${t}</p>`).join('')}</div></section>
|
| 805 |
</div>
|
| 806 |
+
<div class="report-foot"><span>Bron: Lokaal ingelezen scanregistratie · Unieke cliënten uit historie t/m peildatum; bezoeken en instroom uit geselecteerde periode · Historie beperkt tot bestand</span><span class="report-provisional">NIET EXTERN GEVALIDEERD · CONTROLEER VÓÓR PUBLICATIE</span></div>`;
|
| 807 |
}
|
| 808 |
function openWhiteReport(){
|
| 809 |
+
if(!importedData?.last){alert('Upload eerst een Excelbestand en rond de analyse af. Deze witte rapportage gebruikt geen verzonnen demo-KPI’s.');return;}
|
| 810 |
if(importedData.last.site==='*'){alert('Kies eerst één locatie en voer daarna de analyse opnieuw uit. De witte monitor is in v0.8 een rapport per locatie.');return;}
|
| 811 |
buildWhiteReport();$('reportOverlay').classList.add('open');$('reportOverlay').setAttribute('aria-hidden','false');document.body.classList.add('report-open');$('reportOverlay').scrollTop=0;
|
| 812 |
}
|
|
|
|
| 866 |
}
|
| 867 |
function updateControllerContext(){
|
| 868 |
const r=importedData?.last;
|
| 869 |
+
$('controllerContext').textContent=r?`ORCHESTRA: ${r.site==='*'?'Alle locaties (som van locaties)':r.site} · ${r.period.label} · peildatum ${readableDate(r.period.peildatum)}. Gebruik precies deze selectie voor de controle.`:'Analyseer eerst het Excelbestand en kies een locatie en rapportageperiode.';
|
| 870 |
}
|
| 871 |
function compareController(){
|
| 872 |
const r=importedData?.last;
|