Marcel0123's picture
Upload index.html
e2a7636 verified
Raw History Blame Contribute Delete
161 kB
<!doctype html>
<html lang="nl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Reakt | ORCHESTRA</title><style>
:root{font-family:Inter,'Segoe UI',Arial,sans-serif;color:#eaf2ff;background:#060d1d;font-synthesis:none}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 50% -15%,#162d58 0%,#081224 43%,#060b18 100%);min-height:100vh}button,input{font:inherit}button{cursor:pointer}button:disabled{opacity:.5;cursor:wait}.app{display:flex;min-height:100vh}.sidebar{width:225px;flex:none;border-right:1px solid #223351;background:#091326;padding:25px 16px;position:sticky;top:0;height:100vh}.brand{display:flex;align-items:center;gap:12px;margin-bottom:55px}.brand-icon{font-size:34px;color:#54d9ff;text-shadow:0 0 20px #38baff}.brand strong{font-size:24px;letter-spacing:1.5px}.brand-dot,.paper-top span{color:#58d8f5}.brand small{display:block;font-size:9px;letter-spacing:1.8px;color:#7791b5;margin-top:3px}.nav-label{font-size:10px;letter-spacing:1.5px;color:#607a9e;margin:0 8px 15px}.nav-label.second{margin-top:42px}.nav{display:flex;gap:13px;align-items:center;padding:13px 12px;color:#8fa6c7;font-size:13px;border-radius:9px;margin-bottom:5px}.nav.active{color:#c5ecff;background:linear-gradient(90deg,#153d68,#102039);border:1px solid #24517c;box-shadow:inset 3px 0 #4fc9ff}.side-note{border:1px solid #244568;border-radius:10px;background:#0d2137;padding:15px;color:#c5e2f7;font-size:12px;line-height:2}.side-note small{color:#839dbb}.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:#8194ad;margin-right:7px}.dot.green{background:#32e3aa;box-shadow:0 0 10px #32e3aa}.sidebar-bottom{position:absolute;bottom:26px;font-size:12px;color:#8ba3c2;line-height:1.9}.sidebar-bottom span{color:#40cbfb}.sidebar-bottom small{font-size:10px}.main{min-width:0;flex:1;max-width:1900px;padding:24px 30px;margin:auto}header{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:18px;align-items:center;padding-bottom:23px;border-bottom:1px solid #1c3451}.header-copy{min-width:0}.header-center-logo{display:flex;justify-content:center;align-items:center;padding:0 8px}.header-center-logo img{display:block;height:74px;width:auto;filter:drop-shadow(0 6px 16px rgba(0,0,0,.18))}.eyebrow,.section-kicker{font-size:10px;letter-spacing:2px;color:#56b8e9;font-weight:700}h1{font-size:29px;letter-spacing:-.7px;margin:8px 0 5px;font-weight:800}h1 span{color:#75b8ff}h1 em{font-size:10px;font-style:normal;border:1px solid #674cb5;border-radius:5px;color:#bda7ff;padding:5px 8px;letter-spacing:1px;vertical-align:middle;margin-left:8px}header p{margin:0;color:#8299b8;font-size:12px}.header-actions{display:flex;align-items:center;gap:20px;justify-self:end}.pill{border:1px solid #23866c;color:#7aefd0;background:#0b2e2c;border-radius:30px;padding:9px 14px;font-size:10px;font-weight:700;letter-spacing:1px}.pill i{display:inline-block;width:6px;height:6px;background:#54f6bc;border-radius:50%;margin-right:7px}.clock{color:#879dbc;font-size:12px}.topbar{display:flex;justify-content:space-between;align-items:end;margin:23px 0 13px}.topbar h2{font-size:20px;margin:6px 0 0}.live-dot{display:inline-block;background:#34dca7;width:7px;height:7px;border-radius:50%;vertical-align:middle;box-shadow:0 0 13px #24e6a7}.actions{display:flex;gap:10px}button{border:0;border-radius:8px;padding:11px 15px;color:#e9f4ff;background:#172c48;font-size:12px;font-weight:700}.ghost{border:1px solid #2c4568}.primary{background:linear-gradient(110deg,#1369d9,#6b47d4);box-shadow:0 5px 25px #235be355}.panel{border:1px solid #223a5a;background:linear-gradient(150deg,#101e35ee,#0b1629ee);border-radius:13px;box-shadow:0 10px 35px #0003}.orchestra{position:relative;overflow:hidden;padding:18px 20px 0;isolation:isolate}.mesh{position:absolute;inset:0;z-index:-1;background-image:linear-gradient(#28507914 1px,transparent 1px),linear-gradient(90deg,#28507914 1px,transparent 1px);background-size:24px 24px;mask-image:linear-gradient(to bottom,transparent,#000 20%,#000 80%,transparent)}.conductor-wrap{display:flex;justify-content:center}.conductor{width:285px;display:flex;align-items:center;gap:14px;padding:13px 17px;border:1px solid #8054df;border-radius:13px;background:linear-gradient(110deg,#2d1e57,#161d43);box-shadow:0 0 26px #864de94a, inset 0 0 20px #a063ff17;position:relative}.conductor-icon{width:46px;height:46px;display:grid;place-items:center;border-radius:50%;border:1px solid #9a73ef;color:#ddc5ff;font-size:29px;text-shadow:0 0 15px #b79bff;box-shadow:0 0 20px #a77aff44}.conductor small{display:block;color:#ae9bd7;font-size:9px;letter-spacing:1.5px}.conductor strong{display:block;font-size:17px;margin:4px 0;font-weight:750}.conductor span{font-size:12px;color:#cbbce9}.conductor-indicator{position:absolute;right:14px;top:14px;width:7px;height:7px;border-radius:50%;background:#8e7bad}.conductor.running{animation:conductorPulse 1.5s infinite}.conductor.running .conductor-indicator{background:#c5a2ff;box-shadow:0 0 12px #d1a9ff}.connections{display:block;width:100%;height:90px;overflow:visible}.connections path{fill:none;stroke:#38527a;stroke-width:1.5;vector-effect:non-scaling-stroke}.connections .flowline{stroke:#a77bff;stroke-width:2;stroke-dasharray:7 20;opacity:.2}.orchestra.running .flowline{opacity:1;animation:dash 1.2s linear infinite;filter:drop-shadow(0 0 5px #b38cff)}.agents{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;position:relative}.agent{min-width:0;border:1px solid color-mix(in srgb,var(--accent) 55%,#1d3150);background:linear-gradient(150deg,#142841,#0a182c);border-radius:12px;padding:13px 12px 11px;box-shadow:inset 0 0 25px color-mix(in srgb,var(--accent) 6%,transparent);transition:box-shadow .3s,transform .3s,border-color .3s}.agent.active{transform:translateY(-5px);border-color:var(--accent);box-shadow:0 0 24px color-mix(in srgb,var(--accent) 43%,transparent),inset 0 0 22px color-mix(in srgb,var(--accent) 12%,transparent)}.agent.done{border-color:var(--accent)}.agent-top{display:flex;justify-content:space-between;gap:4px;align-items:center;color:var(--accent);font-size:9px;letter-spacing:1px}.agent .status{font-size:8px;color:#8499b6;letter-spacing:0}.agent.active .status{color:var(--accent)}.agent-symbol{font-size:27px;color:var(--accent);margin-top:13px;text-shadow:0 0 17px var(--accent)}.agent h3{font-size:17px;line-height:1.22;font-weight:750;letter-spacing:.1px;margin:9px 0 5px}.agent p{font-size:13px;line-height:1.35;color:#c2d2e8;margin:0;font-weight:500}.mini-visual{height:42px;margin:13px 0;color:var(--accent);font-size:24px;display:flex;align-items:center;justify-content:center}.mini-visual.bars{align-items:end;gap:6px;padding-bottom:6px}.bars b{background:var(--accent);opacity:.8;width:9px;border-radius:2px 2px 0 0;height:45%}.bars b:nth-child(2){height:80%}.bars b:nth-child(3){height:55%}.bars b:nth-child(4){height:95%}.bars b:nth-child(5){height:65%}.bars b:nth-child(6){height:75%}.bars b:nth-child(7){height:45%}.spark{font-size:27px;letter-spacing:-2px}.rings{font-size:22px;gap:9px}.rings span{font-size:29px}.checkicons{font-size:19px}.reportlines{font-size:13px;line-height:1.1;letter-spacing:3px}.agent-bottom{display:flex;align-items:center;gap:7px}.track{height:5px;flex:1;background:#26344e;border-radius:20px;overflow:hidden}.track i{display:block;height:100%;width:0;background:var(--accent);box-shadow:0 0 9px var(--accent);transition:width .13s linear}.percent{font-size:9px;color:#a3b3cc;width:25px;text-align:right}.orchestra-footer{display:flex;justify-content:space-between;gap:10px;border-top:1px solid #203550;margin-top:20px;padding:13px 0;color:#849bb8;font-size:10px}.content-grid{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(295px,.75fr);gap:14px;margin-top:15px}.left-content,.right-content{display:flex;flex-direction:column;gap:14px;min-width:0}.kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:11px}.kpi{padding:15px;border:1px solid #2b4160;background:linear-gradient(135deg,#11223b,#0c192d);border-radius:10px}.kpi small{display:block;font-size:10px;color:#829bbb}.kpi strong{display:block;font-size:24px;margin:8px 0 3px}.kpi span{color:#55dcb5;font-size:10px}.kpi:last-child span{color:#ffba6b}.charts-grid{display:grid;grid-template-columns:1.15fr 1fr;gap:14px}.chart-panel,.verification,.logpanel,.chatpanel,.preview{padding:17px}.panel-heading{display:flex;align-items:start;justify-content:space-between;gap:10px;margin-bottom:12px}.panel-heading h3{font-size:14px;margin:5px 0}.subtle{font-size:10px;color:#8aa2c0}.chart-panel canvas{display:block;width:100%;height:165px}.legend{display:flex;gap:10px;flex-wrap:wrap;color:#9caec7;font-size:9px;margin-top:10px}.legend i{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:3px}.checks{display:flex;flex-direction:column;gap:9px}.check-row{display:flex;justify-content:space-between;font-size:10px;color:#aebed3;border-bottom:1px solid #223650;padding-bottom:7px;gap:5px}.check-row strong{color:#61e4b5}.check-row strong.bad{color:#ffb56b}.warning{background:#5b3d1e55;border:1px solid #9c6b37;border-radius:7px;padding:9px;margin-top:12px;color:#ffc48b;font-size:10px}.warn-label{font-size:9px;color:#ffb971;border:1px solid #7b5834;padding:5px 8px;border-radius:5px}.logpanel{min-height:145px}.logs{max-height:150px;overflow:auto;font-size:11px;color:#abc0d9}.logitem{padding:6px 0;border-bottom:1px solid #1d314b}.logitem time{color:#6985a7;margin-right:10px}.logitem.muted{color:#7287a4}.chatpanel{display:flex;flex-direction:column;min-height:425px}.chat-icon{color:#b2a0ff}.demo-badge{font-size:9px;border:1px solid #4a6384;color:#9eb5d3;padding:5px 7px;border-radius:6px}.chat-messages{flex:1;overflow:auto;max-height:275px;min-height:140px;display:flex;flex-direction:column;gap:12px}.message{padding:12px;border-radius:10px;font-size:11px;line-height:1.55}.message.assistant{background:#142c49;border:1px solid #264c74;align-self:flex-start}.message.user{background:#25385c;border:1px solid #3e5681;align-self:flex-end}.message small{color:#6ed0f2;font-size:9px;letter-spacing:1px}.message p{margin:6px 0 0}.quick-questions{display:flex;flex-wrap:wrap;gap:6px;margin:12px 0}.quick-questions button{font-size:9px;padding:7px;background:#152944;border:1px solid #315075}.chat-form{display:flex;gap:7px}.chat-form input{min-width:0;flex:1;background:#0b1b30;border:1px solid #2c4b6e;border-radius:8px;padding:12px;color:white;font-size:11px}.chat-form button{background:#275cba;font-size:16px;padding:7px 13px}.chat-disclaimer{font-size:9px;color:#7e91ac;margin-top:9px}.paper{background:#f3f7fd;color:#163354;border-radius:5px;padding:16px;min-height:225px;position:relative;overflow:hidden}.paper-top{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #0e618c;padding-bottom:7px}.paper-top strong{font-size:14px}.paper-top small{font-size:7px;color:#5b7490}.paper h4{font-size:18px;margin:14px 0 2px}.paper p{font-size:9px;color:#67809c;margin:0}.paper-kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:5px;margin:13px 0}.paper-kpis div{background:#e0ebf5;border-radius:4px;padding:7px}.paper-kpis strong{font-size:14px;display:block}.paper-kpis small{font-size:8px;color:#52728c}.paper-bars{height:43px;display:flex;align-items:end;gap:5px}.paper-bars i{flex:1;height:45%;background:#338bb9}.paper-bars i:nth-child(3n){height:80%;background:#f2aa65}.paper-bars i:nth-child(4n){height:65%}.paper-bars i:nth-child(5n){height:95%;background:#42b9a2}.paper-watermark{position:absolute;bottom:0;left:0;right:0;background:#a13c3c;color:white;text-align:center;padding:4px;font-size:9px;letter-spacing:1px}.export{width:100%;margin-top:10px;border:1px solid #345579;background:#183354}.export:disabled{opacity:.6}footer{display:flex;justify-content:space-between;gap:10px;padding:22px 0;color:#6e88a9;font-size:9px;letter-spacing:1px}@keyframes dash{to{stroke-dashoffset:-54}}@keyframes conductorPulse{50%{box-shadow:0 0 42px #9a6bffbb,inset 0 0 28px #a063ff33}}@media(max-width:1250px){.sidebar{width:185px}.main{padding:18px}.agents{gap:7px}.agent{padding:10px 8px}.agent h3{font-size:14px}.agent p{font-size:12px}.charts-grid{grid-template-columns:1fr}}@media(max-width:900px){.sidebar{display:none}.content-grid{grid-template-columns:1fr}.right-content{display:grid;grid-template-columns:1fr 1fr}.agents{grid-template-columns:repeat(5,minmax(0,1fr))}}@media(max-width:680px){.main{padding:12px}header{align-items:start}h1{font-size:21px}.header-actions .clock{display:none}.topbar{align-items:start;gap:10px}.topbar h2{font-size:16px}.actions{flex-direction:column}.agents{grid-template-columns:repeat(2,minmax(0,1fr))}.connections{display:none}.orchestra{padding:12px}.agent:last-child{grid-column:span 2}.kpi-grid{grid-template-columns:repeat(2,1fr)}.right-content{display:flex}.orchestra-footer{flex-wrap:wrap}.clock{display:none}footer span{display:none}}@media print{.sidebar,header,.topbar,.orchestra,.chatpanel,.logpanel,.chart-panel,.verification,.kpi-grid,footer,.export{display:none!important}.content-grid,.right-content{display:block}.preview{border:0;background:white}.paper{max-width:750px;min-height:800px;margin:auto}.paper-watermark{font-size:15px}.paper h4{font-size:40px}}
/* ORCHESTRA v0.3 — alleen de dirigent: subtiele activiteit en gestuurde lichtsignalen */
.conductor-icon {position:relative;isolation:isolate;flex:none;}
.conductor-icon::before,.conductor-icon::after {content:"";position:absolute;border-radius:50%;pointer-events:none;}
.conductor-icon::before {inset:-7px;border:2px solid transparent;border-top-color:#cfb0ff;border-right-color:#9367ff;filter:drop-shadow(0 0 5px #ab74ff);animation:orbitConductor 10s linear infinite;opacity:.9;}
.conductor-icon::after {inset:-3px;border:1px dashed #9470db;animation:orbitConductor 18s linear infinite reverse;opacity:.38;}
.conductor-icon .brain-symbol {width:30px;height:30px;display:block;position:relative;z-index:1;filter:drop-shadow(0 0 5px #ad82ff);animation:brainBreath 3.8s ease-in-out infinite;transform-origin:center;}
.conductor.running .conductor-icon::before {animation-duration:2.1s;border-top-color:#e6d3ff;border-right-color:#be98ff;filter:drop-shadow(0 0 9px #c99aff);}
.conductor.running .conductor-icon::after {animation-duration:5s;opacity:.75;}
.conductor.running .brain-symbol {animation-duration:1.7s;}
.conductor.attention .conductor-icon::before {border-top-color:#ffbc6d;border-right-color:#f59c4b;filter:drop-shadow(0 0 6px #ffb167);}
.conductor.attention .brain-symbol {color:#ffd3a0;filter:drop-shadow(0 0 7px #ffb167);}
.connections .signal-layer .signal-particle {stroke:none;filter:drop-shadow(0 0 4px #c69aff);}
.connections .signal-layer .signal-halo {stroke:none;opacity:.24;filter:drop-shadow(0 0 7px #b78cff);}
@keyframes orbitConductor {to{transform:rotate(360deg)}}
@keyframes brainBreath {0%,100% {opacity:.78;transform:scale(.94)}50% {opacity:1;transform:scale(1.08)}}
@media (prefers-reduced-motion:reduce) { .conductor-icon::before,.conductor-icon::after,.conductor-icon .brain-symbol {animation:none!important} .connections .signal-layer {display:none} }
/* v0.7 – lokale Excel-proef met Scanpassen; bestaande dashboardstijl bewust ongewijzigd */
.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)}
.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}
.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}
.import-file-label span{color:#53d9bf;font-size:16px;margin-right:7px}
#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}
#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}
@media(max-width:760px){.import-panel{align-items:start}.import-intro{flex-basis:100%}.import-state{flex-basis:100%;max-width:none}}
/* v0.5: agent-workflow-inzicht, zonder de goedgekeurde basisvormgeving te wijzigen */
.pace-label{display:flex;gap:7px;align-items:center;color:#a3b8d5;font-size:11px}
#paceSelect{max-width:175px;color:#e2f2ff;background:#102641;border:1px solid #36577d;border-radius:7px;padding:8px;font-size:11px}
.agent-steps{display:none;margin:11px 0 10px;min-height:116px;flex-direction:column;gap:5px}
.agent.has-steps .agent-steps{display:flex}
.agent.has-steps .mini-visual{display:none}
.agent-step{display:flex;gap:6px;align-items:flex-start;font-size:10px;line-height:1.35;color:#7288a7;min-height:17px}
.agent-step .step-mark{flex:none;display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;border:1px solid #526782;border-radius:50%;font-size:9px;color:#718cac;margin-top:1px}
.agent-step.complete{color:#d6efe8}.agent-step.complete .step-mark{border-color:#34e6b3;background:#0c624b;color:#fff}
.agent-step.current{color:#f1f7ff;font-weight:650}.agent-step.current .step-mark{border-color:var(--accent);color:var(--accent);box-shadow:0 0 7px var(--accent);animation:stepBlink 1s ease-in-out infinite}
.agent-step.skipped{color:#b6a9c9}.agent-step.skipped .step-mark{border-color:#b18aff;color:#b18aff}
@keyframes stepBlink{50%{opacity:.45}}
@media(max-width:1250px){.agent-step{font-size:9px}.pace-label{font-size:10px}}
@media(max-width:680px){.pace-label{flex-direction:column;align-items:start}.agent-steps{min-height:110px}}
@media(prefers-reduced-motion:reduce){.agent-step.current .step-mark{animation:none}}
/* V0.8: Witte vaste rapportlay-out, gescheiden van de ORCHESTRA-werkruimte. */
#reportOverlay{position:fixed;inset:0;background:#050b19eF;z-index:5000;overflow:auto;padding:22px;display:none}
#reportOverlay.open{display:block}body.report-open{overflow:hidden}
.report-actions{max-width:1510px;margin:0 auto 14px;display:flex;justify-content:space-between;gap:12px;align-items:center;color:#eaf2ff;font-size:14px}
.report-actions button{background:#1e497f;border:1px solid #4c7bad;padding:11px 18px}.report-actions .report-print{background:#146bd8}.report-actions .report-png{background:#17664f;border-color:#39967a}.report-actions .report-jpg{background:#7a4f12;border-color:#b07d34}
#reportSheet{width:min(100%,1510px);margin:auto;background:white;color:#0a164a;font-family:'Segoe UI',Arial,sans-serif;padding:25px 28px 17px;box-shadow:0 22px 65px #0009;line-height:1.24}
#reportSheet *{box-sizing:border-box}#reportSheet .report-brand{margin-bottom:11px}#reportSheet .report-logo{display:block;height:82px;width:auto;object-fit:contain}
.report-heading{display:flex;justify-content:space-between;align-items:center;gap:18px;margin-bottom:14px}.report-heading h1{font-size:32px;margin:0;color:#0b164d;letter-spacing:-1.1px}.report-heading p{font-size:16px;margin:3px 0 0;color:#29335f}
.report-meta{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;min-width:48%}.report-meta>div{border:1px solid #dce2ef;border-radius:7px;padding:10px 13px;display:flex;align-items:center;gap:11px;min-width:0}.report-meta .meta-icon{font-size:23px;color:#20116c}.report-meta strong{font-size:11px;display:block}.report-meta span{font-size:12px}
.report-kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:12px 0}.report-kpi{border:1px solid #e0e5ef;border-radius:8px;min-height:136px;padding:13px 12px;display:grid;grid-template-columns:54px 1fr;column-gap:12px;position:relative;overflow:hidden}.report-kpi .k-icon{width:52px;height:52px;border-radius:50%;display:grid;place-items:center;color:white;font-size:26px;font-weight:700}.report-kpi .k-label{font-size:14px;font-weight:750;margin-top:1px}.report-kpi .k-num{font-size:32px;font-weight:850;margin:7px 0 2px;color:#0b164d;white-space:nowrap}.report-kpi .k-sub{font-size:11px;color:#333f65;line-height:1.33}.report-kpi.purple .k-icon{background:linear-gradient(140deg,#863eec,#4c24bc)}.report-kpi.green .k-icon{background:linear-gradient(140deg,#50c52d,#168c24)}.report-kpi.orange .k-icon{background:linear-gradient(140deg,#fe8c20,#ff4d00)}.report-kpi.purple .k-label{color:#5b20d6}.report-kpi.green .k-label{color:#219824}.report-kpi.orange .k-label{color:#f45c0b}.report-kpi .k-underline{position:absolute;bottom:0;left:0;right:0;height:5px;background:linear-gradient(90deg,transparent,#ded6ff,transparent)}
.report-chart-grid{display:grid;grid-template-columns:1fr 1fr 1.13fr;gap:10px}.report-panel{border:1px solid #e1e5ef;border-radius:8px;padding:12px 14px;min-width:0;background:white}.report-panel h2{font-size:15px;margin:0 0 9px;color:#11176b}.report-panel h2 small{font-weight:400;font-size:11px}.report-panel .chart-box{height:172px;display:flex;align-items:stretch;min-width:0}
.report-chart-grid .report-panel{min-height:224px}.report-bottom-grid{display:grid;grid-template-columns:1fr 1.1fr 1.08fr;gap:10px;margin-top:10px}.report-bottom-grid .report-panel{min-height:216px}
.report-mini-table{width:100%;border-collapse:collapse;font-size:11px;text-align:center}.report-mini-table th{background:#f6f7fd;padding:10px 4px;color:#10166b;border:1px solid #e7eafa;font-weight:700}.report-mini-table td{padding:11px 3px;border-bottom:1px solid #f0f1f7}.report-mini-table .total td{font-weight:750;background:#f5f2ff;color:#5c20da}.report-mini-table td:first-child{text-align:left}
.report-intensity-row{display:flex;align-items:center;gap:9px;font-size:11px;margin:9px 0}.report-intensity-row .int-label{width:99px;flex:none}.report-intensity-row .int-bar-track{flex:1;height:19px;background:#f7f4ff}.report-intensity-row .int-bar{height:100%;background:linear-gradient(90deg,#7037c7,#a46ce6)}.report-intensity-row strong{width:25px}.report-intensity-summary{margin-top:9px;padding:8px;background:#f8f9fe;font-size:12px;color:#0a165a;display:flex;gap:14px;justify-content:space-between}.report-intensity-summary strong{font-size:17px}
.report-analysis{font-size:12px;line-height:1.45;color:#263254}.report-analysis p{margin:0 0 9px}.report-foot{margin-top:12px;font-size:10px;color:#53638e;display:flex;justify-content:space-between;gap:12px;border-top:1px solid #e6eafa;padding-top:9px}.report-provisional{color:#8f3c12;font-weight:700}.report-no-data{display:flex;align-items:center;justify-content:center;text-align:center;color:#697497;font-size:12px;border:1px dashed #e4e7f2;width:100%;height:100%;padding:15px}
.report-preview-trigger{width:100%;margin-top:9px;border:1px solid #536de4;background:linear-gradient(110deg,#203f8c,#663dc2);padding:12px 8px;color:white}
@media(max-width:1100px){.report-heading{display:block}.report-meta{margin-top:12px}.report-chart-grid,.report-bottom-grid{grid-template-columns:1fr}.report-kpi-grid{grid-template-columns:repeat(2,1fr)}}
@media print{
@page{size:A3 landscape;margin:6mm}
html,body{background:white!important;margin:0!important;padding:0!important;height:auto!important;overflow:visible!important}
body>.app{display:none!important}#reportOverlay{display:block!important;position:static!important;background:white!important;padding:0!important;margin:0!important;overflow:visible!important}
.report-actions{display:none!important}#reportSheet{box-shadow:none!important;width:100%!important;max-width:none!important;margin:0!important;padding:0!important;zoom:.94;print-color-adjust:exact;-webkit-print-color-adjust:exact}
.report-panel,.report-kpi{break-inside:avoid}.report-chart-grid,.report-bottom-grid{break-inside:avoid}
}
/* v0.8.1 — PDF-afdruk: A3 liggend, vaste dashboardkolommen ongeacht printviewport */
@media print {
@page { size: 420mm 297mm; margin: 7mm; }
html,body { width:100%!important; margin:0!important; padding:0!important; overflow:visible!important; }
#reportOverlay { display:block!important; position:static!important; width:100%!important; padding:0!important; margin:0!important; overflow:visible!important; }
#reportSheet {
display:block!important; box-sizing:border-box!important;
width:100%!important; max-width:none!important; margin:0!important;
padding:4mm 5mm!important; zoom:1!important;
break-inside:avoid!important; page-break-inside:avoid!important;
}
#reportSheet .report-heading { display:flex!important; align-items:center!important; }
#reportSheet .report-meta { display:grid!important; grid-template-columns:repeat(3,minmax(0,1fr))!important; margin-top:0!important; }
#reportSheet .report-kpi-grid { display:grid!important; grid-template-columns:repeat(4,minmax(0,1fr))!important; }
#reportSheet .report-chart-grid { display:grid!important; grid-template-columns:1fr 1fr 1.13fr!important; }
#reportSheet .report-bottom-grid { display:grid!important; grid-template-columns:1fr 1.1fr 1.08fr!important; }
#reportSheet .report-chart-grid .report-panel { min-height:190px!important; }
#reportSheet .report-bottom-grid .report-panel { min-height:196px!important; }
#reportSheet .report-panel .chart-box { height:145px!important; }
#reportSheet .report-kpi { min-height:116px!important; }
#reportSheet .report-kpi .k-num { font-size:28px!important; }
#reportSheet .report-intensity-row { margin:6px 0!important; }
#reportSheet .report-analysis { font-size:11px!important; }
#reportSheet .report-foot { margin-top:7px!important; }
#reportSheet .report-panel, #reportSheet .report-kpi { break-inside:avoid!important; }
}
/* v0.8.2 — A4 liggend: vaste drie kolommen en alle onderdelen op één afdrukvel. */
@media print {
@page { size: A4 landscape; margin: 4mm; }
html,body { width:100%!important; margin:0!important; padding:0!important; }
#reportOverlay { width:100%!important; margin:0!important; padding:0!important; }
#reportSheet { width:100%!important; max-width:none!important; padding:2mm 3mm!important; zoom:1!important; }
#reportSheet .report-brand { font-size:24px!important; margin-bottom:6px!important; }
#reportSheet .report-brand sup { font-size:8px!important; }
#reportSheet .report-heading { display:flex!important; gap:9px!important; margin-bottom:8px!important; }
#reportSheet .report-heading h1 { font-size:22px!important; letter-spacing:-.4px!important; }
#reportSheet .report-heading p { font-size:11px!important; }
#reportSheet .report-meta { min-width:49%!important; gap:4px!important; }
#reportSheet .report-meta>div { gap:5px!important; padding:5px 6px!important; }
#reportSheet .report-meta .meta-icon { font-size:17px!important; }
#reportSheet .report-meta strong { font-size:9px!important; }
#reportSheet .report-meta span { font-size:9px!important; overflow-wrap:anywhere; }
#reportSheet .report-kpi-grid { gap:6px!important; margin:8px 0!important; }
#reportSheet .report-kpi { min-height:96px!important; padding:9px 8px!important; grid-template-columns:38px minmax(0,1fr)!important; column-gap:8px!important; }
#reportSheet .report-kpi .k-icon { width:36px!important; height:36px!important; font-size:19px!important; }
#reportSheet .report-kpi .k-label { font-size:11px!important; }
#reportSheet .report-kpi .k-num { font-size:24px!important; margin:3px 0 1px!important; }
#reportSheet .report-kpi .k-sub { font-size:9px!important; line-height:1.2!important; }
#reportSheet .report-chart-grid { grid-template-columns:1fr 1fr 1.13fr!important; gap:6px!important; }
#reportSheet .report-bottom-grid { grid-template-columns:1fr 1.1fr 1.08fr!important; gap:6px!important; margin-top:6px!important; }
#reportSheet .report-panel { padding:7px 8px!important; border-radius:6px!important; }
#reportSheet .report-panel h2 { font-size:11px!important; margin:0 0 5px!important; }
#reportSheet .report-panel h2 small { font-size:8px!important; }
#reportSheet .report-chart-grid .report-panel { min-height:160px!important; }
#reportSheet .report-bottom-grid .report-panel { min-height:167px!important; }
#reportSheet .report-panel .chart-box { height:137px!important; }
#reportSheet .report-mini-table { font-size:8px!important; }
#reportSheet .report-mini-table th { padding:6px 2px!important; }
#reportSheet .report-mini-table td { padding:7px 2px!important; }
#reportSheet .report-intensity-row { margin:5px 0!important; gap:5px!important; font-size:9px!important; }
#reportSheet .report-intensity-row .int-label { width:80px!important; }
#reportSheet .report-intensity-row .int-bar-track { height:13px!important; }
#reportSheet .report-intensity-summary { margin-top:5px!important; padding:5px!important; font-size:9px!important; }
#reportSheet .report-intensity-summary strong { font-size:12px!important; }
#reportSheet .report-analysis { font-size:9px!important; line-height:1.3!important; }
#reportSheet .report-analysis p { margin-bottom:5px!important; }
#reportSheet .report-foot { margin-top:6px!important; padding-top:5px!important; font-size:8px!important; }
}
/* v0.9.1: transparante cliëntdefinities zonder aanpassing van de ORCHESTRA-huisstijl. */
.definition-guide{border:1px solid #2c4971;border-radius:11px;background:linear-gradient(110deg,#11243b,#111a33);padding:12px 16px;margin:11px 0 16px;color:#cadcf0}
.definition-guide-heading{display:flex;align-items:center;gap:10px;margin-bottom:9px;font-size:12px}
.definition-guide-heading strong{color:#f2f8ff}.definition-guide-heading>span:last-child{font-size:10px;color:#93abc9;margin-left:auto}
.definition-guide-icon{color:#57dcbc;font-size:17px}
.definition-guide-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}
.definition-guide-grid>div{border-left:2px solid #3d7091;padding:1px 0 1px 10px}
.definition-guide-grid b{font-size:11px;color:#e3f3ff}.definition-guide-grid p{font-size:10px;line-height:1.42;margin:4px 0 0;color:#adbed6}
.definition-guide-note{font-size:10px;color:#77dbb7;margin-top:10px;line-height:1.35}
@media(max-width:760px){.definition-guide-grid{grid-template-columns:1fr}.definition-guide-heading{flex-wrap:wrap}.definition-guide-heading>span:last-child{margin-left:0}}
.report-definitions{border:1px solid #e2e6f1;border-radius:7px;background:#f8faff;padding:8px 12px;margin:-3px 0 10px}
.report-definitions-title{font-weight:800;color:#181c65;font-size:11px;margin-bottom:5px}
.report-definitions-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
.report-definitions-grid>div{border-left:3px solid #7850b9;padding:2px 0 2px 8px;color:#324062;font-size:10px;line-height:1.32}
.report-definitions-grid b{display:block;font-size:10px;color:#161c64;margin-bottom:2px}
.report-definitions-note{font-size:9px;color:#52618d;margin-top:5px}
@media(max-width:1000px){.report-definitions-grid{grid-template-columns:1fr}}
@media print{#reportSheet .report-definitions-grid{grid-template-columns:repeat(3,minmax(0,1fr))!important}#reportSheet .report-definitions{padding:4px 7px!important;margin:1px 0 6px!important}#reportSheet .report-definitions-grid>div{font-size:7px!important;line-height:1.2!important}#reportSheet .report-definitions-grid b,#reportSheet .report-definitions-title{font-size:8px!important}#reportSheet .report-definitions-note{font-size:7px!important}}
/* v1.2: los, compact controlepaneel; bestaande ORCHESTRA en rapport-opmaak ongewijzigd */
.controller-panel{padding:17px}.controller-note,.controller-footer{font-size:11px;line-height:1.55;color:#a8bad5;margin:6px 0 12px}.controller-context{border:1px solid #345075;background:#102642;border-radius:7px;padding:9px 12px;color:#b5d5f5;font-size:11px;margin-bottom:12px}
.controller-entry{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.controller-entry label{display:flex;flex-direction:column;gap:7px;color:#b5c6dd;font-size:10px}.controller-entry input{width:100%;min-width:0;background:#0a1528;border:1px solid #365579;border-radius:7px;padding:10px;color:#f2f7ff;font-size:14px}.controller-entry input:focus{outline:1px solid #8e8bff}.controller-actions{display:flex;gap:9px;margin:12px 0}.controller-result{font-size:11px;color:#a9bfda}.controller-table{width:100%;border-collapse:collapse;margin-top:8px;font-size:11px}.controller-table th,.controller-table td{text-align:right;padding:9px 7px;border-bottom:1px solid #28415e}.controller-table th:first-child,.controller-table td:first-child{text-align:left}.controller-table th{color:#94b4dc;font-weight:600}.controller-ok{color:#57edb7;font-weight:700}.controller-bad{color:#ffb37b;font-weight:700}.controller-summary{padding:10px 12px;border-radius:7px;border:1px solid #5a5b79;background:#23243d;margin:10px 0}.controller-footer{margin-bottom:0}.controller-note strong{color:#eaf4ff}@media(max-width:800px){.controller-entry{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:460px){.controller-entry{grid-template-columns:1fr}}
/* v1.5: uitsluitend compacte toevoegingen voor historische invoer */
.history-status{grid-column:1/-1;color:#94b7d7;font-size:10px;line-height:1.4;border-top:1px solid #26415e;padding-top:9px}
.history-status.ready{color:#5ce5b4}
.history-status.warning{color:#ffc27f}
.history-label{border-color:#4c5581!important}
#historyFile{display:none}
/* v1.6: Bronafwijkingen met aantallen en voorzichtige KPI-impact, uitsluitend in verificatiepaneel */
.source-audit{margin-top:13px;padding:12px;border:1px solid #344d70;background:#0c1b31;border-radius:9px}
.source-audit h4{margin:0 0 6px;font-size:11px;color:#c5d9f7;letter-spacing:.25px}
.source-audit .source-intro{font-size:10px;line-height:1.5;color:#9fb4d2;margin:0 0 9px}
.source-issue{border-top:1px solid #243d5b;padding:10px 0 8px}
.source-issue-top{display:flex;align-items:center;justify-content:space-between;gap:9px}
.source-issue-name{font-size:11px;color:#e8f0ff;font-weight:700}
.source-issue-count{font-size:10px;color:#d3dff1;white-space:nowrap}
.source-issue-impact{font-size:9px;font-weight:700;letter-spacing:.2px;margin-top:5px}
.source-issue-impact.none{color:#5fe3b9}.source-issue-impact.possible{color:#ffc17e}.source-issue-impact.unknown{color:#c4c5fa}
.source-issue-detail{font-size:10px;color:#adbfdb;line-height:1.5;margin:4px 0 0}
.source-audit-ok{font-size:10px;color:#71e8be;line-height:1.5}
@media(max-width:1080px){header{grid-template-columns:1fr;justify-items:start}.header-center-logo{justify-content:flex-start;padding:4px 0 0}.header-center-logo img{height:60px}.header-actions{justify-self:start}}
/* v1.6.3: alleen de witte managementrapportage, bestaande dashboard-huisstijl onveranderd */
#reportSheet .report-average-note{margin-top:8px;font-size:9px;color:#68739a;line-height:1.25}
#reportSheet .report-client-month-chart{height:157px;max-width:100%;min-width:0}
#reportSheet .report-client-month-chart svg{display:block;max-width:100%;height:100%}
@media print{#reportSheet .report-average-note{font-size:8px!important;margin-top:5px!important}#reportSheet .report-client-month-chart{height:133px!important}}
/* v1.6.4 — leesbare managementrij, uitsluitend witte Inloop Monitor */
@media(min-width:1101px){
#reportSheet .report-bottom-grid{grid-template-columns:.98fr 1.30fr 1fr;}
}
#reportSheet .report-bottom-grid .report-panel{min-height:232px}
#reportSheet .report-client-month-chart{height:196px;}
#reportSheet .report-average-highlight{display:flex;gap:9px;align-items:center;margin-top:14px;padding:12px 14px;background:#f6f3ff;border:1px solid #e5dafa;border-radius:7px;}
#reportSheet .report-average-highlight .highlight-num{font-size:27px;font-weight:850;color:#5b26c3;line-height:1;white-space:nowrap;}
#reportSheet .report-average-highlight .highlight-label{font-size:12px;font-weight:700;color:#27325b;line-height:1.3;}
#reportSheet .report-analysis{font-size:12px;line-height:1.52;}
#reportSheet .report-analysis p{margin:0 0 11px;}
@media print{
#reportSheet .report-bottom-grid{grid-template-columns:.98fr 1.30fr 1fr!important;}
#reportSheet .report-bottom-grid .report-panel{min-height:183px!important;}
#reportSheet .report-client-month-chart{height:153px!important;}
#reportSheet .report-average-highlight{margin-top:5px!important;padding:6px!important;gap:5px!important;}
#reportSheet .report-average-highlight .highlight-num{font-size:16px!important;}
#reportSheet .report-average-highlight .highlight-label{font-size:8px!important;}
#reportSheet .report-analysis{font-size:9px!important;line-height:1.38!important;}
#reportSheet .report-analysis p{margin:0 0 7px!important;}
}
/* v1.6.5b: alleen een extra, exact gecentreerde ORCHESTRA-titel in de bestaande header. */
.main header{position:relative}
.orchestra-hero-title{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;text-align:center;white-space:nowrap;pointer-events:none;z-index:1}
.orchestra-hero-word{display:block;font-family:Inter,'Segoe UI',Arial,sans-serif;font-size:clamp(37px,3.7vw,62px);line-height:1.02;font-weight:850;letter-spacing:.3px;background:linear-gradient(100deg,#91efff 0%,#49c0fd 49%,#468dff 100%);background-clip:text;-webkit-background-clip:text;color:transparent;-webkit-text-fill-color:transparent}
.orchestra-hero-credit{display:block;font-size:12px;letter-spacing:4px;font-weight:500;color:#60c9f8;line-height:1.4}
/* Op smallere schermen eigen gecentreerde rij, zodat de bestaande headerdelen nooit overlappen. */
@media(max-width:1380px){.orchestra-hero-title{position:static;transform:none;grid-column:1/-1;grid-row:2;justify-self:center;margin:14px 0 2px}.orchestra-hero-word{font-size:clamp(35px,5vw,54px)}}
@media(max-width:1080px){.orchestra-hero-title{grid-column:1;grid-row:auto;justify-self:center;margin:12px 0 4px}}
@media(max-width:680px){.orchestra-hero-word{font-size:clamp(31px,8vw,44px)}.orchestra-hero-credit{font-size:10px;letter-spacing:3px}}
/* v1.6.5c: uitsluitend de oranje Reakt-woordmark in de donkere bovenbalk. */
.header-center-logo .reakt-wordmark{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;width:max-content;background:transparent;color:#ff5a0a;line-height:1;white-space:nowrap;}
.header-center-logo .reakt-name{display:block;font-family:Arial,'Segoe UI',sans-serif;font-weight:800;font-size:clamp(40px,3vw,56px);letter-spacing:-2.6px;line-height:.99;color:#ff5a0a;}
.header-center-logo .reakt-tagline{display:block;font-family:Arial,'Segoe UI',sans-serif;font-weight:500;font-size:clamp(12px,.9vw,15px);line-height:1.16;letter-spacing:-.2px;color:#ff5a0a;margin-top:4px;}
@media(max-width:1080px){.header-center-logo .reakt-name{font-size:44px}.header-center-logo .reakt-tagline{font-size:12px}}
/* ORCHESTRA NEXT: dynamischer command center met vloeiende netwerkanimaties */
.orchestra{padding:28px 28px 0;background:radial-gradient(ellipse at 50% 10%,#20376a 0%,#0b1730 58%,#08111f 100%);overflow:hidden}
.orchestra::before,.orchestra::after{content:'';position:absolute;pointer-events:none;z-index:0;border-radius:50%;filter:blur(54px);opacity:.2}
.orchestra::before{width:320px;height:320px;left:-70px;top:90px;background:#35c9ff}
.orchestra::after{width:360px;height:360px;right:-95px;top:170px;background:#8549ff}
.orchestra .conductor-wrap{position:relative;z-index:3;margin-bottom:34px}
.orchestra .conductor{width:362px;padding:20px 22px;border-color:#c194ff;box-shadow:0 0 45px #8c54df6f,inset 0 0 22px #9a69ff24;transition:transform .35s ease,box-shadow .35s ease,border-color .35s ease;overflow:hidden}
.orchestra .conductor::before{content:'';position:absolute;inset:-45% auto -45% -60%;width:52%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.18),transparent);transform:rotate(18deg) translateX(-160%);opacity:0}
.orchestra .conductor::after{content:'';position:absolute;inset:auto;left:50%;top:50%;width:155px;height:155px;background:radial-gradient(circle,rgba(188,150,255,.16),transparent 68%);transform:translate(-50%,-50%);pointer-events:none;opacity:.65}
.orchestra .conductor strong{font-size:19px}
.orchestra .conductor.running{animation:conductorFloat 4.8s ease-in-out infinite}
.orchestra .conductor.transfering{transform:translateY(-4px) scale(1.02);border-color:#efd7ff;box-shadow:0 0 60px #b779ff88,inset 0 0 28px #d0a7ff30}
.orchestra .conductor.transfering::before{opacity:1;animation:conductorSweep 1s ease-out}
.orchestra .conductor.transfering .conductor-icon{box-shadow:0 0 28px #d0a7ff99,0 0 54px #7de1ff33}
.orchestra .conductor.transfering .brain-symbol{animation-duration:.9s}
.orchestra .conductor.attention{box-shadow:0 0 46px #ffae5990,inset 0 0 26px #ffbf6a24}
.orchestra .conductor.attention::after{background:radial-gradient(circle,rgba(255,189,106,.18),transparent 68%)}
.orchestra .agents{display:grid;grid-template-columns:repeat(3,minmax(300px,1fr));gap:34px 58px;max-width:1490px;margin:0 auto;z-index:2;position:relative;padding:20px 18px 28px}
.orchestra .agent{min-height:178px;padding:16px 18px;position:relative;z-index:2;overflow:hidden;transition:transform .42s ease,box-shadow .42s ease,border-color .42s ease;box-shadow:0 0 16px color-mix(in srgb,var(--accent) 10%,transparent),inset 0 0 25px color-mix(in srgb,var(--accent) 6%,transparent)}
.orchestra .agent::before{content:'';position:absolute;inset:auto -10% -48% auto;width:110px;height:110px;background:radial-gradient(circle,color-mix(in srgb,var(--accent) 34%,transparent),transparent 72%);opacity:.58;filter:blur(8px);pointer-events:none}
.orchestra .agent::after{content:'';position:absolute;inset:-30% auto -30% -55%;width:42%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.12),transparent);transform:rotate(12deg) translateX(-210%);opacity:0;pointer-events:none}
.orchestra .agent:hover{transform:translateY(-2px)}
.orchestra .agent.active{transform:translateY(-8px) scale(1.012);box-shadow:0 0 34px color-mix(in srgb,var(--accent) 34%,transparent),inset 0 0 24px color-mix(in srgb,var(--accent) 13%,transparent)}
.orchestra .agent.active::after{opacity:1;animation:agentSweep 1.35s linear infinite}
.orchestra .agent.done .status{color:#62e5bd}
.orchestra .agent h3{font-size:17px;margin:4px 0}
.orchestra .agent .agent-symbol{font-size:23px;margin-top:7px;animation:iconFloat 5.5s ease-in-out infinite}
.orchestra .agent .mini-visual{margin:5px 0;height:27px;font-size:17px;opacity:.95}
.orchestra .agent .agent-steps{margin:5px 0;min-height:82px}
.orchestra .agent .agent-step{font-size:10px}
.orchestra .agent p{font-size:12px}
.orchestra .agent-bottom .track{height:6px;background:#223650}
.orchestra .agent-bottom .track i{transition:width .35s ease}
#agent-data{order:1}#agent-identity{order:2}#agent-history{order:3}#agent-analysis{order:4}#agent-insight{order:5}#agent-verify{order:6}#agent-privacy{order:7}#agent-report{order:8}#agent-quality{order:9}
#agent-data .bars b{animation:dataBar 1.75s ease-in-out infinite}
#agent-data .bars b:nth-child(2){animation-delay:.18s}#agent-data .bars b:nth-child(3){animation-delay:.36s}#agent-data .bars b:nth-child(4){animation-delay:.54s}#agent-data .bars b:nth-child(5){animation-delay:.72s}#agent-data .bars b:nth-child(6){animation-delay:.9s}#agent-data .bars b:nth-child(7){animation-delay:1.08s}
#agent-analysis .spark{animation:analysisSpark 2.4s ease-in-out infinite;letter-spacing:1px}
#agent-insight .rings{animation:ringsDrift 7.5s ease-in-out infinite}
#agent-insight .rings span{display:inline-block;animation:insightPulse 2.1s ease-in-out infinite}
#agent-verify .checkicons{animation:verifyPulse 2.35s ease-in-out infinite}
#agent-report .reportlines{animation:reportScan 2.6s ease-in-out infinite}
#agent-identity .mini-visual{animation:identityLink 2.8s ease-in-out infinite;letter-spacing:1px}
#agent-history .mini-visual{animation:historyLoop 3.7s ease-in-out infinite}
#agent-privacy .mini-visual{animation:privacyShield 2.9s ease-in-out infinite}
#agent-quality .mini-visual{animation:qualitySpark 2.4s ease-in-out infinite}
#agent-data.active .bars b,#agent-analysis.active .spark,#agent-insight.active .rings,#agent-insight.active .rings span,#agent-verify.active .checkicons,#agent-report.active .reportlines,#agent-identity.active .mini-visual,#agent-history.active .mini-visual,#agent-privacy.active .mini-visual,#agent-quality.active .mini-visual{animation-duration:.95s}
.network-links{position:absolute;inset:0;z-index:1;pointer-events:none;overflow:hidden}
#networkSvg{width:100%;height:100%;overflow:visible}
#networkEdges path{fill:none;stroke:#3e6888;stroke-width:1.55;opacity:.48;vector-effect:non-scaling-stroke;animation:routeGlow 5.5s ease-in-out infinite}
#networkEdges path.transfer{stroke:#86ecff;stroke-width:3.1;stroke-dasharray:8 12;opacity:1;animation:nextStream .72s linear infinite,routeTransferGlow 1.15s ease-in-out infinite;filter:drop-shadow(0 0 7px #75dafa)}
#networkEdges path.route-done{stroke:#4cc69f;opacity:.68;filter:drop-shadow(0 0 5px rgba(76,198,159,.35))}
#networkSignals circle{filter:drop-shadow(0 0 8px #8df3ff)}
@keyframes nextStream{to{stroke-dashoffset:-40}}
@keyframes routeGlow{50%{opacity:.72}}
@keyframes routeTransferGlow{50%{filter:drop-shadow(0 0 12px #9ef4ff)}}
@keyframes conductorFloat{50%{transform:translateY(-4px)}}
@keyframes conductorSweep{0%{transform:rotate(18deg) translateX(-160%)}100%{transform:rotate(18deg) translateX(360%)}}
@keyframes agentSweep{0%{transform:rotate(12deg) translateX(-220%)}100%{transform:rotate(12deg) translateX(360%)}}
@keyframes iconFloat{50%{transform:translateY(-3px)}}
@keyframes dataBar{0%,100%{transform:scaleY(.72);opacity:.7}50%{transform:scaleY(1.18);opacity:1}}
@keyframes analysisSpark{0%,100%{transform:translateX(0);opacity:.78}50%{transform:translateX(4px);opacity:1}}
@keyframes ringsDrift{50%{transform:translateY(-2px) rotate(4deg)}}
@keyframes insightPulse{50%{transform:scale(1.16);text-shadow:0 0 10px currentColor}}
@keyframes verifyPulse{0%,100%{opacity:.75}50%{opacity:1;text-shadow:0 0 14px currentColor}}
@keyframes reportScan{0%,100%{transform:translateY(0);opacity:.82}50%{transform:translateY(-2px);letter-spacing:4px;opacity:1}}
@keyframes identityLink{50%{letter-spacing:3px;transform:translateX(2px)}}
@keyframes historyLoop{50%{transform:translateY(-2px) scale(1.03);opacity:1}}
@keyframes privacyShield{50%{transform:scale(1.08);text-shadow:0 0 11px currentColor}}
@keyframes qualitySpark{50%{transform:translateY(-2px) scale(1.08);text-shadow:0 0 10px currentColor}}
.network-legend{display:flex;gap:20px;flex-wrap:wrap;justify-content:center;font-size:11px;color:#99b3d3;padding:2px 10px 14px;position:relative;z-index:2}
.network-legend strong{color:#d5eaff}
.next-narrative{text-align:center;color:#d9ebff;font-size:13px;line-height:1.55;margin:4px auto 15px;max-width:880px;text-shadow:0 0 12px rgba(98,167,255,.12)}
.next-mode{font-size:11px;color:#70dcc3;border:1px solid #255e5c;padding:6px 11px;border-radius:30px;white-space:nowrap}
@media(max-width:1080px){.orchestra .agents{max-width:1200px;gap:22px 24px;grid-template-columns:repeat(3,minmax(0,1fr))}.orchestra .agent{padding:13px 12px;min-height:170px}.orchestra .agent h3{font-size:15px}}
@media(max-width:700px){.orchestra .agents{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.orchestra .agent{min-height:154px}.orchestra .conductor{max-width:100%}.network-links{display:none}.network-legend{justify-content:flex-start}}
@media(prefers-reduced-motion:reduce){#networkEdges path,#networkEdges path.transfer,.orchestra .conductor,.orchestra .agent .agent-symbol,.orchestra .agent .mini-visual,#agent-data .bars b,#agent-analysis .spark,#agent-insight .rings,#agent-insight .rings span,#agent-verify .checkicons,#agent-report .reportlines,#agent-identity .mini-visual,#agent-history .mini-visual,#agent-privacy .mini-visual,#agent-quality .mini-visual{animation:none!important}.orchestra .agent::after,.orchestra .conductor::before{display:none}}
</style></head>
<body>
<div class="app">
<main class="main">
<header><div class="header-copy"><div class="eyebrow">REAKT / AI ANALYTICS PLATFORM</div><h1>Inloop <span>Intelligence</span> <em>ORCHESTRA</em></h1><p>Maak complexe informatie begrijpelijk en toegankelijk voor iedereen.</p></div><div class="orchestra-hero-title" aria-label="ORCHESTRA by Marcel Ooms"><span class="orchestra-hero-word">ORCHESTRA</span><span class="orchestra-hero-credit">by Marcel Ooms</span></div><div class="header-center-logo"><div class="reakt-wordmark" role="img" aria-label="Reakt — Werken aan je toekomst"><strong class="reakt-name">Reakt</strong><span class="reakt-tagline">Werken aan je toekomst</span></div></div><div class="header-actions"><span class="pill"><i></i> LOKALE ANALYSE</span><span class="clock" id="clock"></span></div></header>
<div class="topbar"><div><span class="section-kicker">CONTROL CENTER</span><h2>Agent Command Center <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 voorbeeldanalyse</button></div></div>
<section class="import-panel panel" aria-label="Lokale Excelanalyse met 2026 en historische referentie 2025">
<div class="import-intro"><span class="section-kicker">ORCHESTRA NEXT · BROWSER-LOKAAL</span><strong>Scanregistratie 2026 + referentie 2025</strong><small>Browser-lokale analyse · alle bronlocaties · geen bestanden in Space-repository</small></div>
<label class="import-file-label" for="scanFile"><span>▤</span> Rapportage 2026 (.xlsx)</label><input type="file" id="scanFile" accept=".xlsx" aria-label="Rapportagebestand januari tot en met september 2026" />
<label class="import-file-label history-label" for="historyFile"><span>◴</span> Historie 2025 (.xlsx)</label><input type="file" id="historyFile" accept=".xlsx" aria-label="Historisch referentiebestand 2025" disabled/>
<label class="site-label" for="siteSelect">Locatie</label><select id="siteSelect" disabled><option value="*">Alle locaties</option></select>
<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>
<span id="importState" class="import-state">Kies eerst het rapportagebestand 2026</span>
<div id="historyStatus" class="history-status">◴ Historie 2025 nog niet geladen. Zonder deze referentie zijn instroomcijfers voorlopig.</div>
</section>
<section class="orchestra panel">
<div class="mesh"></div><p class="next-narrative">Van ruwe scangegevens naar helder inzicht · volg taakverdeling, gegevensoverdracht en controles onder de motorkap.</p><div class="conductor-wrap"><div class="conductor" id="conductor"><div class="conductor-glow"></div><div class="conductor-icon" aria-hidden="true"><svg class="brain-symbol" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.55" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5c-2.7-3.2-7.4-.9-7.1 2.4C2 9 2 13.6 5 15.2c-.7 4.4 4.9 6.8 7 2.6M12 5c2.7-3.2 7.4-.9 7.1 2.4 2.9 1.6 2.9 6.2-.1 7.8.7 4.4-4.9 6.8-7 2.6M12 5v13"/><path d="M6.6 10.1 9 12v3M17.4 10.1 15 12v3"/><circle cx="9" cy="15.5" r=".9" fill="currentColor" stroke="none"/><circle cx="15" cy="15.5" r=".9" fill="currentColor" stroke="none"/></svg></div><div><small>MASTER ORCHESTRATOR</small><strong>THE CONDUCTOR</strong><span id="conductorStatus">Gereed voor analyse</span></div><div class="conductor-indicator"></div></div></div>
<div class="network-links" aria-hidden="true"><svg id="networkSvg" preserveAspectRatio="none"><g id="networkEdges"></g><g id="networkSignals"></g></svg></div>
<div class="agents">
<article class="agent" id="agent-data" style="--accent:#34e6b3"><div class="agent-top"><span>01 / DATA</span><span class="status">STANDBY</span></div><div class="agent-symbol">▤</div><h3>Data Agent</h3><p>Structuur & kwaliteit</p><div class="agent-steps" aria-live="polite" aria-label="Werkstappen"></div><div class="mini-visual bars"><b></b><b></b><b></b><b></b><b></b><b></b><b></b></div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-analysis" style="--accent:#ffcb62"><div class="agent-top"><span>04 / ANALYSE</span><span class="status">STANDBY</span></div><div class="agent-symbol">⌁</div><h3>Analysis Agent</h3><p>Trends & vergelijkingen</p><div class="agent-steps" aria-live="polite" aria-label="Werkstappen"></div><div class="mini-visual spark">▁ ▃ ▂ ▅ ▄ ▆ ▇</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-insight" style="--accent:#51b9ff"><div class="agent-top"><span>06 / TREND</span><span class="status">STANDBY</span></div><div class="agent-symbol">✦</div><h3>Trend Agent</h3><p>Maandpatronen & duiding</p><div class="agent-steps" aria-live="polite" aria-label="Werkstappen"></div><div class="mini-visual rings">◎ <span>◉</span> ◎</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-verify" style="--accent:#b18aff"><div class="agent-top"><span>07 / CONTROLE</span><span class="status">STANDBY</span></div><div class="agent-symbol">⬡</div><h3>Verification Agent</h3><p>Schaduwberekeningen</p><div class="agent-steps" aria-live="polite" aria-label="Werkstappen"></div><div class="mini-visual checkicons">✓ &nbsp; ✓ &nbsp; ! &nbsp; ✓</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-report" style="--accent:#ff9a65"><div class="agent-top"><span>09 / RAPPORT</span><span class="status">STANDBY</span></div><div class="agent-symbol">▧</div><h3>Report Agent</h3><p>Infographic & export</p><div class="agent-steps" aria-live="polite" aria-label="Werkstappen"></div><div class="mini-visual reportlines">▰▰▰▰▰<br>▰▰▰▰<br>▰▰▰</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-identity" style="--accent:#53d5ec"><div class="agent-top"><span>02 / IDENTITEIT</span><span class="status">STANDBY</span></div><div class="agent-symbol">⛓</div><h3>Identity Agent</h3><p>Pas- en cliëntkoppelingen</p><div class="agent-steps" aria-live="polite"></div><div class="mini-visual">◎ ↔ ◎</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-history" style="--accent:#60bafa"><div class="agent-top"><span>03 / HISTORIE</span><span class="status">STANDBY</span></div><div class="agent-symbol">◷</div><h3>History Agent</h3><p>Referentie 2025</p><div class="agent-steps" aria-live="polite"></div><div class="mini-visual">2025 ↔ 2026</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-privacy" style="--accent:#f28cbd"><div class="agent-top"><span>08 / PRIVACY</span><span class="status">STANDBY</span></div><div class="agent-symbol">◇</div><h3>Privacy Agent</h3><p>Lokale verwerkingsroute</p><div class="agent-steps" aria-live="polite"></div><div class="mini-visual">◈ ⛨ ◈</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-quality" style="--accent:#ea88e9"><div class="agent-top"><span>10 / KWALITEIT</span><span class="status">STANDBY</span></div><div class="agent-symbol">✧</div><h3>Quality Agent</h3><p>Rapportagestatus</p><div class="agent-steps" aria-live="polite"></div><div class="mini-visual">✓ / !</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
</div>
<div class="orchestra-footer"><span><i class="dot green"></i> Systeem gereed</span><span id="flowState">Wacht op startcommando</span><span>◌ <span id="completed">0</span> / 9 agenten voltooid</span></div>
</section>
<div class="content-grid">
<div class="left-content">
<div class="kpi-grid" id="kpis"></div>
<section class="definition-guide" aria-label="Betekenis van de cliëntcijfers">
<div class="definition-guide-heading"><span class="definition-guide-icon">ⓘ</span><strong>Wat betekenen deze cliëntcijfers?</strong><span>Rekenregels per inlooplocatie</span></div>
<div class="definition-guide-grid">
<div><b>Unieke cliënten</b><p>Iedere cliënt die op deze locatie binnen de gekozen rapportageperiode minimaal één geldige scan heeft.</p></div>
<div><b>Nieuwe instroom</b><p>Cliënten met hun allereerste geldige scan op deze locatie in de gekozen rapportageperiode.</p></div>
<div><b>Actieve cliënten</b><p>Cliënten die in de afgelopen drie maanden t/m de peildatum minimaal één keer op deze locatie hebben gescand.</p></div>
</div>
<div class="definition-guide-note">Een verloren of vervangen scanpas telt niet als een nieuwe cliënt: meerdere passen van dezelfde cliënt worden samengevoegd.</div>
</section>
<div class="charts-grid"><section class="panel chart-panel"><div class="panel-heading"><div><span class="section-kicker">TRENDANALYSE</span><h3>Bezoeken per locatie</h3></div><span class="subtle">Jul – Sep 2026</span></div><canvas id="chart" width="640" height="250" aria-label="Grafiek fictieve bezoeken per locatie"></canvas><div class="legend"><span><i style="background:#3ee1b6"></i> Soeda</span><span><i style="background:#ffc66a"></i> Rietdijk</span><span><i style="background:#5b9cff"></i> Vredehof</span><span><i style="background:#ad88ff"></i> Gouda</span></div></section>
<section class="panel verification"><div class="panel-heading"><div><span class="section-kicker">KWALITEITSBORGING</span><h3>Verificatie Businesscontroller</h3></div><span class="warn-label">DEMO</span></div><div id="checks" class="checks"></div><div class="warning">⚠ 1 fictieve afwijking. Rapport niet gevalideerd.</div></section></div>
<section class="panel controller-panel" id="controllerPanel" aria-label="Vergelijking met businesscontroller">
<div class="panel-heading"><div><span class="section-kicker">EXTERNE SCHADUWCONTROLE · FICTIEVE TEST</span><h3>Vergelijk met businesscontroller</h3></div><span class="warn-label" id="controllerBadge">NIET AANGELEVERD</span></div>
<p class="controller-note">Vul uitsluitend <strong>fictieve, geaggregeerde</strong> controlecijfers in voor exact dezelfde locatie, periode en definities. Geen cliëntgegevens. Invoer blijft in deze browserpagina en wordt niet opgeslagen.</p>
<div class="controller-context" id="controllerContext">Analyseer eerst het fictieve Excel-bestand en kies een locatie en kwartaal.</div>
<div class="controller-entry" id="controllerFields">
<label>Unieke cliënten (periode)<input id="ctrlUnique" type="number" min="0" step="1" inputmode="numeric" placeholder="Bijv. 18"></label>
<label>Nieuwe instroom (periode)<input id="ctrlIntake" type="number" min="0" step="1" inputmode="numeric" placeholder="Bijv. 14"></label>
<label>Actieve cliënten (3 maanden)<input id="ctrlActive" type="number" min="0" step="1" inputmode="numeric" placeholder="Bijv. 17"></label>
<label>Unieke bezoekdagen (periode)<input id="ctrlVisits" type="number" min="0" step="1" inputmode="numeric" placeholder="Bijv. 49"></label>
</div>
<div class="controller-actions"><button id="controllerCompare" class="primary" type="button">↔ Vergelijk cijfers</button><button id="controllerClear" class="ghost" type="button">Wissen</button></div>
<div id="controllerResult" class="controller-result" aria-live="polite">Nog geen onafhankelijke controlecijfers vergeleken.</div>
<div class="controller-footer">Interne controles en vergelijking met ingevoerde waarden zijn afzonderlijk. Een overeenkomst geldt niet als formele validatie of vrijgave.</div>
</section>
<section class="panel logpanel"><div class="panel-heading"><div><span class="section-kicker">REAL-TIME PROCESS</span><h3>Agent activity log</h3></div><span class="subtle" id="logStatus">Stand-by</span></div><div class="logs" id="logs"><div class="logitem muted">◌ &nbsp; Klik op ‘Start demo-analyse’ om de workflow te bekijken.</div></div></section>
</div>
<aside class="right-content"><section class="panel chatpanel"><div class="panel-heading"><div><span class="section-kicker">MANAGEMENT ASSISTENT</span><h3><span class="chat-icon">✧</span> Vraag het aan Inloop AI</h3></div><span class="demo-badge">DEMO</span></div><div class="chat-messages" id="messages"><div class="message assistant"><small>INLOOP AI · DEMONSTRATIE</small><p>Welkom! Start de demo-analyse en stel daarna een vraag over de fictieve locatiecijfers.</p></div></div><div class="quick-questions"><button data-q="Welke locatie groeit het sterkst?">Grootste groei?</button><button data-q="Zijn de cijfers gevalideerd?">Validatiestatus?</button><button data-q="Maak een managementsamenvatting">Samenvatting?</button></div><form id="chatForm" class="chat-form"><input id="chatInput" autocomplete="off" placeholder="Stel een vraag over de demo..." aria-label="Vraag aan Inloop AI"><button aria-label="Verstuur vraag">➤</button></form><div class="chat-disclaimer">Vooraf ingestelde demo-antwoorden · geen echte AI</div></section>
<section class="panel preview"><div class="panel-heading"><div><span class="section-kicker">OUTPUT</span><h3>Infographic preview</h3></div><span class="warn-label">CONCEPT</span></div><div class="paper"><div class="paper-top"><strong>REAKT<span>.</span></strong><small>INLOOPMONITOR / Q3 2026</small></div><h4>Inloop in beeld</h4><p>Illustratieve managementrapportage</p><div class="paper-kpis"><div><strong>12.480</strong><small>Bezoeken</small></div><div><strong>1.286</strong><small>Uniek</small></div><div><strong>164</strong><small>Instroom</small></div></div><div class="paper-bars"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div><div class="paper-watermark">FICTIEVE DATA · NIET GEVALIDEERD</div></div><button class="export" id="exportBtn">▣ Open witte Reakt-monitor</button></section></aside>
</div>
<footer>REAKT INLOOP INTELLIGENCE <span>ORCHESTRA PROTOTYPE · LOKALE VERWERKING · CONTROLEER CIJFERS VÓÓR PUBLICATIE</span></footer>
</main>
</div>
<div id="reportOverlay" role="dialog" aria-modal="true" aria-label="Reakt Monitor rapport" aria-hidden="true">
<div class="report-actions"><span><strong>REAKT · Rapportvoorvertoning v1.6.5</strong> &nbsp; — voorlopige managementrapportage, controle vereist</span><div><button id="reportClose" type="button">✕ Sluiten</button> <button id="reportPng" class="report-png" type="button">⬇ Download PNG</button> <button id="reportJpg" class="report-jpg" type="button">⬇ Download JPG</button> <button id="reportPrint" class="report-print" type="button">↓ Opslaan als PDF / Afdrukken</button></div></div>
<section id="reportSheet" aria-label="Witte Inloop Monitor"></section>
</div>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script type="application/json" id="orchestra-demo-data">{"mode": "DEMO", "period": "Q3 2026 · fictief", "kpis": [{"label": "Bezoeken", "value": "12.480", "change": "+8,4%"}, {"label": "Unieke bezoekers", "value": "1.286", "change": "+3,2%"}, {"label": "Nieuwe instroom", "value": "164", "change": "+5,1%"}, {"label": "Verificatie", "value": "4 / 5", "change": "1 afwijking"}], "locations": [{"name": "Soeda", "visits": [350, 395, 428], "unique": 121}, {"name": "Rietdijk", "visits": [285, 305, 334], "unique": 108}, {"name": "Vredehof", "visits": [245, 268, 294], "unique": 93}, {"name": "Gouda", "visits": [320, 343, 365], "unique": 115}], "checks": [{"label": "Bezoeken", "ours": 2845, "shadow": 2845}, {"label": "Unieke bezoekers", "ours": 386, "shadow": 381}, {"label": "Nieuwe instroom", "ours": 42, "shadow": 42}, {"label": "Bezoeken vorig jaar", "ours": 2610, "shadow": 2610}, {"label": "Actieve locaties", "ours": 4, "shadow": 4}], "steps": [{"agent": "data", "text": "Demo-gegevens ingelezen en structuur gecontroleerd"}, {"agent": "analysis", "text": "Maandtrends en locatievergelijkingen berekend"}, {"agent": "insight", "text": "Opvallende ontwikkelingen samengevat (demo)"}, {"agent": "verify", "text": "Schaduwberekeningen vergeleken: 1 afwijking"}, {"agent": "report", "text": "Conceptinfographic voorbereid · NIET GEVALIDEERD"}]}</script>
<script>
window.addEventListener('error', function(event){
var status=document.getElementById('flowState');
if(status) status.textContent='Scriptfout – meld dit bij de ontwikkelaar';
console.error('ORCHESTRA scriptfout:', event.message);
});
'use strict';
let demoData=null, running=false, completed=0;
const $=id=>document.getElementById(id);
const ids=['data','identity','history','analysis','insight','verify','privacy','report','quality'];
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
const escapeHTML=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function clock(){ $('clock').textContent=new Intl.DateTimeFormat('nl-NL',{hour:'2-digit',minute:'2-digit',day:'numeric',month:'short'}).format(new Date()); }
clock();setInterval(clock,60000);
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('');}
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')} &nbsp; <strong class="${delta?'bad':''}">${delta?'⚠ '+(delta>0?'+':'')+delta:'✓ 0'}</strong></span></div>`}).join('');}
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+(loc.visits.length===1?0:j*(w-105)/(loc.visits.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+(loc.visits.length===1?0:j*(w-105)/(loc.visits.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+(3===1?0:i*(w-105)/2),239));}
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>`)}
// De lichtpunten volgen de vijf bestaande SVG-verbindingen. Ze lopen alleen tijdens een actieve processtap.
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'};
const signalColors={data:'#34e6b3',analysis:'#ffcb62',insight:'#51b9ff',verify:'#b18aff',report:'#ff9a65'};
function clearSignal(){const layer=document.querySelector('.signal-layer');if(layer)layer.replaceChildren();}
function signalFor(agent){
clearSignal();
const layer=document.querySelector('.signal-layer');
if(!layer||!signalPaths[agent]||window.matchMedia('(prefers-reduced-motion: reduce)').matches)return;
const ns='http://www.w3.org/2000/svg';
for(const [radius,cls,delay] of [[7,'signal-halo','0s'],[3.2,'signal-particle','0s']]){
const circle=document.createElementNS(ns,'circle');circle.setAttribute('r',radius);circle.setAttribute('class',cls);circle.setAttribute('fill',signalColors[agent]);
const motion=document.createElementNS(ns,'animateMotion');motion.setAttribute('path',signalPaths[agent]);motion.setAttribute('dur','1.35s');motion.setAttribute('repeatCount','indefinite');motion.setAttribute('begin',delay);
circle.appendChild(motion);layer.appendChild(circle);
}
}
function reset(){if(running)return;clearStages();clearSignal();$('conductor').classList.remove('attention');completed=0;$('completed').textContent='0';$('conductorStatus').textContent='Gereed voor analyse';$('flowState').textContent='Wacht op startcommando';$('logStatus').textContent='Stand-by';$('logs').innerHTML='<div class="logitem muted">◌ &nbsp; Klik op ‘Start demo-analyse’ om de workflow te bekijken.</div>';$('orchestra-root');document.querySelector('.orchestra').classList.remove('running');$('conductor').classList.remove('running');ids.forEach(id=>{let e=$('agent-'+id);e.classList.remove('active','done');e.querySelector('.status').textContent='STANDBY';e.querySelector('.track i').style.width='0%';e.querySelector('.percent').textContent='0%'});}
// v0.5: voortgang is een presentatielaag, geen indicatie van de rekentijd.
function paceDelay(){return $('paceSelect').value==='fast'?170:1050;}
function stepDescriptions(agent, result, fake){
const n=v=>Number(v).toLocaleString('nl-NL');
const sites=result?.sites??0, rows=result?.rows??0, repeat=result?.duplicates??0;
const provisional=result?.visits??0, unique=result?.unique??0;
if(agent==='data')return fake?['Demobron geladen','4 voorbeeldlocaties','Fictieve scanregels','Democontrole afgerond','Overdracht aan analyse']:[
'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'];
if(agent==='analysis')return fake?['Voorbeeld-KPI’s geladen','Locaties vergeleken','Trends weergegeven','Groei samengevat','Overdracht aan inzicht']:[
'Dubbele scans uitgesloten',`${n(unique)} unieke cliënten in periode`,`${n(result.newIntake)} instromers in periode`,`${n(result.active)} actieve cliënten`, 'Overdracht aan Insight Agent'];
if(agent==='insight')return fake?['Voorbeeldtrend gelezen','Locatieverschillen bekeken','Fictieve inzichten verzameld','Conceptduiding opgesteld','Overdracht aan verificatie']:[
'Maandgegevens beschikbaar','Locaties in grafiek gezet','Signalen zonder causaliteit','AI-duiding niet aangesloten','Overdracht aan controle'];
if(agent==='verify')return fake?['Demovergelijking geopend','5 indicatoren bekeken','1 fictieve afwijking','Rapport niet gevalideerd','Overdracht met waarschuwing']:[
'Tweede telroute vanaf bronregels',`${result?.verification?.checks.length??0} onafhankelijke vergelijkingen`,`${result?.verification?.mismatches??0} interne telverschillen`,`${result?.verification?.sourceIssues.length??0} soorten bronproblemen`,'Extern niet gevalideerd'];
return fake?['Concept gestart','Voorbeeldcijfers verwerkt','Illustratie bijgewerkt','Niet gevalideerd gemarkeerd','Concept gereed']:[
'Concept gestart','KPI-preview bijgewerkt','Instroom op eerste scan berekend','Niet gevalideerd gemarkeerd','Concept gereed'];
}
function showStages(agent,steps){
const box=$('agent-'+agent);box.classList.add('has-steps');const wrap=box.querySelector('.agent-steps');wrap.replaceChildren();
steps.forEach((str,i)=>{const d=document.createElement('div');d.className='agent-step';d.innerHTML='<span class="step-mark">○</span><span class="step-label"></span>';d.querySelector('.step-label').textContent=str;wrap.appendChild(d)});
}
async function animateStages(agent,steps){
const el=$('agent-'+agent),items=[...el.querySelectorAll('.agent-step')];
const delay=paceDelay();
for(let i=0;i<items.length;i++){
items[i].classList.add('current');items[i].querySelector('.step-mark').textContent='◌';
$('flowState').textContent=steps[i];
await sleep(delay);
items[i].classList.remove('current');items[i].classList.add('complete');items[i].querySelector('.step-mark').textContent='✓';
el.querySelector('.track i').style.width=Math.round((i+1)*100/items.length)+'%';
el.querySelector('.percent').textContent=Math.round((i+1)*100/items.length)+'%';
}
}
function clearStages(){ids.forEach(id=>{const el=$('agent-'+id);el.classList.remove('has-steps');el.querySelector('.agent-steps').replaceChildren()})}
async function start(){if(importedData)return startImported();if(running)return;clearStages();clearSignal();$('conductor').classList.remove('attention');running=true;$('startBtn').disabled=true;$('resetBtn').disabled=true;completed=0;ids.forEach(id=>{const e=$('agent-'+id);e.classList.remove('done','active');e.querySelector('.status').textContent='STANDBY';e.querySelector('.track i').style.width='0%';e.querySelector('.percent').textContent='0%'});$('logs').innerHTML='';$('completed').textContent='0';$('conductor').classList.add('running');document.querySelector('.orchestra').classList.add('running');$('conductorStatus').textContent='Workflow wordt aangestuurd';$('logStatus').textContent='Actief';log('Dirigent toont de gesimuleerde workflow stap voor stap');try{const response=await Promise.resolve({ok:true,json:async()=>JSON.parse(document.getElementById('orchestra-demo-data').textContent)});if(!response.ok)throw Error('API reageert niet');demoData=await response.json();renderKPIs(demoData);renderChecks(demoData);drawChart(demoData);for(let i=0;i<demoData.steps.length;i++){const step=demoData.steps[i];const e=$('agent-'+step.agent);signalFor(step.agent);e.classList.add('active');e.querySelector('.status').textContent='ACTIEF';$('flowState').textContent=step.text;log('Start: '+e.querySelector('h3').textContent);const stageLabels=stepDescriptions(step.agent,null,true);showStages(step.agent,stageLabels);await animateStages(step.agent,stageLabels);clearSignal();e.classList.remove('active');e.classList.add('done');if(step.agent==='verify' && demoData.checks.some(c=>c.ours!==c.shadow))$('conductor').classList.add('attention');e.querySelector('.status').textContent=step.agent==='verify'?'AFWIJKING':'GEREED';completed++;$('completed').textContent=String(completed);log(step.text);await sleep(280)}$('conductorStatus').textContent='Analyse voltooid · controle vereist';$('flowState').textContent='Concept gereed · NIET GEVALIDEERD';$('logStatus').textContent='Afgerond';log('Eindstatus: concept beschikbaar, niet vrijgegeven');}catch(e){log('FOUT: '+e.message);$('flowState').textContent='Verwerking mislukt';$('conductorStatus').textContent='Fout gedetecteerd';}finally{clearSignal();running=false;$('startBtn').disabled=false;$('resetBtn').disabled=false;$('conductor').classList.remove('running');document.querySelector('.orchestra').classList.remove('running')}}
function reply(question){const q=question.toLowerCase();if(!completed)return 'Start eerst de demo-analyse. Daarna kan ik vragen beantwoorden over de fictieve cijfers.';if(/valid|wouter|controle|betrouw|verschil|akkoord/.test(q))return 'In deze fictieve demo wijken de unieke bezoekers af: monitor 386 tegenover schaduwberekening 381, een verschil van 5. De oorzaak is niet vastgesteld. Daarom blijft de infographic NIET GEVALIDEERD.';if(/groei|stijg|locatie|soeda|rietdijk|vredehof|gouda/.test(q))return 'In de fictieve reeks juli–september stijgt Soeda van 350 naar 428 bezoeken (+22,3%). Rietdijk stijgt van 285 naar 334 (+17,2%), Vredehof van 245 naar 294 (+20,0%) en Gouda van 320 naar 365 (+14,1%). Soeda heeft de grootste relatieve groei.';if(/samenvat|rapport|infographic|management/.test(q))return 'Conceptmanagementsamenvatting (fictief): alle vier getoonde locaties laten in juli–september groei in bezoeken zien. Er is een afwijking van 5 unieke bezoekers bij de schaduwcontrole. Publicatie is niet toegestaan totdat dit verschil is onderzocht en beoordeeld.';if(/bezoek|uniek|instroom|kwartaal|cijfer/.test(q))return 'De fictieve KPI-kaarten tonen 12.480 bezoeken, 1.286 unieke bezoekers en 164 nieuwe instromers. Deze totaalcijfers zijn illustratief en vormen geen optelsom van de vier voorbeeldlocaties in de grafiek.';return 'Dit is een demo-chat zonder AI-API. Ik kan alleen vooraf gedefinieerde vragen over de fictieve locatiegroei, KPI’s, verificatie en managementsamenvatting beantwoorden.'}
function addMessage(role,text){const e=document.createElement('div');e.className='message '+role;if(role==='assistant'){const s=document.createElement('small');s.textContent='INLOOP AI · DEMONSTRATIE';e.appendChild(s)}const p=document.createElement('p');p.textContent=text;e.appendChild(p);$('messages').appendChild(e);$('messages').scrollTop=$('messages').scrollHeight;}
function ask(q){if(!q.trim())return;addMessage('user',q);addMessage('assistant',reply(q));}
// v0.4: veilige browser-lokale Excel-proef, uitsluitend met fictieve / geanonimiseerde voorbeelden.
// Er vindt GEEN netwerkverzoek, bestandsoverdracht of externe AI-aanroep plaats.
let importedData=null;
const excelLimit=25*1024*1024; // limiet per afzonderlijk XLSX-bestand
const REPORT_YEAR='2026';
const REPORT_SITES=['De Brink','De Kerk Den Haag','Gouda','Haagse Zaak','Hoorn','IJmond','Platinaweg','Purmerend','Rietdijk','Rozebottel','Soeda','Sterhuis','Voorburg','Vredehof','Zinkwerf'];
const siteCanonical=new Map(REPORT_SITES.map(s=>[s.toLocaleLowerCase('nl-NL'),s]));
function allowedSite(label){const raw=String(label??'').trim();if(!raw||raw.length>80)return '';return siteCanonical.get(raw.toLocaleLowerCase('nl-NL'))||raw;}
function historyKey(site,id){return JSON.stringify([site,id]);}
function validClientId(value){
const id=String(value??'').trim();
if(!id||/^0+(?:[.,]0+)?$/.test(id)||/^(?:#.*|n\/a|nan|none|null|undefined|false|onwaar|geen)$/i.test(id))return '';
return id;
}
function setHistoryStatus(message,cls=''){const el=$('historyStatus');el.textContent=message;el.className='history-status '+cls;}
const excelNS='http://schemas.openxmlformats.org/spreadsheetml/2006/main';
const decoder=new TextDecoder('utf-8');
function bytesText(a){return decoder.decode(a)}
function zipDirectory(buffer){
const bytes=new Uint8Array(buffer), view=new DataView(buffer);
let eocd=-1;
for(let i=bytes.length-22;i>=Math.max(0,bytes.length-65557);i--){if(view.getUint32(i,true)===0x06054b50){eocd=i;break}}
if(eocd<0)throw Error('Geen geldig XLSX-/ZIP-bestand');
const count=view.getUint16(eocd+10,true), offset=view.getUint32(eocd+16,true);
if(count>1000)throw Error('Dit testbestand heeft te veel onderdelen');
const entries=new Map();let pos=offset;
for(let i=0;i<count;i++){
if(view.getUint32(pos,true)!==0x02014b50)throw Error('ZIP-structuur ongeldig');
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);
const name=bytesText(bytes.subarray(pos+46,pos+46+len));
if(name.startsWith('xl/'))entries.set(name,{method,size,inflated,local});
pos+=46+len+extra+comment;
}
return {view,bytes,entries};
}
async function xmlEntry(zip,name){
const entry=zip.entries.get(name);if(!entry)return '';
if(entry.inflated>180*1024*1024)throw Error('Een Excel-onderdeel is uitgepakt groter dan 180 MB; bestand eerst opsplitsen');
const {view,bytes}=zip, off=entry.local;
if(view.getUint32(off,true)!==0x04034b50)throw Error('ZIP-bestandskop ongeldig');
const start=off+30+view.getUint16(off+26,true)+view.getUint16(off+28,true);
const compressed=bytes.slice(start,start+entry.size);
if(entry.method===0)return bytesText(compressed);
if(entry.method!==8)throw Error('Niet-ondersteunde Excel-compressie');
if(typeof DecompressionStream!=='function')throw Error('Deze browser ondersteunt lokale Excel-decompressie niet');
return await new Response(new Blob([compressed]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).text();
}
function xmlDoc(str){
const doc=new DOMParser().parseFromString(str,'application/xml');
if(doc.getElementsByTagName('parsererror').length)throw Error('Excel-XML niet leesbaar');return doc;
}
// v1.3: rijgewijs XML uitlezen; geen DOM-boom van 100+ MB grote werkbladen.
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})}
function parseSharedLite(xml){
const arr=[];const si=/<(?:[\w.-]+:)?si\b[^>]*>([\s\S]*?)<\/(?:[\w.-]+:)?si>/g;let m;
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)}
return arr;
}
function *sheetRows(xml,shared){
const rowRe=/<(?:[\w.-]+:)?row\b[^>]*>([\s\S]*?)<\/(?:[\w.-]+:)?row>/g, cellRe=/<(?:[\w.-]+:)?c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/(?:[\w.-]+:)?c>)/g;let row;
while((row=rowRe.exec(xml))!==null){const values={};let c;cellRe.lastIndex=0;
while((c=cellRe.exec(row[1]))!==null){const col=/\br="([A-Z]+)\d+"/.exec(c[1]);if(!col)continue;
const type=/\bt="([^"]+)"/.exec(c[1])?.[1]||'';const payload=c[2]||'';
let v=/<(?:[\w.-]+:)?v(?:\s[^>]*)?>([\s\S]*?)<\/(?:[\w.-]+:)?v>/.exec(payload)?.[1]??'';
if(type==='s')v=shared[Number(v)]??'';
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]);}
else v=decodeXML(v);
values[col[1]]=v;
}
yield values;
}
}
function scanDate(v){
if(v===''||v===undefined||v===null)return '';
const numeric=Number(v);
if(v!==''&&Number.isFinite(numeric)&&numeric>30000&&numeric<80000){
const d=new Date(Date.UTC(1899,11,30)+Math.floor(numeric)*86400000);
return d.toISOString().slice(0,10);
}
const str=String(v).trim(),iso=str.match(/^(\d{4})-(\d{2})-(\d{2})/);
if(iso)return `${iso[1]}-${iso[2]}-${iso[3]}`;
const nl=str.match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})/);
if(nl)return `${nl[3]}-${nl[2].padStart(2,'0')}-${nl[1].padStart(2,'0')}`;
return '';
}
async function readExcelLocal(file,onProgress=()=>{}){
if(!file.name.toLowerCase().endsWith('.xlsx'))throw Error('Kies een XLSX-bestand');
if(file.size>excelLimit)throw Error('Dit bestand is groter dan de v1.3-testgrens van 25 MB');
onProgress('Excel wordt geopend ('+(file.size/1024/1024).toFixed(1)+' MB)…');
const zip=zipDirectory(await file.arrayBuffer()),shared=[];
const scanMeta=zip.entries.get('xl/worksheets/sheet1.xml');
if(!scanMeta)throw Error('Werkblad Data dump (sheet1) ontbreekt');
onProgress('Excel-structuur gecontroleerd · werkbladen worden gelezen…');
const sharedXML=await xmlEntry(zip,'xl/sharedStrings.xml');
if(sharedXML){for(const item of parseSharedLite(sharedXML))shared.push(item);}
const pick=(l,...names)=>names.map(n=>l[n]).find(Boolean)||null;
const passToClient=new Map(),clientToPasses=new Map(),passConflicts=new Set();let passRows=0,passMissing=0;
const pasDates={startPresent:0,endPresent:0,migration:0,invalidDates:0,notAvailable:false,activeAtPeildatum:0};
const passDateRecords=[];
const invalidDatePasses=new Set();
// Eerst de pasregistratie, zodat scans zonder direct cliëntnummer gekoppeld kunnen worden.
const passXML=await xmlEntry(zip,'xl/worksheets/sheet2.xml');
if(passXML){
const it=sheetRows(passXML,shared),first=it.next();
if(!first.done){const pl={};for(const [col,v] of Object.entries(first.value))pl[String(v).toLowerCase().trim()]=col;
const pc=pick(pl,'pasnummer','scanpasnummer','pas nummer','pasnr'),ic=pick(pl,'clientnummer','cliëntnummer'),startCol=pick(pl,'startdatum'),endCol=pick(pl,'einddatum');
pasDates.notAvailable=!(startCol&&endCol);
if(!pc||!ic)throw Error('Scanpassen mist kolommen Pasnummer en Clientnummer');
for(const r of it){const p=String(r[pc]??'').trim(),c=validClientId(r[ic]);passRows++;
if(passRows>350000)throw Error('Meer dan 350.000 pasregistraties; contacteer ontwikkelaar voor grotere import');
const st=startCol?scanDate(r[startCol]):'',en=endCol?scanDate(r[endCol]):'';
if(st)pasDates.startPresent++;if(en)pasDates.endPresent++;
if(st==='2022-10-28')pasDates.migration++;if(st&&en&&en<st){pasDates.invalidDates++;if(p)invalidDatePasses.add(p);}
if(c&&p)passDateRecords.push({client:c,start:st,end:en});
if(!p||!c){passMissing++;continue}
if(passToClient.has(p)&&passToClient.get(p)!==c)passConflicts.add(p);
else passToClient.set(p,c);
if(!clientToPasses.has(c))clientToPasses.set(c,new Set());clientToPasses.get(c).add(p);
}
}
}
onProgress('Scanpassen verwerkt ('+passRows.toLocaleString('nl-NL')+') · scanhistorie wordt gelezen…');
await new Promise(resolve=>setTimeout(resolve,0));
const sheet=await xmlEntry(zip,'xl/worksheets/sheet1.xml');
if(!sheet)throw Error('Werkblad Data dump ontbreekt');
const it=sheetRows(sheet,shared),first=it.next();if(first.done)throw Error('Werkblad Data dump is leeg');
const lookup={};for(const [col,name] of Object.entries(first.value))lookup[String(name).toLowerCase().trim()]=col;
const dateCol=pick(lookup,'scantijd','scandatum'),siteCol=pick(lookup,'locatie'),idCol=pick(lookup,'clientnummer','cliëntnummer'),passColumn=pick(lookup,'pasnummer','scanpasnummer','pas nummer','pasnr');
if(!dateCol||!siteCol||(!idCol&&!passColumn))throw Error('Kolommen scantijd, locatie en cliëntnummer of pasnummer ontbreken');
const all=[],invalid={datum:0,locatie:0,client:0},unmatchedPasses=new Set(),identityConflicts=new Set();let linkedByPass=0,rowsWithPass=0,rawCount=0,ignoredSites=0,resultZero=0,resultZeroNoIdentity=0;
const qualityRows=[],undatedBySite=new Map(),excludedFalseRows=[]; // Alleen minimale foutsignalen; geen identifiers.
const outcomeColumn=pick(lookup,'resultaat');
for(const r of it){rawCount++;
if(rawCount>500000)throw Error('Meer dan 500.000 scans: importlimiet v1.3 bereikt; bestand opsplitsen');
const site=allowedSite(r[siteCol]),date=scanDate(r[dateCol]);
if(!site){ignoredSites++;continue;}
const pass=passColumn?String(r[passColumn]??'').trim():'',directId=idCol?validClientId(r[idCol]):'';
const resolved=pass&&!passConflicts.has(pass)?passToClient.get(pass):null;
const id=directId||resolved||'';
// Afgekeurde/onware scans (Resultaat=0) tellen niet mee in 2026 en ook niet in de historische referentie.
if(outcomeColumn&&['0','false','onwaar'].includes(String(r[outcomeColumn]??'').trim().toLowerCase())){
resultZero++;if(!id)resultZeroNoIdentity++;excludedFalseRows.push({site,date});continue;
}
if(pass)rowsWithPass++;
if(pass&&passXML&&!resolved)unmatchedPasses.add(pass);
if(directId&&resolved&&directId!==resolved)identityConflicts.add(pass);
if(!directId&&resolved)linkedByPass++;
if(!date){invalid.datum++;undatedBySite.set(site,(undatedBySite.get(site)||0)+1);}if(!id)invalid.client++;
const badPassConflict=!!pass&&passConflicts.has(pass);
const badClientMatch=!!directId&&!!resolved&&directId!==resolved;
const badUnknownPass=!!pass&&!!passXML&&!resolved;
const badPassDate=!!pass&&invalidDatePasses.has(pass);
if(date&&date.slice(0,4)===REPORT_YEAR&&(badPassConflict||badClientMatch||badUnknownPass||badPassDate||!id)){
qualityRows.push({site,date,hasIdentity:!!id,hasDirectId:!!directId,
passConflict:badPassConflict,clientMismatch:badClientMatch,unknownPass:badUnknownPass,badPassDate});
}
if(!id||!date||date.slice(0,4)!==REPORT_YEAR)continue;
// Alleen ware, aan een cliënt gekoppelde scans tellen; vervangende passen blijven op cliëntnummer samengevoegd.
all.push({date,site,id,pass});
}
if(!all.length)throw Error('Geen geldige (Resultaat=1) scanregels over 2026; controleer bestand, locatienamen en status');
const sites=[...new Set(all.map(r=>r.site))].sort((a,b)=>a.localeCompare(b,'nl'));
const peildatum=all.reduce((m,r)=>r.date>m?r.date:m,'');
const activeIds=new Set(passDateRecords.filter(r=>r.start&&r.end&&r.start<=peildatum&&r.end>=peildatum).map(r=>r.client));
pasDates.activeAtPeildatum=activeIds.size;
onProgress('Ingelezen: '+rawCount.toLocaleString('nl-NL')+' scanregels · '+sites.length+' locaties');
return {all,sites,peildatum,pasDates,rawCount,invalid,hasPassColumn:!!passColumn,
passSheetFound:!!passXML,passRows,passMissing,passToClient,clientToPasses,
passConflicts:passConflicts.size,identityConflicts:identityConflicts.size,
unmatchedPasses:unmatchedPasses.size,linkedByPass,rowsWithPass,ignoredSites,resultZero,resultZeroNoIdentity,excludedFalseRows,
qualityRows,undatedBySite,
historyFirst:new Map(),historyLoaded:false,historyInfo:null};
}
// v1.5: historische index met uitsluitend de eerste scan per cliënt/locatie.
// Lees GEEN inhoud van het pasregister van 2025 en bewaar geen individuele scanregels uit 2025.
async function readHistoricFirsts(file,onProgress=()=>{}){
if(!file.name.toLowerCase().endsWith('.xlsx'))throw Error('Kies een XLSX-bestand voor 2025');
if(file.size>excelLimit)throw Error('2025-bestand overschrijdt de limiet van 25 MB');
onProgress('Referentie 2025 openen ('+(file.size/1024/1024).toFixed(1)+' MB)…');
const zip=zipDirectory(await file.arrayBuffer());
const sharedXML=await xmlEntry(zip,'xl/sharedStrings.xml');
const shared=sharedXML?parseSharedLite(sharedXML):[];
onProgress('2025: scanregels controleren en eerste bezoekdatums verzamelen…');
await new Promise(resolve=>setTimeout(resolve,0));
const sheet=await xmlEntry(zip,'xl/worksheets/sheet1.xml');
if(!sheet)throw Error('Scanwerkblad 2025 ontbreekt');
const it=sheetRows(sheet,shared),first=it.next();
if(first.done)throw Error('Het scanwerkblad 2025 is leeg');
const cols={};for(const [col,v] of Object.entries(first.value))cols[String(v).toLowerCase().trim()]=col;
const dateCol=cols.scantijd||cols.scandatum,siteCol=cols.locatie,idCol=cols.clientnummer||cols['cliëntnummer'];
if(!dateCol||!siteCol||!idCol)throw Error('2025 mist Scantijd, Locatie of Clientnummer');
const firsts=new Map();let raw=0,valid=0,otherSites=0,missingIds=0,otherYears=0,falseScans=0;
const resultCol=cols.resultaat;
for(const r of it){raw++;if(raw>500000)throw Error('2025 bevat meer dan 500.000 scanregels');
// Tel alle onware scans, ook de uitzonderlijke regels zonder locatienaam; gebruik geen van deze voor historie.
if(resultCol&&['0','false','onwaar'].includes(String(r[resultCol]??'').trim().toLowerCase())){falseScans++;continue;}
const site=allowedSite(r[siteCol]);if(!site){otherSites++;continue;}
const date=scanDate(r[dateCol]);if(!date||date.slice(0,4)!=='2025'){otherYears++;continue;}
const id=validClientId(r[idCol]);if(!id){missingIds++;continue;}
valid++;const key=historyKey(site,id);
if(!firsts.has(key)||date<firsts.get(key))firsts.set(key,date);
}
if(!valid)throw Error('Geen geldige historische scans uit 2025 in de beschikbare locaties');
onProgress('Referentie 2025 gereed: '+valid.toLocaleString('nl-NL')+' bruikbare scanregels');
return {firsts,info:{raw,valid,otherSites,missingIds,otherYears,falseScans,people:firsts.size}};
}
// v1.6.2 — unieke cliënten binnen periode; historische referentie alleen voor nieuwe instroom.
function quarterKey(date){return date.slice(0,4)+'-Q'+(Math.floor((Number(date.slice(5,7))-1)/3)+1);}
function quarterDates(key){
const m=String(key).match(/^(\d{4})-Q([1-4])$/);if(!m)throw Error('Kies een geldig rapportagekwartaal');
const year=Number(m[1]),q=Number(m[2]),month=(q-1)*3+1;
const begin=`${year}-${String(month).padStart(2,'0')}-01`;
const end=new Date(Date.UTC(year,month+2,0)).toISOString().slice(0,10);
return {begin,end,key,label:`Q${q} ${year}`,months:Array.from({length:3},(_,i)=>`${year}-${String(month+i).padStart(2,'0')}`)};
}
function precedingThreeMonths(peildatum){
// Rolling interval: (peildatum - 3 kalendermaanden, peildatum], dag-afgekapt.
// Bijvoorbeeld 30 september -> 1 juli; 25 september -> 26 juni.
const [year,month,day]=peildatum.split('-').map(Number);
const base=new Date(Date.UTC(year,month-1-3,1));
const lastDay=new Date(Date.UTC(base.getUTCFullYear(),base.getUTCMonth()+1,0)).getUTCDate();
const earlier=new Date(Date.UTC(base.getUTCFullYear(),base.getUTCMonth(),Math.min(day,lastDay)));
earlier.setUTCDate(earlier.getUTCDate()+1);
return earlier.toISOString().slice(0,10);
}
function readableDate(date){if(!date)return '—';const [y,m,d]=date.split('-');return `${d}-${m}-${y}`;}
function firstScansBySite(all){
const first=new Map();
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);}
return first;
}
function reportPeriodDates(key){
const match=String(key).match(/^(\d{4})-(Q[1-4]|H[12]|M9)$/);
if(!match)throw Error('Ongeldige rapportageperiode');
const year=Number(match[1]),part=match[2];
const first=part.startsWith('Q')?(Number(part[1])-1)*3+1:part==='H2'?7:1;
const count=part.startsWith('Q')?3:part.startsWith('H')?6:9;
const begin=`${year}-${String(first).padStart(2,'0')}-01`;
const end=new Date(Date.UTC(year,first+count-1,0)).toISOString().slice(0,10);
const months=Array.from({length:count},(_,i)=>`${year}-${String(first+i).padStart(2,'0')}`);
const label=part==='Q1'?`Januari t/m maart ${year}`:part==='H1'?`Januari t/m juni ${year}`:part==='M9'?`Januari t/m september ${year}`:part.startsWith('Q')?`Q${part[1]} ${year}`:`H${part[1]} ${year}`;
return {key,begin,end,months,label,type:part.startsWith('Q')?'quarter':part.startsWith('H')?'half':'nine'};
}
function availablePeriods(type){
const dataMonths=[...new Set(importedData.all.map(r=>r.date.slice(0,7)))].sort();
const years=[REPORT_YEAR];
const keys=[];
for(const year of years){
const suffixes=type==='quarter'?['Q1']:type==='half'?['H1']:['M9'];
for(const suffix of suffixes){
const key=year+'-'+suffix,p=reportPeriodDates(key);
if(dataMonths.some(month=>month>=p.months[0]&&month<=p.months.at(-1)))keys.push(key);
}
}
return keys;
}
function refreshPeriodChoices(prefer){
const select=$('periodSelect'),keys=availablePeriods($('periodType').value);
select.replaceChildren();
keys.forEach(key=>{const opt=document.createElement('option');opt.value=key;opt.textContent=reportPeriodDates(key).label;select.appendChild(opt)});
if(prefer&&keys.includes(prefer))select.value=prefer;
select.disabled=!keys.length;
if(!keys.length){const opt=document.createElement('option');opt.textContent='Geen perioden beschikbaar';select.appendChild(opt)}
}
function periodContext(key){
const p=reportPeriodDates(key);
const seen=importedData.all.reduce((max,r)=>r.date>max?r.date:max,'');
// Onvolledig nieuwste tijdvak: laatste beschikbare scandag is de peildatum.
const latestInPeriod=importedData.all.reduce((max,r)=>r.date>=p.begin&&r.date<=p.end&&r.date>max?r.date:max,'');
const peildatum=seen<p.end?latestInPeriod:p.end;
if(!peildatum)throw Error('Geen scans in de geselecteerde rapportageperiode');
return {...p,peildatum,activeFrom:precedingThreeMonths(peildatum)};
}
function calculateImported(site='*'){
if(!importedData)throw Error('Laad eerst fictieve gegevens');
const chosenPeriod=$('periodSelect').value;
const period=periodContext(chosenPeriod);
const locationRows=importedData.all.filter(r=>site==='*'||r.site===site);
const historicRows=locationRows.filter(r=>r.date<=period.peildatum);
const quarterRows=historicRows.filter(r=>r.date>=period.begin&&r.date<=period.peildatum);
const visitKeys=new Set(),clientKeys=new Set(),quarterClientKeys=new Set(),activeKeys=new Set(),monthSite=new Map();
const instroomByMonth=new Map(period.months.map(m=>[m,0]));
const firstByClient=new Map(),firstWithoutReference=new Map();
// Historische bezoeken uit 2025 bepalen de vroegste geregistreerde scan,
// maar dragen NOOIT bij aan bezoeken, actieve cliënten of maandgrafieken over 2026.
if(importedData.historyLoaded){for(const [key,date] of importedData.historyFirst){
if(site==='*'||JSON.parse(key)[0]===site)firstByClient.set(key,date);
}}
for(const r of historicRows){
const key=historyKey(r.site,r.id);clientKeys.add(key);
if(!firstWithoutReference.has(key)||r.date<firstWithoutReference.get(key))firstWithoutReference.set(key,r.date);
if(!firstByClient.has(key)||r.date<firstByClient.get(key))firstByClient.set(key,r.date);
if(r.date>=period.activeFrom)activeKeys.add(key);
}
let instroom=0;
for(const [key,date] of firstByClient){
if(date>=period.begin&&date<=period.peildatum){instroom++;
const month=date.slice(0,7);instroomByMonth.set(month,(instroomByMonth.get(month)||0)+1);
}
}
for(const r of quarterRows){
const ck=JSON.stringify([r.site,r.id]);quarterClientKeys.add(ck);
const key=JSON.stringify([r.site,r.date,r.id]);if(visitKeys.has(key))continue;
visitKeys.add(key);
const mk=r.site+'|'+r.date.slice(0,7);monthSite.set(mk,(monthSite.get(mk)||0)+1);
}
const active=activeKeys.size,unique=quarterClientKeys.size; // Wouter: unieke cliënten = minstens één geldige scan in geselecteerde periode
const priorCandidates=[...firstWithoutReference].filter(([key,date])=>date>=period.begin&&date<=period.peildatum);
const preHistoryIntake=priorCandidates.length;
const correctedExisting=priorCandidates.filter(([key])=>importedData.historyLoaded&&importedData.historyFirst.has(key)).length;
const correctedJanuary=priorCandidates.filter(([key,date])=>date.slice(0,7)==='2026-01'&&importedData.historyLoaded&&importedData.historyFirst.has(key)).length;
const passtiedIds=new Set(historicRows.map(r=>r.id));
const passByClient=new Map();let missingPass=0;
for(const r of historicRows){if(r.pass){if(!passByClient.has(r.id))passByClient.set(r.id,new Set());passByClient.get(r.id).add(r.pass)}else missingPass++;}
let multiPassClients=0;
if(importedData.passSheetFound){for(const [client,passes] of importedData.clientToPasses)if(passtiedIds.has(client)&&passes.size>1)multiPassClients++;}
else multiPassClients=[...passByClient.values()].filter(p=>p.size>1).length;
const locations=(site==='*'?importedData.sites:[site]).map(name=>({name,visits:period.months.map(m=>monthSite.get(name+'|'+m)||0)}));
const excludedFalse=importedData.excludedFalseRows.filter(r=>(site==='*'||r.site===site)&&r.date>=period.begin&&r.date<=period.peildatum).length;
return {site,period,rows:quarterRows.length,historyRows:historicRows.length,visits:visitKeys.size,unique,
quarterClients:quarterClientKeys.size,newIntake:instroom,active,firstByClient,instroomByMonth,
preHistoryIntake,correctedExisting,correctedJanuary,excludedFalse,historyLoaded:importedData.historyLoaded,
historyPeople:importedData.historyLoaded?importedData.historyInfo.people:0,
duplicates:quarterRows.length-visitKeys.size,months:period.months,locations,
invalid:importedData.invalid,passChecked:importedData.hasPassColumn||importedData.passSheetFound,
passSheetFound:importedData.passSheetFound,pasDates:importedData.pasDates,peildatum:period.peildatum,multiPassClients,
passConflicts:importedData.passConflicts,identityConflicts:importedData.identityConflicts,
unmatchedPasses:importedData.unmatchedPasses,linkedByPass:importedData.linkedByPass,missingPass,passRows:importedData.passRows};
}
// v1.6: Verifieer bronafwijkingen apart. De conclusie "geen direct effect" geldt alleen
// als het betreffende veld niet in de KPI-rekenregels wordt gebruikt of iedere
// betroffen scan een rechtstreeks cliëntnummer heeft. Mogelijke effecten blijven waarschuwingen.
function assessSourceIssues(result){
const source=importedData,site=result.site,{begin,peildatum}=result.period;
const selected=source.qualityRows.filter(r=>(site==='*'||r.site===site)&&r.date>=begin&&r.date<=peildatum);
const count=f=>selected.filter(f).length;
const undated=site==='*'?[...source.undatedBySite.values()].reduce((a,b)=>a+b,0):(source.undatedBySite.get(site)||0);
const issues=[];
const add=(label,total,impact,detail)=>{if(total>0)issues.push({label,total,impact,detail});};
const badDateScan=count(r=>r.badPassDate);
add('Pasregistraties met onlogische start-/einddatum',result.pasDates?.invalidDates||0,'none',
`${badDateScan.toLocaleString('nl-NL')} scanregel${badDateScan===1?'':'s'} in deze rapportage ${badDateScan===1?'gebruikt':'gebruiken'} een van deze passen. Pasdatums tellen niet mee bij bezoekdagen, instroom of actieve cliënten; daarom geen direct rekenkundig effect. Controleer de bronregistratie wel.`);
const conflictRows=count(r=>r.passConflict),conflictExcluded=count(r=>r.passConflict&&!r.hasIdentity);
add('Pasnummer gekoppeld aan verschillende cliënten',result.passConflicts||0,conflictExcluded>0?'possible':'unknown',
`${conflictRows.toLocaleString('nl-NL')} betreffende scanregel${conflictRows===1?'':'s'} in deze periode, waarvan ${conflictExcluded.toLocaleString('nl-NL')} zonder koppelbaar cliëntnummer uitgesloten. Bij overige regels geldt het cliëntnummer uit de scan; de identiteit moet worden nagekeken.`);
const mismatchRows=count(r=>r.clientMismatch);
add('Cliëntnummer scan wijkt af van pasregistratie',result.identityConflicts||0,'possible',
`${mismatchRows.toLocaleString('nl-NL')} betreffende scanregel${mismatchRows===1?'':'s'} in deze periode. Het rechtstreeks geregistreerde cliëntnummer wordt gebruikt. Een onjuiste koppeling kan cliënt- en instroomaantallen beïnvloeden; niet automatisch te kwantificeren.`);
const unknownRows=count(r=>r.unknownPass),unknownExcluded=count(r=>r.unknownPass&&!r.hasIdentity);
add('Niet-herkende of niet eenduidige scanpassen',result.unmatchedPasses||0,unknownExcluded>0?'possible':'none',
`${unknownRows.toLocaleString('nl-NL')} betrokken scanregel${unknownRows===1?'':'s'} in deze periode; ${unknownExcluded.toLocaleString('nl-NL')} ${unknownExcluded===1?'is':'zijn'} zonder cliëntnummer uitgesloten. ${unknownExcluded>0?'Daardoor kunnen de KPI’s te laag zijn.':'Alle betrokken scanregels hebben een rechtstreeks cliëntnummer en tellen gewoon mee.'}`);
const excludedNoId=count(r=>!r.hasIdentity);
add('Scans zonder koppelbaar cliëntnummer',excludedNoId,'possible',
`${excludedNoId.toLocaleString('nl-NL')} regel${excludedNoId===1?'':'s'} in deze locatie/periode ${excludedNoId===1?'is':'zijn'} niet meegenomen. Zonder identiteit kan ORCHESTRA geen betrouwbare unieke cliënt of bezoekdag vaststellen; mogelijke onderschatting.`);
add('Scans zonder bruikbare scandatum',undated,'unknown',
`Deze regels zijn uitgesloten. Hun periode is niet vast te stellen; het getal betreft de gekozen locatie over het hele 2026-bestand, niet uitsluitend deze rapportageperiode. Mogelijke onderschatting.`);
return issues;
}
// v1.1: onafhankelijke controletelling. Geen hergebruik van de KPI-sets uit calculateImported.
function independentlyVerify(result){
const {begin,peildatum,activeFrom,months}=result.period;
const rows=importedData.all.filter(r=>(result.site==='*'||r.site===result.site)&&r.date<=peildatum);
const people=new Map(),visits=new Map(),monthFirst=new Map(months.map(m=>[m,0])),seen2026=new Map();
if(importedData.historyLoaded){for(const [key,date] of importedData.historyFirst){
if(result.site==='*'||JSON.parse(key)[0]===result.site)people.set(key,{first:date,recent:false,inQuarter:false});
}}
for(const r of rows){
const key=historyKey(r.site,r.id);
if(!seen2026.has(key)||r.date<seen2026.get(key))seen2026.set(key,r.date);
let p=people.get(key);
if(!p){p={first:r.date,recent:false,inQuarter:false};people.set(key,p);}
if(r.date<p.first)p.first=r.date;
if(r.date>=activeFrom&&r.date<=peildatum)p.recent=true;
if(r.date>=begin&&r.date<=peildatum){
p.inQuarter=true;
const vkey=JSON.stringify([r.site,r.id,r.date]);
visits.set(vkey,(visits.get(vkey)||0)+1);
}
}
let intake=0,active=0,quarterClients=0;
for(const p of people.values()){
if(p.recent)active++;
if(p.inQuarter)quarterClients++;
if(p.first>=begin&&p.first<=peildatum){intake++;const m=p.first.slice(0,7);monthFirst.set(m,monthFirst.get(m)+1);}
}
const inQuarterRows=[...visits.values()].reduce((s,n)=>s+n,0);
const shadow={unique:quarterClients,newIntake:intake,active,visits:visits.size,quarterClients,
duplicates:inQuarterRows-visits.size,monthNew:[...monthFirst.values()].reduce((a,b)=>a+b,0)};
const uncorrected=[...seen2026].filter(([key,date])=>date>=begin&&date<=peildatum);
const independentlyRejected=uncorrected.filter(([key])=>importedData.historyLoaded&&importedData.historyFirst.has(key)).length;
const janRejected=uncorrected.filter(([key,date])=>date.slice(0,7)==='2026-01'&&importedData.historyLoaded&&importedData.historyFirst.has(key)).length;
const checks=[
{label:'Unieke cliënten in periode · tweede telling',ours:result.unique,shadow:shadow.unique},
{label:'Nieuwe instroom · eerste scan',ours:result.newIntake,shadow:shadow.newIntake},
{label:'Actieve cliënten · laatste 3 maanden',ours:result.active,shadow:shadow.active},
{label:'Unieke bezoekdagen · cliënt/locatie/dag',ours:result.visits,shadow:shadow.visits},
{label:'Bezoekers rapportageperiode',ours:result.quarterClients,shadow:shadow.quarterClients},
{label:'Herhaalde scans in periode',ours:result.duplicates,shadow:shadow.duplicates},
{label:'Maandinstroom versus periode-instroom',ours:result.newIntake,shadow:shadow.monthNew}
];
if(importedData.historyLoaded){
checks.push({label:'Correctie instroom door historie 2025',ours:result.correctedExisting,shadow:independentlyRejected});
checks.push({label:'Correctie januari door historie 2025',ours:result.correctedJanuary,shadow:janRejected});
}
const mismatches=checks.filter(c=>c.ours!==c.shadow).length;
const sourceIssues=assessSourceIssues(result);
return {checks,mismatches,sourceIssues,passed:mismatches===0&&sourceIssues.length===0};
}
function drawImportedChart(result){
const cvs=$('chart'),ctx=cvs.getContext('2d'),w=cvs.width,h=cvs.height;
ctx.clearRect(0,0,w,h);ctx.font='13px Segoe UI';
const series=result.locations.slice(0,4),max=Math.max(10,...series.flatMap(x=>x.visits))*1.15;
ctx.strokeStyle='#29405a';ctx.fillStyle='#8aa4c3';ctx.textAlign='left';
for(let i=0;i<=4;i++){const y=25+i*45;ctx.beginPath();ctx.moveTo(44,y);ctx.lineTo(w-14,y);ctx.stroke();ctx.fillText(String(Math.round(max*(4-i)/4)),7,y+4)}
const colors=['#3ee1b6','#ffc66a','#5b9cff','#ad88ff'];
series.forEach((loc,li)=>{
ctx.strokeStyle=colors[li];ctx.lineWidth=3;ctx.beginPath();
loc.visits.forEach((v,j)=>{const x=72+(loc.visits.length===1?0:j*(w-105)/(loc.visits.length-1)),y=205-(v/max)*180;j===0?ctx.moveTo(x,y):ctx.lineTo(x,y)});ctx.stroke();
loc.visits.forEach((v,j)=>{const x=72+(loc.visits.length===1?0:j*(w-105)/(loc.visits.length-1)),y=205-(v/max)*180;ctx.fillStyle=colors[li];ctx.beginPath();ctx.arc(x,y,4,0,Math.PI*2);ctx.fill()});
});ctx.fillStyle='#a6b9d0';ctx.textAlign='center';
result.months.forEach((m,i)=>ctx.fillText(m,72+(result.months.length===1?0:i*(w-105)/(result.months.length-1)),239));
const legend=document.querySelector('.legend');
legend.innerHTML=series.map((loc,i)=>`<span><i style="background:${colors[i]}"></i> ${escapeHTML(loc.name)}</span>`).join('');
document.querySelector('.chart-panel .subtle').textContent=result.months.join(' · ')+(result.locations.length>4?' · 4 van '+result.locations.length+' locaties':'');
}
function renderImported(result){
const num=n=>n.toLocaleString('nl-NL');
const kpis=[{label:'Unieke bezoekdagen',value:num(result.visits),change:result.period.label+' · cliënt / dag / locatie'},
{label:'Unieke cliënten',value:num(result.unique),change:'Min. 1 geldige scan in gekozen periode'},
{label:'Nieuwe instroom',value:num(result.newIntake),change:'Eerste geldige scan in '+result.period.label+(result.historyLoaded?' · met historie 2025':' · voorlopig zonder 2025')},
{label:'Actieve cliënten',value:num(result.active),change:'Laatste 3 maanden t/m peildatum'}];
renderKPIs({kpis});drawImportedChart(result);
const quality=result.verification||independentlyVerify(result);
const lines=[
['Geselecteerde periode',result.period.label+' ('+readableDate(result.period.begin)+' t/m '+readableDate(result.period.peildatum)+')'],
['Actieve cliënten: meetperiode',readableDate(result.period.activeFrom)+' t/m '+readableDate(result.peildatum)],
['Unieke cliënten in gekozen periode',num(result.unique)],
['Nieuwe instroom (per locatie)',num(result.newIntake)],
['Actieve cliënten (per locatie)',num(result.active)],
['Cliënten periode · extra controletelling',num(result.quarterClients)],
['Unieke bezoekdagen periode',num(result.visits)],
['Herhaalde scans periode',num(result.duplicates)],
['Historisch referentiebestand 2025',result.historyLoaded?'INGELEZEN · '+num(result.historyPeople)+' cliënt/locatie-combinaties':'NIET AANGELEVERD · instroom voorlopig'],
['Resultaat=0 uitgesloten uit historie 2025',result.historyLoaded?num(importedData.historyInfo.falseScans):'NIET AANGELEVERD'],
['Nieuwe instroom zonder 2025',num(result.preHistoryIntake)],
['Herkenbaar als bestaande cliënt in 2025',result.historyLoaded?num(result.correctedExisting):'NIET TE BEPALEN'],
['Waarvan januari 2026',result.historyLoaded?num(result.correctedJanuary):'NIET TE BEPALEN'],
['Resultaat=0 uitgesloten (2026 totaal)',num(importedData.resultZero)],
['Resultaat=0 uitgesloten (gekozen locatie/periode)',num(result.excludedFalse)],
['Lege/ongeldige locatienamen uitgesloten',num(importedData.ignoredSites)],
['Scanpassen: apart werkblad',result.passSheetFound?num(result.passRows)+' registraties':'NIET AANWEZIG'],
['Cliënten met meerdere passen',result.passChecked?num(result.multiPassClients):'NIET TE CONTROLEREN'],
['Scanregels via pas gekoppeld (hele bestand)',num(result.linkedByPass)],
['Migratiedatum 28-10-2022 (passen)',num(result.pasDates?.migration??0)],
['Ongeldige pasdatums',num(result.pasDates?.invalidDates??0)],
['Niet-herkende passen',num(result.unmatchedPasses)],
['Tegenstrijdige pasregistraties',num(result.passConflicts)],
['Verschil scan/registratie',num(result.identityConflicts)],
['Businesscontroller','NIET AANGELEVERD']];
const verifyLines=quality.checks.map(c=>'<div class="check-row"><span>'+escapeHTML(c.label)+'</span><span>'+num(c.ours)+' / '+num(c.shadow)+' &nbsp;<strong class="'+(c.ours===c.shadow?'':'bad')+'">'+(c.ours===c.shadow?'✓ gelijk':'⚠ verschil')+'</strong></span></div>').join('');
$('checks').innerHTML='<div style="font-size:10px;color:#a8c9ee;margin:5px 0 11px">TWEEDE CONTROLETELLING · DASHBOARD / CONTROLE</div>'+verifyLines+'<div style="font-size:10px;color:#a8c9ee;margin:12px 0 9px">BRON- EN PERIODECONTROLES</div>'+lines.map(([name,val])=>'<div class="check-row"><span>'+escapeHTML(name)+'</span><span>'+escapeHTML(val)+'</span></div>').join('');
const impactLabel={none:'GEEN DIRECT REKENKUNDIG EFFECT',possible:'MOGELIJKE INVLOED OP KPI’S',unknown:'IMPACT NIET VAST TE STELLEN'};
const sourceAudit=quality.sourceIssues.length
?'<div class="source-audit"><h4>BRONAFWIJKINGEN · AANTAL & KPI-IMPACT</h4><p class="source-intro">Aantallen komen uit het bronbestand; de toelichting over scanregels betreft de gekozen locatie en periode, tenzij anders vermeld. Eén scanregel kan onder meerdere afwijkingen vallen.</p>'+
quality.sourceIssues.map(i=>'<div class="source-issue"><div class="source-issue-top"><span class="source-issue-name">'+escapeHTML(i.label)+'</span><span class="source-issue-count">'+num(i.total)+' registratie'+(i.total===1?'':'s')+'</span></div><div class="source-issue-impact '+i.impact+'">'+impactLabel[i.impact]+'</div><p class="source-issue-detail">'+escapeHTML(i.detail)+'</p></div>').join('')+'</div>'
:'<div class="source-audit"><h4>BRONAFWIJKINGEN · AANTAL & KPI-IMPACT</h4><p class="source-audit-ok">Geen bronafwijkingen gesignaleerd door de beschikbare controles.</p></div>';
$('checks').insertAdjacentHTML('beforeend',sourceAudit);
const issue=quality.mismatches||quality.sourceIssues.length;
document.querySelector('.verification .warning').textContent=quality.mismatches?`⚠ ${quality.mismatches} interne telverschillen gevonden. Cijfers nader onderzoeken. NIET GEVALIDEERD.`:
quality.sourceIssues.length?`⚠ Tweede telling sluit aan; ${quality.sourceIssues.length} soorten bronafwijkingen beoordeeld. Zie aantallen en KPI-impact hierboven. NIET EXTERN GEVALIDEERD.`:
`✓ ${quality.checks.length} interne telcontroles sluiten aan. ${result.historyLoaded?'Historie 2025 meegenomen.':'2025 ontbreekt: instroom voorlopig.'} Businesscontrollercontrole optioneel; niet extern gevalideerd.`;
document.querySelector('.paper-watermark').textContent='LOKALE EXCEL-ANALYSE · NIET EXTERN GEVALIDEERD';
document.querySelector('.paper p').textContent=result.period.label+' · lokale Excel-proef · niet voor publicatie';
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});
const pl=document.querySelectorAll('.paper-kpis small');if(pl[2])pl[2].textContent='Eerste geldige scan in '+result.period.label;
}
async function startImported(){
if(running)return;
running=true;$('startBtn').disabled=true;$('resetBtn').disabled=true;completed=0;clearSignal();
$('logs').innerHTML='';$('completed').textContent='0';$('conductor').classList.remove('attention');
$('conductor').classList.add('running');document.querySelector('.orchestra').classList.add('running');
$('conductorStatus').textContent='Excel-berekeningen worden gepresenteerd';$('logStatus').textContent='Actief';
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%'}
const result=calculateImported($('siteSelect').value);result.verification=independentlyVerify(result);importedData.last=result;
const steps=[
{agent:'data',text:`${result.rows.toLocaleString('nl-NL')} scanregels gecontroleerd; ${result.duplicates} herhaalde scans; Scanpassen: ${result.passSheetFound?result.passRows+' registraties':'geen apart werkblad'}`},
{agent:'analysis',text:`${result.unique} unieke cliënten in de periode; ${result.newIntake} nieuwe instromers in ${result.period.label}; ${result.active} actieve cliënten`},
{agent:'insight',text:'Maandtrends berekend; AI-duiding nog niet verbonden'},
{agent:'verify',text:`${result.verification.checks.length} onafhankelijke interne telcontroles; ${result.verification.mismatches} afwijkingen; ${result.correctedExisting} instromers herkend uit 2025. Extern niet gevalideerd`},
{agent:'report',text:'Conceptpreview bijgewerkt · NIET GEVALIDEERD'}];
try{
resetControllerComparison(true);updateControllerContext();
renderImported(result);
for(const step of steps){const el=$('agent-'+step.agent);signalFor(step.agent);el.classList.add('active');el.querySelector('.status').textContent='ACTIEF';$('flowState').textContent=step.text;log('Start: '+el.querySelector('h3').textContent);
const stageLabels=stepDescriptions(step.agent,{...result,sites:importedData.sites.length},false);showStages(step.agent,stageLabels);await animateStages(step.agent,stageLabels);
clearSignal();el.classList.remove('active');el.classList.add('done');el.querySelector('.status').textContent=step.agent==='verify'?(result.verification.passed?'INTERN OK':'LET OP'):'GEREED';completed++;$('completed').textContent=String(completed);log(step.text);await sleep(200)
}
$('conductor').classList.add('attention');$('flowState').textContent='Concept · NIET GEVALIDEERD';$('conductorStatus').textContent='Proefberekening voltooid';$('logStatus').textContent='Afgerond';
}catch(err){log('FOUT: '+err.message);$('flowState').textContent='Proefberekening mislukt';}finally{clearSignal();running=false;$('startBtn').disabled=false;$('resetBtn').disabled=false;$('conductor').classList.remove('running');document.querySelector('.orchestra').classList.remove('running')}
}
$('scanFile').addEventListener('change',async e=>{
importedData=null;$('historyFile').disabled=true;$('historyFile').value='';setHistoryStatus('◴ 2025 nog niet geladen. Instroom is voorlopig.');resetControllerComparison(true);updateControllerContext();const file=e.target.files?.[0];if(!file)return;
const state=$('importState');state.textContent='Bestand wordt lokaal gelezen...';$('startBtn').disabled=true;
try{
const importStart=performance.now();
importedData=await readExcelLocal(file,msg=>{state.textContent=msg});
const importSeconds=((performance.now()-importStart)/1000).toFixed(1);
const sel=$('siteSelect');sel.innerHTML='<option value="*">Alle locaties</option>';
importedData.sites.forEach(loc=>{const opt=document.createElement('option');opt.value=loc;opt.textContent=loc;sel.appendChild(opt)});
sel.disabled=false;
$('historyFile').disabled=false;
const type=$('periodType');
type.disabled=false;
type.value=availablePeriods('nine').length?'nine':'quarter';
refreshPeriodChoices();
state.textContent=`2026: ${importedData.all.length.toLocaleString('nl-NL')} bruikbare scans · ${importedData.sites.length} locaties · ${importSeconds} sec · historie 2025 optioneel`;
setHistoryStatus('◴ Historie 2025 ontbreekt nog: upload het aparte referentiebestand om bestaande cliënten uit 2025 te herkennen.','warning');
$('startBtn').textContent='▶ Analyseer Excelbestand';
$('flowState').textContent='Excelbestand lokaal ingelezen · start de analyse';
log('Excelbestand lokaal verwerkt; analyse moet nog starten. Voor echte data eerst interne toestemming controleren.');
}catch(err){$('startBtn').textContent='▶ Start voorbeeldanalyse';state.textContent='⚠ '+err.message;$('siteSelect').disabled=true;$('periodSelect').disabled=true;$('periodType').disabled=true;log('Excel-proef: '+err.message)}finally{$('startBtn').disabled=false}
});
$('historyFile').addEventListener('change',async e=>{
const file=e.target.files?.[0];if(!file||!importedData)return;
const state=$('importState');$('startBtn').disabled=true;
importedData.historyLoaded=false;importedData.historyFirst=new Map();importedData.historyInfo=null;importedData.last=null;
resetControllerComparison(true);updateControllerContext();
try{
const begun=performance.now();
const index=await readHistoricFirsts(file,msg=>setHistoryStatus(msg));
importedData.historyFirst=index.firsts;importedData.historyInfo=index.info;importedData.historyLoaded=true;
const duration=((performance.now()-begun)/1000).toFixed(1);
setHistoryStatus(`✓ Historie 2025 ingelezen: ${index.info.valid.toLocaleString('nl-NL')} scans · ${index.info.people.toLocaleString('nl-NL')} cliënt/locatie-combinaties · ${duration} sec. Start de analyse opnieuw.`,'ready');
state.textContent='2026 + historische referentie 2025 gereed · kies locatie/periode en start analyse';
$('flowState').textContent='2025 gekoppeld · analyse opnieuw starten';
log('Referentie 2025 gekoppeld; alle locaties uit het rapportagebestand; eerdere bezoeken kunnen instroom corrigeren.');
}catch(err){setHistoryStatus('⚠ Historie 2025 niet gekoppeld: '+err.message,'warning');$('historyFile').value='';log('Historische import: '+err.message)}
finally{$('startBtn').disabled=false;}
});
$('siteSelect').addEventListener('change',()=>{if(importedData){importedData.last=null;resetControllerComparison(true);updateControllerContext();$('flowState').textContent='Locatie gewijzigd · analyse opnieuw starten';}});
$('periodSelect').addEventListener('change',()=>{if(importedData){importedData.last=null;resetControllerComparison(true);updateControllerContext();$('flowState').textContent='Rapportageperiode gewijzigd · analyse opnieuw starten';}});
$('periodType').addEventListener('change',()=>{if(importedData){refreshPeriodChoices();importedData.last=null;resetControllerComparison(true);updateControllerContext();$('flowState').textContent='Rapportagesoort gewijzigd · analyse opnieuw starten';}});
// Bestaande chat blijft demo-gericht. Voor echte Excel-proef antwoorden we uitsluitend met controleerbare berekende cijfers.
const originalReply=reply;
reply=function(q){
if(!importedData)return originalReply(q);
const r=importedData.last;
if(!r)return 'Klik op Start demo-analyse om de lokaal ingelezen testgegevens te berekenen.';
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.';
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 geselecteerde periode, ${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.`;
return 'De AI-API is nog niet gekoppeld. Ik kan voorlopig alleen de gecontroleerde basisgetallen van het fictieve Excel-testbestand toelichten.';
};
// v0.8 — rapport wordt opgebouwd uit lokaal berekende, geaggregeerde testgegevens.
// Er is geen LLM, er wordt niet geüpload en ontbrekende velden worden NIET ingevuld.
const reportNL=n=>Number(n).toLocaleString('nl-NL');
function reportChartBars(names,values,color='#7935cf',asWeek=false){
const W=440,H=195,left=38,right=12,top=17,bottom=31,max=Math.max(1,...values);
let out=`<svg viewBox="0 0 ${W} ${H}" width="100%" height="100%" role="img" aria-label="Staafgrafiek">`;
for(let i=0;i<=4;i++){let y=top+(H-top-bottom)*i/4;out+=`<line x1="${left}" y1="${y}" x2="${W-right}" y2="${y}" stroke="#e6e8f2" stroke-width="1"/>`}
const band=(W-left-right)/Math.max(1,names.length),bar=Math.min(38,band*.7);
values.forEach((v,i)=>{const bh=(H-top-bottom-10)*v/max,x=left+band*i+(band-bar)/2,y=H-bottom-bh;
out+=`<rect x="${x}" y="${y}" width="${bar}" height="${Math.max(bh,0)}" fill="${color}"/><text x="${x+bar/2}" y="${Math.max(10,y-5)}" text-anchor="middle" fill="#18116b" font-size="${asWeek?10:12}" font-weight="700">${v}</text><text x="${x+bar/2}" y="${H-11}" text-anchor="middle" fill="#1f2a56" font-size="${asWeek?8:11}">${escapeHTML(names[i])}</text>`});
return out+'</svg>';
}
function reportChartClientMonth(names,values){
// Alleen presentatie: dezelfde berekende maandwaarden; iedere cliënt eenmaal per maand.
const W=480,H=220,left=24,right=9,top=19,bottom=34;
const max=Math.max(1,...values),span=H-top-bottom;
const band=(W-left-right)/Math.max(1,names.length),bar=Math.min(46,band*.82);
let out=`<svg viewBox="0 0 ${W} ${H}" width="100%" height="100%" role="img" aria-label="Aantal unieke cliënten per maand">`;
for(let i=0;i<=4;i++){
const y=top+span*i/4;
out+=`<line x1="${left}" y1="${y}" x2="${W-right}" y2="${y}" stroke="#e8e6f2" stroke-width="1"/>`;
}
values.forEach((v,i)=>{
const height=(span-15)*v/max;
const x=left+band*i+(band-bar)/2,y=H-bottom-height;
out+=`<rect x="${x}" y="${y}" width="${bar}" height="${Math.max(0,height)}" fill="#763fc7" rx="1.8"/>`;
out+=`<text x="${x+bar/2}" y="${Math.max(13,y-6)}" text-anchor="middle" fill="#2e196e" font-size="14" font-weight="800">${v}</text>`;
out+=`<text x="${x+bar/2}" y="${H-11}" text-anchor="middle" fill="#22285e" font-size="12" font-weight="600">${escapeHTML(names[i])}</text>`;
});
return out+'</svg>';
}
function reportChartLine(names,values){
if(!values.length)return '<div class="report-no-data">Geen gegevens beschikbaar</div>';
const W=440,H=195,L=40,T=24,B=34,R=15,max=Math.max(1,...values)*1.16,span=W-L-R;
let out=`<svg viewBox="0 0 ${W} ${H}" width="100%" height="100%" role="img" aria-label="Lijngrafiek">`;
for(let i=0;i<=4;i++){const y=T+(H-T-B)*i/4;out+=`<line x1="${L}" y1="${y}" x2="${W-R}" y2="${y}" stroke="#e5e8f2"/><text x="7" y="${y+4}" font-size="10" fill="#3e4771">${Math.round(max*(4-i)/4)}</text>`}
const dots=values.map((v,i)=>[L+(values.length===1?span/2:span*i/(values.length-1)),H-B-v/max*(H-T-B-7)]);
out+=`<polyline fill="none" stroke="#6630dc" stroke-width="3" points="${dots.map(p=>p.join(',')).join(' ')}"/>`;
dots.forEach(([x,y],i)=>{out+=`<circle cx="${x}" cy="${y}" r="4" fill="#6630dc"/><text x="${x}" y="${Math.max(11,y-10)}" font-size="11" font-weight="700" text-anchor="middle" fill="#5a22cb">${values[i]}</text><text x="${x}" y="${H-9}" text-anchor="middle" font-size="10" fill="#1d255e">${escapeHTML(names[i])}</text>`});
return out+'</svg>';
}
function getReportData(){
if(!importedData?.last) return null;
const result=importedData.last,site=result.site,period=result.period;
const history=importedData.all.filter(r=>(site==='*'||r.site===site)&&r.date<=period.peildatum);
const quarter=history.filter(r=>r.date>=period.begin);
const counts=new Map(),uniqueDates=new Set(),byMonth=new Map(period.months.map(m=>[m,0])),clientsByMonth=new Map(period.months.map(m=>[m,new Set()])),daysWithScans=new Set(),byWeek=[0,0,0,0,0,0,0];
for(const r of quarter){
const key=JSON.stringify([r.site,r.id,r.date]);if(uniqueDates.has(key))continue;
uniqueDates.add(key);
const mk=r.date.slice(0,7);byMonth.set(mk,(byMonth.get(mk)||0)+1);
const wk=new Date(r.date+'T12:00:00Z').getUTCDay();byWeek[(wk+6)%7]++;
const ck=JSON.stringify([r.site,r.id]);counts.set(ck,(counts.get(ck)||0)+1);
clientsByMonth.get(mk)?.add(ck);
daysWithScans.add(r.date);
}
const intensities=[0,0,0,0,0],daysPerClient=[...counts.values()];
daysPerClient.forEach(c=>{intensities[c<=5?0:c<=20?1:c<=50?2:c<=100?3:4]++});
const sorted=daysPerClient.sort((a,b)=>a-b),median=sorted.length?(sorted.length%2?sorted[(sorted.length-1)/2]:(sorted[sorted.length/2-1]+sorted[sorted.length/2])/2):0;
const monthLabels=['jan','feb','mrt','apr','mei','jun','jul','aug','sep','okt','nov','dec'];
const months=period.months.map(key=>({key,label:monthLabels[Number(key.slice(5))-1],visits:byMonth.get(key),clients:clientsByMonth.get(key)?.size||0,newIntake:result.instroomByMonth.get(key)||0}));
return {site:site==='*'?'Alle locaties':site,period,months,byWeek,intensities,
visits:uniqueDates.size,unique:result.unique,quarterClients:result.quarterClients,newIntake:result.newIntake,active:result.active,
average:counts.size?uniqueDates.size/counts.size:0,averageClientsPerDay:daysWithScans.size?uniqueDates.size/daysWithScans.size:0,daysWithScans:daysWithScans.size,median,maxDays:Math.max(0,...daysPerClient),
earliest:period.begin,latest:period.peildatum,issues:result.passConflicts+result.identityConflicts+result.unmatchedPasses,hasShadow:false,
historyLoaded:result.historyLoaded,correctedExisting:result.correctedExisting,correctedJanuary:result.correctedJanuary};
}
function buildWhiteReport(){
const d=getReportData();if(!d)return;
const range=d.earliest===d.latest?d.earliest:`${d.earliest} t/m ${d.latest}`;
const avgDay=d.averageClientsPerDay.toLocaleString('nl-NL',{minimumFractionDigits:1,maximumFractionDigits:1});
const activeWindow=readableDate(d.period.activeFrom)+' t/m '+readableDate(d.period.peildatum);
const mon=d.months.map(m=>m.label),values=d.months.map(m=>m.visits),clientsPerMonth=d.months.map(m=>m.clients),newPerMonth=d.months.map(m=>m.newIntake),week=['Ma','Di','Wo','Do','Vr','Za','Zo'];
const weekdayMax=Math.max(...d.byWeek),weekdayMin=Math.min(...d.byWeek),busiest=week[d.byWeek.indexOf(weekdayMax)],quietest=week[d.byWeek.indexOf(weekdayMin)];
const summary=[
`In ${d.period.label} registreerde ${escapeHTML(d.site)} ${reportNL(d.unique)} unieke cliënten en ${reportNL(d.visits)} bezoekdagen.`,
`Op ${reportNL(d.daysWithScans)} dagen met geregistreerde scans kwamen gemiddeld ${avgDay} cliënten per dag. De meeste bezoekdagen vielen op ${busiest} (${reportNL(weekdayMax)}).`,
`Nieuwe instroom: ${reportNL(d.newIntake)} cliënten. Actieve cliënten in de afgelopen drie maanden: ${reportNL(d.active)}.${d.historyLoaded?' Na vergelijking met 2025 bleken '+reportNL(d.correctedExisting)+' aanvankelijk nieuwe cliënten al eerder geregistreerd.':' Historie over 2025 ontbreekt; de instroom is voorlopig.'}`
];
const kpis=[{klass:'purple',icon:'♟',label:'Unieke cliënten',value:reportNL(d.unique),sub:''},
{klass:'green',icon:'+',label:'Nieuwe instroom',value:reportNL(d.newIntake),sub:'Eerste geldige scan op locatie in '+d.period.label},
{klass:'purple',icon:'♙',label:'Actieve cliënten',value:reportNL(d.active),sub:'Minimaal 1 scan in de afgelopen drie maanden.'},
{klass:'orange',icon:'▦',label:'Bezoekdagen',value:reportNL(d.visits),sub:'Eén cliënt per locatie per kalenderdag'}];
const kpiHTML=kpis.map(k=>`<div class="report-kpi ${k.klass}"><div class="k-icon">${k.icon}</div><div><div class="k-label">${k.label}</div><div class="k-num">${k.value}</div><div class="k-sub">${k.sub}</div></div><div class="k-underline"></div></div>`).join('');
$('reportSheet').innerHTML=`
<div class="report-brand"><img class="report-logo" src="data:image/png;base64,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" alt="Reakt PG logo"></div>
<div class="report-heading"><div><h1>Reakt Inloop Monitor ${escapeHTML(d.site)}</h1><p>Inzicht in bereik, betrokkenheid en bezoekpatronen</p></div>
<div class="report-meta"><div><b class="meta-icon">▦</b><span><strong>Periode</strong>${escapeHTML(range)}</span></div><div><b class="meta-icon">⌖</b><span><strong>Locatie</strong>${escapeHTML(d.site)}</span></div><div><b class="meta-icon">↻</b><span><strong>Peildatum</strong>${escapeHTML(d.latest)}</span></div></div></div>
<div class="report-kpi-grid">${kpiHTML}</div>
<section class="report-definitions" aria-label="Definities cliëntindicatoren">
<div class="report-definitions-title">Zo leest u de cliëntcijfers</div>
<div class="report-definitions-grid">
<div><b>Unieke cliënten</b>Iedere cliënt met minimaal één geldige scan op deze locatie binnen de gekozen rapportageperiode.</div>
<div><b>Nieuwe instroom</b>De eerste geldige scan van een cliënt op deze locatie (binnen de beschikbare historie) valt in de rapportageperiode.</div>
<div><b>Actieve cliënten</b>Minimaal één scan op deze locatie tussen ${escapeHTML(activeWindow)}.</div>
</div>
<div class="report-definitions-note">Een vervangende scanpas telt niet als nieuwe cliënt.</div>
</section>
<div class="report-chart-grid">
<section class="report-panel"><h2>Instroom per maand <small>(nieuwe cliënten)</small></h2><div class="chart-box">${reportChartBars(mon,newPerMonth,'#288e28')}</div></section>
<section class="report-panel"><h2>Bezoekdagen per maand</h2><div class="chart-box">${reportChartLine(mon,values)}</div></section>
<section class="report-panel"><h2>Bezoekdagen per weekdag</h2><div class="chart-box">${reportChartBars(week,d.byWeek,'#ff6410',true)}</div></section>
</div>
<div class="report-bottom-grid">
<section class="report-panel"><h2>Kerncijfers locatie</h2><table class="report-mini-table"><thead><tr><th>Locatie</th><th>Unieke cliënten</th><th>Bezoekdagen</th><th>Gem. cliënten per dag*</th></tr></thead><tbody><tr><td>⌖ ${escapeHTML(d.site)}</td><td>${reportNL(d.unique)}</td><td>${reportNL(d.visits)}</td><td>${avgDay}</td></tr><tr class="total"><td>Totaal / gemiddelde</td><td>${reportNL(d.unique)}</td><td>${reportNL(d.visits)}</td><td>${avgDay}</td></tr></tbody></table><div class="report-average-highlight"><span class="highlight-num">${avgDay}</span><span class="highlight-label">Gemiddeld aantal cliënten per dag</span></div><div class="report-average-note">* Berekend over ${reportNL(d.daysWithScans)} dagen met geregistreerde scans.</div></section>
<section class="report-panel"><h2>Aantal cliënten per maand <small>(iedere cliënt maximaal één keer per maand)</small></h2><div class="chart-box report-client-month-chart">${reportChartClientMonth(mon,clientsPerMonth)}</div></section>
<section class="report-panel"><h2>Managementanalyse</h2><div class="report-analysis">${summary.map(t=>`<p>${t}</p>`).join('')}</div></section>
</div>
<div class="report-foot"><span>Bron: scanregistratie 2026${d.historyLoaded?' en historisch referentiebestand 2025':''}. Rapportageperiode en peildatum staan bovenaan vermeld.</span></div>`;
}
function openWhiteReport(){
if(!importedData?.last){alert('Upload eerst een Excelbestand en rond de analyse af. Deze witte rapportage gebruikt geen verzonnen demo-KPI’s.');return;}
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;}
buildWhiteReport();$('reportOverlay').classList.add('open');$('reportOverlay').setAttribute('aria-hidden','false');document.body.classList.add('report-open');$('reportOverlay').scrollTop=0;
}
function closeWhiteReport(){$('reportOverlay').classList.remove('open');$('reportOverlay').setAttribute('aria-hidden','true');document.body.classList.remove('report-open')}
async function downloadReportImage(type='png'){
const overlay=$('reportOverlay');
if(!overlay.classList.contains('open')) openWhiteReport();
const target=$('reportSheet');
const pngBtn=$('reportPng'), jpgBtn=$('reportJpg');
const activeBtn=type==='png'?pngBtn:jpgBtn;
if(!window.html2canvas){
alert('De afbeeldingsexport kon niet starten. Vernieuw de pagina en probeer opnieuw.');
return;
}
const prev=activeBtn.textContent;
try{
activeBtn.disabled=true; activeBtn.textContent='Bezig met exporteren...';
await new Promise(r=>setTimeout(r,80));
const canvas=await html2canvas(target,{backgroundColor:'#ffffff',scale:2.5,useCORS:true,logging:false});
const mime=type==='jpg'?'image/jpeg':'image/png';
const quality=type==='jpg'?0.94:1;
const dataUrl=canvas.toDataURL(mime,quality);
const link=document.createElement('a');
const site=(importedData?.last?.site||'monitor').toLowerCase().replace(/[^a-z0-9]+/gi,'_');
link.href=dataUrl;
link.download=`reakt_inloop_monitor_${site}.${type==='jpg'?'jpg':'png'}`;
document.body.appendChild(link);
link.click();
link.remove();
}catch(err){
console.error(err);
alert('Het downloaden van de afbeelding is niet gelukt. Probeer het opnieuw.');
}finally{
activeBtn.disabled=false; activeBtn.textContent=prev;
}
}
$('reportClose').addEventListener('click',closeWhiteReport);
$('reportPrint').addEventListener('click',()=>window.print());
$('reportPng').addEventListener('click',()=>downloadReportImage('png'));
$('reportJpg').addEventListener('click',()=>downloadReportImage('jpg'));
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&$('reportOverlay').classList.contains('open'))closeWhiteReport()});
$('startBtn').addEventListener('click',start);$('resetBtn').addEventListener('click',reset);$('chatForm').addEventListener('submit',e=>{e.preventDefault();const q=$('chatInput').value;$('chatInput').value='';ask(q)});document.querySelectorAll('[data-q]').forEach(b=>b.addEventListener('click',()=>ask(b.dataset.q)));$('exportBtn').addEventListener('click',openWhiteReport);
Promise.resolve({ok:true,json:async()=>JSON.parse(document.getElementById('orchestra-demo-data').textContent)}).then(r=>r.json()).then(d=>{demoData=d;renderKPIs(d);renderChecks(d);drawChart(d)}).catch(()=>log('Demo-gegevens konden niet worden geladen'));
// v1.2 — expliciete, handmatig ingevoerde onafhankelijke fictieve aggregaten.
// Resultaten worden NIET opgeslagen; andere locatie/kwartaal/bestand maakt vergelijking ongeldig.
const controllerDefs=[['unique','ctrlUnique','Unieke cliënten (periode)'],['newIntake','ctrlIntake','Nieuwe instroom'],['active','ctrlActive','Actieve cliënten'],['visits','ctrlVisits','Unieke bezoekdagen']];
let controllerComparison=null;
function resetControllerComparison(clearInputs=false){
controllerComparison=null;
$('controllerBadge').textContent='NIET AANGELEVERD';
$('controllerResult').textContent='Nog geen onafhankelijke controlecijfers vergeleken.';
if(clearInputs)controllerDefs.forEach(([,id])=>{$(id).value=''});
}
function updateControllerContext(){
const r=importedData?.last;
$('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.';
}
function compareController(){
const r=importedData?.last;
if(!r){$('controllerResult').textContent='⚠ Analyseer eerst een fictief Excel-testbestand.';return}
const values={};
for(const [key,id] of controllerDefs){const el=$(id),v=el.value.trim();if(!/^\d+$/.test(v)||!Number.isSafeInteger(Number(v))){$('controllerResult').textContent='⚠ Vul alle vier de controlewaarden in als geldige, niet-negatieve gehele getallen.';el.focus();return}values[key]=Number(v)}
const rows=controllerDefs.map(([key,,label])=>({label,ours:r[key],controller:values[key],delta:r[key]-values[key]}));
const count=rows.filter(x=>x.delta!==0).length;
controllerComparison={site:r.site,period:r.period.label,rows,count};
const fmt=x=>Number(x).toLocaleString('nl-NL');
const rendered=rows.map(x=>`<tr><td>${escapeHTML(x.label)}</td><td>${fmt(x.ours)}</td><td>${fmt(x.controller)}</td><td class="${x.delta?'controller-bad':'controller-ok'}">${x.delta?(x.delta>0?'+':'')+fmt(x.delta):'✓ 0'}</td></tr>`).join('');
$('controllerResult').innerHTML=`<div class="controller-summary">${count?`⚠ ${count} van 4 indicatoren wijken af. Onderzoek de oorzaak voordat de cijfers worden gebruikt.`:'✓ Alle 4 ingevoerde controlegetallen komen overeen. Dit is nog geen formele validatie.'}</div><table class="controller-table"><thead><tr><th>Indicator</th><th>ORCHESTRA</th><th>Controle</th><th>Verschil</th></tr></thead><tbody>${rendered}</tbody></table>`;
$('controllerBadge').textContent=count?`${count} VERSCHIL(LEN)`:'4 / 4 GELIJK';
log('Fictieve businesscontrollervergelijking: '+count+' verschil(len) op 4 indicatoren; geen formele vrijgave');
}
$('controllerCompare').addEventListener('click',compareController);
$('controllerClear').addEventListener('click',()=>resetControllerComparison(true));
controllerDefs.forEach(([,id])=>$(id).addEventListener('input',()=>{if(controllerComparison)resetControllerComparison(false)}));
// ORCHESTRA NEXT — echte statusovergangen gekoppeld aan bestaande berekeningen.
// Geen nieuwe externe AI-call en geen wijziging aan XLSX-, KPI- of rapportagerekenregels.
const nextRoutes=[['data','identity'],['data','history'],['identity','analysis'],['history','analysis'],['analysis','insight'],['analysis','verify'],['insight','report'],['verify','report'],['privacy','quality'],['report','quality'],['verify','analysis'],['identity','history'],['conductor','privacy']];
const nextAgents={data:'Bronregels beschikbaar',identity:'Pas- en cliëntkoppelingen',history:'Historische referentie',analysis:'KPI-berekeningen',insight:'Trendgegevens',verify:'Interne onafhankelijke telling',privacy:'Lokale browserroute',report:'Dashboard en rapportage',quality:'Status en kwaliteitsmelding'};
function nextWorkflowDelay(){return $('paceSelect').value==='fast'?650:3300;}
function nextSignalDuration(){return $('paceSelect').value==='fast'?'0.9s':'1.8s';}
let nextActiveRoutes=[],nextCompletedRoutes=[],nextGraphReady=false,nextTransferTimer=null;
function nextEl(id){return id==='conductor'?$('conductor'):$('agent-'+id)}
function nextSetConductorTransfer(active){const c=$('conductor');if(!c)return;c.classList.toggle('transfering',!!active);if(active){clearTimeout(nextTransferTimer);nextTransferTimer=setTimeout(()=>c.classList.remove('transfering'),1050)}}
function nextRememberRoutes(routes=[]){for(const route of routes.map(r=>r.join('>'))){if(!nextCompletedRoutes.includes(route))nextCompletedRoutes.push(route)}}
function nextGraph(){
const host=document.querySelector('.orchestra'),svg=$('networkSvg'),edgeLayer=$('networkEdges');if(!host||!svg||!edgeLayer)return;
const bounds=host.getBoundingClientRect();svg.setAttribute('viewBox',`0 0 ${bounds.width} ${bounds.height}`);edgeLayer.replaceChildren();
for(const [from,to] of nextRoutes){
const a=nextEl(from)?.getBoundingClientRect(),b=nextEl(to)?.getBoundingClientRect();if(!a||!b)continue;
const ax=a.left+a.width/2-bounds.left,ay=a.bottom-bounds.top,bx=b.left+b.width/2-bounds.left,by=b.top-bounds.top;
const sameRow=Math.abs((a.top+a.height/2)-(b.top+b.height/2))<45;
const curveLift=Math.min(82,Math.max(38,Math.abs(bx-ax)*.16));
const d=sameRow
?`M${ax} ${ay-a.height*.38} C${ax} ${ay-curveLift},${bx} ${by-curveLift},${bx} ${by-b.height*.38}`
:`M${ax} ${ay} C${ax} ${ay+curveLift},${bx} ${by-curveLift},${bx} ${by}`;
const routeId=from+'>'+to;
const p=document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('d',d);p.dataset.route=routeId;
const classes=[];if(nextCompletedRoutes.includes(routeId))classes.push('route-done');if(nextActiveRoutes.includes(routeId))classes.push('transfer');
if(classes.length)p.setAttribute('class',classes.join(' '));
edgeLayer.appendChild(p);
}
nextBuildSignals();
nextGraphReady=true;
}
function nextBuildSignals(){
const layer=$('networkSignals');if(!layer)return;layer.replaceChildren();
if(window.matchMedia('(prefers-reduced-motion: reduce)').matches)return;
const ns='http://www.w3.org/2000/svg';
const dur=nextSignalDuration();
document.querySelectorAll('#networkEdges path.transfer').forEach((path,index)=>{
const d=path.getAttribute('d');
[[10,'signal-halo',`${index*.12}s`,.18],[3.7,'signal-core',`${.24+index*.12}s`,1]].forEach(([radius,cls,begin,opacity])=>{
const circle=document.createElementNS(ns,'circle');circle.setAttribute('r',radius);circle.setAttribute('class',cls);circle.setAttribute('fill','#9ef7ff');circle.setAttribute('opacity',opacity);
const motion=document.createElementNS(ns,'animateMotion');motion.setAttribute('path',d);motion.setAttribute('dur',dur);motion.setAttribute('repeatCount','indefinite');motion.setAttribute('begin',begin);
circle.appendChild(motion);layer.appendChild(circle);
});
})
}
function nextRoutesActive(routes=[]){nextActiveRoutes=routes.map(r=>r.join('>'));if(routes.length)nextRememberRoutes(routes);if(!nextGraphReady)nextGraph();else{document.querySelectorAll('#networkEdges path').forEach(p=>{const route=p.dataset.route;const classes=[];if(nextCompletedRoutes.includes(route))classes.push('route-done');if(nextActiveRoutes.includes(route))classes.push('transfer');p.setAttribute('class',classes.join(' '));});nextBuildSignals();}nextSetConductorTransfer(nextActiveRoutes.length>0);}
function nextAgentState(id,state,detail){const el=$('agent-'+id);if(!el)return;
el.classList.toggle('active',state==='active');el.classList.toggle('done',state==='done'||state==='warning');
el.querySelector('.status').textContent=state==='active'?'ACTIEF':state==='done'?'GEREED':state==='warning'?'LET OP':state==='skipped'?'NIET BESCHIKBAAR':'STANDBY';
const percent=state==='done'||state==='warning'?100:state==='active'?65:0;
el.querySelector('.track i').style.width=percent+'%';el.querySelector('.percent').textContent=percent+'%';
if(detail){el.classList.add('has-steps');const wrap=el.querySelector('.agent-steps');wrap.replaceChildren();const line=document.createElement('div');line.className='agent-step '+(state==='active'?'current':state==='skipped'?'skipped':'complete');const mark=document.createElement('span');mark.className='step-mark';mark.textContent=state==='warning'?'!':state==='active'?'◌':'✓';const text=document.createElement('span');text.textContent=detail;line.append(mark,text);wrap.append(line)}
}
function nextFinish(){completed=ids.filter(id=>$('agent-'+id)?.classList.contains('done')).length;$('completed').textContent=String(completed)}
function nextLog(id,message,state='done',routes=[]){nextAgentState(id,state,message);nextRoutesActive(routes);log(nextEl(id)?.querySelector('h3')?.textContent+': '+message);$('flowState').textContent=message;nextFinish()}
function nextResetUI(){nextActiveRoutes=[];nextCompletedRoutes=[];nextSetConductorTransfer(false);const signal=$('networkSignals');if(signal)signal.replaceChildren();nextGraphReady=false;requestAnimationFrame(nextGraph);}
const nextOriginalReset=reset;
reset=function(){nextOriginalReset();nextResetUI();};
const nextOriginalStart=start;
start=async function(){if(!importedData){return nextOriginalStart()};return nextStartImported()};
$('startBtn').removeEventListener('click',nextOriginalStart);$('startBtn').addEventListener('click',start);
$('resetBtn').removeEventListener('click',nextOriginalReset);$('resetBtn').addEventListener('click',reset);
async function nextStartImported(){
if(running)return;
running=true;$('startBtn').disabled=true;$('resetBtn').disabled=true;completed=0;nextActiveRoutes=[];nextCompletedRoutes=[];
$('logs').replaceChildren();$('conductor').classList.remove('attention');$('conductor').classList.add('running');
document.querySelector('.orchestra').classList.add('running');$('conductorStatus').textContent='Verwerkingsstappen en overdrachten';$('logStatus').textContent='Actief';
for(const id of ids){nextAgentState(id,'standby');const e=$('agent-'+id);e.classList.remove('has-steps');e.querySelector('.agent-steps').replaceChildren()}
$('completed').textContent='0';nextGraph();
const modeDelay=nextWorkflowDelay();
const t0=performance.now();
let calculationMs=0; // Houd echte verwerkingstijd gescheiden van de gekozen presentatieduur.
try{
nextLog('data',`${importedData.all.length.toLocaleString('nl-NL')} bruikbare scans eerder lokaal ingelezen`,'done',[]);await sleep(modeDelay);
// Privacy-agent beschrijft alleen de feitelijke implementatieroute; geen juridische certificering.
nextLog('privacy','Rekenwerk in deze browser; geen AI-API. Privacytoetsing blijft organisatieverantwoordelijkheid.','done',[['conductor','privacy']]);await sleep(modeDelay);
nextAgentState('identity','active','Koppelingen worden in bestaande telroute toegepast');nextRoutesActive([['data','identity']]);
// Zelfde oorspronkelijke rekenfunctie: 2025-historie, pasregels, datums en KPI-definities blijven ongewijzigd.
const calcStart=performance.now();
const result=calculateImported($('siteSelect').value);
calculationMs+=performance.now()-calcStart;
nextLog('identity',`${result.multiPassClients} cliënten met meerdere passen gesignaleerd`,'done',[['data','identity'],['identity','history']]);await sleep(modeDelay);
if(result.historyLoaded)nextLog('history',`${result.correctedExisting} eerdere cliënten vanuit 2025 herkend`,'done',[['data','history'],['history','analysis']]);
else nextLog('history','Geen 2025-referentie: instroom voorlopig','warning',[['data','history']]);
await sleep(modeDelay);
nextLog('analysis',`${result.visits.toLocaleString('nl-NL')} unieke bezoekdagen en ${result.newIntake} nieuwe instromers berekend`,'done',[['identity','analysis'],['history','analysis']]);await sleep(modeDelay);
nextLog('insight',`${result.months.length} maandperioden beschikbaar; geen automatisch gegenereerde AI-duiding`,'done',[['analysis','insight']]);await sleep(modeDelay);
nextAgentState('verify','active','Onafhankelijke tweede telroute loopt');nextRoutesActive([['analysis','verify']]);
const verifyStart=performance.now();
result.verification=independentlyVerify(result);importedData.last=result;
calculationMs+=performance.now()-verifyStart;
nextLog('verify',`${result.verification.checks.length} interne controles; ${result.verification.mismatches} telverschillen`,result.verification.mismatches?'warning':'done',[['analysis','verify'],['verify','analysis']]);await sleep(modeDelay);
resetControllerComparison(true);updateControllerContext();
const renderStart=performance.now();
renderImported(result);
calculationMs+=performance.now()-renderStart;
nextLog('report','Bestaand dashboard en rapportagepreview bijgewerkt','done',[['verify','report'],['insight','report']]);await sleep(modeDelay);
const concern=result.verification.mismatches>0||!result.historyLoaded;
nextLog('quality',concern?'Concept: controle of historische referentie vraagt aandacht':'Intern gecontroleerd; niet extern gevalideerd',concern?'warning':'done',[['report','quality'],['privacy','quality']]);await sleep(modeDelay);
$('conductor').classList.add('attention');$('conductorStatus').textContent='Verwerking afgerond';
$('flowState').textContent='Concept beschikbaar · NIET EXTERN GEVALIDEERD';$('logStatus').textContent='Afgerond';
log(`Echte KPI-berekening, verificatie en dashboardopbouw: ${(calculationMs/1000).toFixed(2)} sec. Totale workflowweergave: ${((performance.now()-t0)/1000).toFixed(1)} sec (exclusief XLSX-import).`);
}catch(err){log('FOUT: '+err.message);$('flowState').textContent='Verwerking mislukt';$('conductorStatus').textContent='Procesfout'}
finally{nextRoutesActive([]);running=false;$('startBtn').disabled=false;$('resetBtn').disabled=false;$('conductor').classList.remove('running');document.querySelector('.orchestra').classList.remove('running')}
}
// Voorbeeldmodus blijft herkenbaar als demonstratie van fictieve data; geen echte berekeningen claimen.
const nextDemoStart=nextOriginalStart;
// Maak ook de vier extra agenten zichtbaar in de voorbeeldmodus zonder de bestaande demonstratiegegevens te veranderen.
const nextOldDemoStarter=start;
start=async function(){if(importedData)return nextStartImported();nextActiveRoutes=[];nextCompletedRoutes=[];nextResetUI();for(const id of ['identity','history','privacy','quality'])nextAgentState(id,'skipped','Voorbeeldmodus · geen broncontrole');return nextDemoStart()};
$('startBtn').removeEventListener('click',nextOldDemoStarter);$('startBtn').addEventListener('click',start);
window.addEventListener('resize',()=>requestAnimationFrame(nextGraph));
if(typeof ResizeObserver!=='undefined')new ResizeObserver(()=>requestAnimationFrame(nextGraph)).observe(document.querySelector('.orchestra'));
requestAnimationFrame(nextGraph);
</script></body></html>