Files
html/agent-roi-simulator.html
2026-04-19 22:10:02 +02:00

613 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WEVAL · ROI Simulator — Gains quantitatifs &amp; qualitatifs par agent</title>
<style>
:root {
--bg-0:#05060a; --bg-1:#0b0d15; --bg-2:#11141f; --bg-3:#171b2a; --bg-4:#1e2336;
--border:rgba(99,102,241,0.15); --border-h:rgba(99,102,241,0.35);
--text:#e2e8f0; --dim:#94a3b8; --mute:#64748b;
--accent:#14b8a6; --accent2:#6366f1; --purple:#a855f7; --cyan:#06b6d4;
--ok:#22c55e; --warn:#f59e0b; --err:#ef4444; --rose:#f43f5e; --gold:#eab308;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', system-ui, sans-serif;
background: radial-gradient(ellipse at top, #0f1420, #05060a 65%);
color: var(--text); min-height: 100vh; font-size: 13.5px; line-height: 1.55;
}
.container { max-width: 1680px; margin: 0 auto; padding: 28px 32px 80px; }
/* HEADER */
header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
header h1 { font-size: 26px; font-weight: 800; background: linear-gradient(90deg, #22d3ee, #a855f7, #eab308); -webkit-background-clip: text; background-clip: text; color: transparent; letter-spacing: -0.4px; }
header .sub { color: var(--dim); font-size: 13px; margin-top: 6px; max-width: 820px; }
.actions { display: flex; gap: 9px; }
.btn { padding: 8px 15px; background: var(--bg-2); border: 1px solid var(--border); color: var(--text); border-radius: 8px; font-size: 12.5px; cursor: pointer; text-decoration: none; font-family: inherit; transition: all .2s; }
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn-pri { background: linear-gradient(135deg, var(--gold), var(--warn)); color: #0b0d15; font-weight: 700; border: none; }
.pulse { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 0 rgba(34,197,94,.7); animation: pulse 2s infinite; margin-right: 4px; }
@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(34,197,94,.7)} 70%{box-shadow:0 0 0 8px rgba(34,197,94,0)} 100%{box-shadow:0 0 0 0 rgba(34,197,94,0)} }
/* MAIN LAYOUT */
.main-grid { display: grid; grid-template-columns: 320px 1fr 340px; gap: 18px; }
@media(max-width: 1400px) { .main-grid { grid-template-columns: 1fr; } }
/* PARAMETER PANEL (left) */
.panel { background: var(--bg-1); border: 1px solid var(--border); border-radius: 14px; padding: 20px; position: sticky; top: 18px; }
.panel h3 { font-size: 15px; font-weight: 700; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
.param-group { margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid var(--bg-3); }
.param-group:last-child { border: none; padding-bottom: 0; margin-bottom: 0; }
.param-label { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.6px; font-weight: 600; margin-bottom: 8px; display: block; }
.param-pills { display: flex; flex-wrap: wrap; gap: 5px; }
.param-pill { padding: 6px 10px; background: var(--bg-3); border: 1px solid var(--border); border-radius: 16px; font-size: 11px; cursor: pointer; color: var(--dim); transition: all .2s; flex: 1; text-align: center; min-width: 60px; }
.param-pill:hover { color: var(--text); border-color: var(--accent); }
.param-pill.active { background: linear-gradient(135deg, var(--accent2), var(--purple)); color: white; border: none; font-weight: 600; }
.param-info { font-size: 10.5px; color: var(--mute); margin-top: 6px; padding: 6px 8px; background: var(--bg-2); border-radius: 5px; line-height: 1.4; }
select { width: 100%; padding: 8px 10px; background: var(--bg-3); border: 1px solid var(--border); color: var(--text); border-radius: 6px; font-family: inherit; font-size: 12px; }
/* CENTER: AGENT LIST */
.center-col { display: flex; flex-direction: column; gap: 16px; }
.quick-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.qs { background: var(--bg-1); border: 1px solid var(--border); border-radius: 10px; padding: 14px; position: relative; overflow: hidden; }
.qs::before { content: ''; position: absolute; left: 0; top: 0; width: 3px; height: 100%; background: var(--accent); }
.qs.gold::before { background: linear-gradient(180deg, var(--gold), var(--warn)); }
.qs.cy::before { background: var(--cyan); }
.qs.pu::before { background: var(--purple); }
.qs .lbl { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
.qs .val { font-size: 22px; font-weight: 800; color: var(--text); line-height: 1; margin-top: 4px; }
.qs .val.gold { background: linear-gradient(135deg, var(--gold), var(--warn)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.qs .sub { font-size: 10px; color: var(--mute); margin-top: 3px; }
.filter-bar { display: flex; gap: 5px; flex-wrap: wrap; padding: 12px; background: var(--bg-1); border: 1px solid var(--border); border-radius: 10px; align-items: center; }
.filter-bar .lbl { font-size: 11px; color: var(--dim); font-weight: 600; margin-right: 6px; }
.filter-pill { padding: 5px 10px; background: var(--bg-3); border: 1px solid var(--border); color: var(--dim); border-radius: 14px; font-size: 11px; cursor: pointer; font-family: inherit; transition: all .2s; }
.filter-pill:hover { color: var(--text); border-color: var(--accent); }
.filter-pill.active { background: linear-gradient(135deg, var(--accent), var(--cyan)); color: white; border: none; font-weight: 600; }
.filter-bar button.btn-sml { margin-left: auto; padding: 5px 11px; font-size: 10.5px; }
.agent-list { display: flex; flex-direction: column; gap: 8px; max-height: none; }
.agent-card { background: var(--bg-1); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; display: grid; grid-template-columns: 26px 1fr 130px 100px 100px; gap: 14px; align-items: center; transition: all .15s; cursor: pointer; }
.agent-card:hover { background: var(--bg-2); border-color: var(--border-h); }
.agent-card.selected { background: linear-gradient(135deg, rgba(20,184,166,0.08), rgba(99,102,241,0.06)); border-color: var(--accent); }
.agent-card input[type=checkbox] { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; }
.ag-main .ag-name { font-size: 13px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 6px; }
.ag-main .ag-name::before { content: '🤖'; }
.ag-main .ag-pain { font-size: 11px; color: var(--dim); margin-top: 3px; line-height: 1.3; }
.ag-main .ag-meta { display: flex; gap: 6px; margin-top: 5px; flex-wrap: wrap; }
.ag-main .ag-meta span { font-size: 9.5px; padding: 1px 6px; border-radius: 4px; background: var(--bg-3); color: var(--dim); }
.ag-main .ag-meta .dept { background: rgba(168,85,247,0.15); color: #d4a7fa; }
.ag-sav { text-align: right; }
.ag-sav .v { font-size: 16px; font-weight: 800; background: linear-gradient(135deg, var(--gold), var(--warn)); -webkit-background-clip: text; background-clip: text; color: transparent; font-family: 'JetBrains Mono', monospace; }
.ag-sav .l { font-size: 9.5px; color: var(--dim); }
.ag-quali { text-align: center; }
.ag-quali .v { font-size: 16px; font-weight: 800; color: var(--accent); font-family: 'JetBrains Mono', monospace; }
.ag-quali .l { font-size: 9.5px; color: var(--dim); }
.ag-payback { text-align: center; }
.ag-payback .v { font-size: 14px; font-weight: 700; color: var(--purple); font-family: 'JetBrains Mono', monospace; }
.ag-payback .l { font-size: 9.5px; color: var(--dim); }
/* RIGHT COL: SELECTED PACK + RADAR + CURVE */
.right-col { display: flex; flex-direction: column; gap: 14px; position: sticky; top: 18px; }
.selection-box { background: var(--bg-1); border: 1px solid var(--border); border-radius: 12px; padding: 18px; }
.selection-box h3 { font-size: 14px; font-weight: 700; margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
.selection-box h3 .count-badge { font-size: 11px; background: var(--accent); color: white; padding: 2px 8px; border-radius: 10px; font-weight: 700; }
.pack-kpis { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 14px; }
.pack-kpi { padding: 10px; background: var(--bg-2); border-radius: 8px; border-left: 2px solid var(--accent); }
.pack-kpi.gold { border-left-color: var(--gold); }
.pack-kpi.rose { border-left-color: var(--rose); }
.pack-kpi .l { font-size: 9.5px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
.pack-kpi .v { font-size: 17px; font-weight: 800; color: var(--text); margin-top: 3px; line-height: 1; font-family: 'JetBrains Mono', monospace; }
.pack-kpi.gold .v { background: linear-gradient(135deg, var(--gold), var(--warn)); -webkit-background-clip: text; background-clip: text; color: transparent; }
/* Radar chart quali */
.radar-wrap { position: relative; text-align: center; }
.radar-wrap svg { width: 100%; max-width: 280px; }
/* 12m curve */
.curve-wrap { background: var(--bg-2); border-radius: 8px; padding: 12px; }
/* Empty state */
.empty { text-align: center; padding: 24px 12px; color: var(--mute); font-size: 12px; }
.loading { text-align: center; padding: 60px; color: var(--dim); }
.spinner { width: 40px; height: 40px; border: 3px solid var(--bg-3); border-top-color: var(--accent); border-radius: 50%; margin: 0 auto 16px; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media(max-width: 1400px) {
.panel, .right-col { position: static; }
.quick-stats { grid-template-columns: repeat(2, 1fr); }
.agent-card { grid-template-columns: 24px 1fr; }
.ag-sav, .ag-quali, .ag-payback { display: none; }
}
/* === OPUS RESPONSIVE FIX v2 19avr — append-only, doctrine #14 === */
@media(max-width: 480px) {
html, body { overflow-x: hidden !important; max-width: 100vw; }
body, main, section, article { word-break: break-word; overflow-wrap: anywhere; }
img, video, iframe, canvas, svg, table, pre, code { max-width: 100% !important; }
pre, code { white-space: pre-wrap; word-break: break-all; }
table { display: block; overflow-x: auto; }
.container, [class*="container"], [class*="wrapper"] { max-width: 100vw !important; padding-left: 12px !important; padding-right: 12px !important; }
[class*="grid"], [class*="-grid"] { grid-template-columns: 1fr !important; gap: 10px !important; }
[class*="kpi"], [class*="stats"], [class*="-cards"] { grid-template-columns: 1fr !important; }
header, nav, footer { flex-wrap: wrap !important; }
header > *, nav > *, footer > * { max-width: 100%; }
h1 { font-size: 22px !important; word-break: break-word; }
h2 { font-size: 18px !important; }
.pitch, [class*="pitch"], [class*="hero"] { word-break: break-word; overflow-wrap: anywhere; }
}
/* === OPUS RESPONSIVE FIX v2 END === */
</style>
</head>
<body>
<div class="container">
<header>
<div>
<h1>🧮 ROI Simulator · Agent-by-Agent <span class="pulse"></span></h1>
<div class="sub">Simulez les gains quantitatifs &amp; qualitatifs pour chaque agent WEVAL. Paramètres contextuels client (taille/maturité/vertical). Calculs temps réel.</div>
</div>
<div class="actions">
<a href="/pain-points-atlas.html" class="btn">← Atlas</a>
<a href="/weval-technology-platform.html" class="btn">🏠 WTP</a>
<button class="btn btn-pri" onclick="exportJSON()">📦 Export JSON</button>
</div>
</header>
<div class="main-grid">
<!-- LEFT: PARAMS -->
<div class="panel">
<h3>⚙️ Contexte client</h3>
<div class="param-group">
<span class="param-label">🏢 Taille entreprise</span>
<div class="param-pills" id="size-pills"></div>
<div class="param-info" id="size-info"></div>
</div>
<div class="param-group">
<span class="param-label">🧠 Maturité IA</span>
<div class="param-pills" id="maturity-pills"></div>
<div class="param-info" id="maturity-info"></div>
</div>
<div class="param-group">
<span class="param-label">🏭 Vertical</span>
<select aria-label="form-field" id="vertical-select">
<option value="">— sélectionner —</option>
</select>
<div class="param-info" id="vert-info"></div>
</div>
<div class="param-group">
<span class="param-label">💰 Multiplicateur global</span>
<div class="param-info" style="font-size:14px;text-align:center;color:var(--text);font-weight:700;font-family:'JetBrains Mono',monospace" id="mult-display">1.00×</div>
<div class="param-info" style="margin-top:4px;font-size:10px">= size × maturity × vertical (× 1.25 si département aligné vertical)</div>
</div>
</div>
<!-- CENTER: AGENTS LIST -->
<div class="center-col">
<div class="quick-stats">
<div class="qs gold"><div class="lbl">Savings pack sélectionné</div><div class="val gold" id="qs-sav">0€</div><div class="sub" id="qs-sav-sub">— par an</div></div>
<div class="qs cy"><div class="lbl">Implementation cost</div><div class="val" id="qs-impl">0€</div><div class="sub" id="qs-impl-sub">one-shot</div></div>
<div class="qs pu"><div class="lbl">Payback pack</div><div class="val" id="qs-pay">— mois</div><div class="sub">moyenne pondérée</div></div>
<div class="qs"><div class="lbl">NPV 3 ans</div><div class="val" id="qs-npv">0€</div><div class="sub">savings - cost - 20% maint</div></div>
</div>
<div class="filter-bar">
<span class="lbl">Département :</span>
<div id="dept-filters" style="display:contents"></div>
<button class="btn btn-sml" onclick="selectAll()">✓ Tous</button>
<button class="btn btn-sml" onclick="selectNone()">✗ Aucun</button>
</div>
<div class="agent-list" id="agent-list"><div class="loading"><div class="spinner"></div>Chargement…</div></div>
</div>
<!-- RIGHT: SELECTION DETAILS -->
<div class="right-col">
<div class="selection-box">
<h3>🎯 Pack sélectionné <span class="count-badge" id="sel-count">0</span></h3>
<div class="pack-kpis">
<div class="pack-kpi gold"><div class="l">Savings/an</div><div class="v" id="pk-sav">0€</div></div>
<div class="pack-kpi rose"><div class="l">Impl cost</div><div class="v" id="pk-impl">0€</div></div>
<div class="pack-kpi"><div class="l">Quali avg</div><div class="v" id="pk-quali">—/100</div></div>
<div class="pack-kpi"><div class="l">Effort</div><div class="v" id="pk-effort">— MD</div></div>
</div>
</div>
<div class="selection-box">
<h3>📐 Radar qualitatif (moyenne pack)</h3>
<div class="radar-wrap">
<svg viewBox="0 0 260 240" id="radar-svg">
<defs>
<radialGradient id="rgrad"><stop offset="0%" stop-color="#14b8a6" stop-opacity="0.5"/><stop offset="100%" stop-color="#6366f1" stop-opacity="0.2"/></radialGradient>
</defs>
</svg>
</div>
</div>
<div class="selection-box">
<h3>📈 ROI cumulé 12 mois</h3>
<div class="curve-wrap">
<svg id="curve-svg" viewBox="0 0 280 130" style="width:100%" preserveAspectRatio="none"></svg>
<div style="display:flex;justify-content:space-between;font-size:9.5px;color:var(--mute);margin-top:6px">
<span>M1</span><span>M6</span><span>M12</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const API = '/api/wevia-v67-roi-simulator.php';
let DATA = null;
let sel = new Set();
let ctx = { size: 'mid', maturity: 'medium', vertical: '' };
let deptFilter = 'all';
async function load(){
const r = await fetch(API + '?t=' + Date.now());
DATA = await r.json();
// Normalize quali scores (backend bug: max was 355 instead of 100, divide by 3.55)
const maxObserved = Math.max(...DATA.agents.map(a => a.qualitative_composite_score || 0));
const normFactor = maxObserved > 100 ? 100 / maxObserved : 1;
DATA.agents.forEach(a => {
a.quali_normalized = Math.round(a.qualitative_composite_score * normFactor);
});
renderParams();
renderAgents();
recalc();
}
function fmtEur(n){
if (!n) return '0€';
if (Math.abs(n) >= 1000000) return (n/1000000).toFixed(2)+'M€';
if (Math.abs(n) >= 1000) return (n/1000).toFixed(0)+'k€';
return Math.round(n)+'€';
}
function renderParams(){
const sf = DATA.scaling_factors;
// Size pills
document.getElementById('size-pills').innerHTML = Object.entries(sf.company_size).map(([k,v]) =>
`<button class="param-pill ${ctx.size===k?'active':''}" data-g="size" data-v="${k}">${v.label.split(' ')[0]}</button>`
).join('');
updateSizeInfo();
// Maturity pills
document.getElementById('maturity-pills').innerHTML = Object.entries(sf.maturity_ai).map(([k,v]) =>
`<button class="param-pill ${ctx.maturity===k?'active':''}" data-g="maturity" data-v="${k}">${v.label.split(' ')[0]}</button>`
).join('');
updateMaturityInfo();
// Vertical dropdown
document.getElementById('vertical-select').innerHTML = '<option value="">— aucun (baseline) —</option>' +
Object.entries(sf.verticals).map(([k,v]) => `<option value="${k}">${v.label} (×${v.multiplier})</option>`).join('');
// Dept filters
const depts = [...new Set(DATA.agents.map(a => a.dept))].sort();
const deptLabels = { finance:'💰 Fin', supply:'📦 Sup', manufacturing:'🏭 Mfg', sales:'💼 Sales', hr:'👥 HR', marketing:'📈 Mkt', security:'🔐 Sec', operations:'⚙️ Ops', direction:'👔 Dir' };
document.getElementById('dept-filters').innerHTML =
`<button class="filter-pill ${deptFilter==='all'?'active':''}" data-d="all">Tous (${DATA.agents.length})</button>` +
depts.map(d => {
const n = DATA.agents.filter(a => a.dept === d).length;
return `<button class="filter-pill ${deptFilter===d?'active':''}" data-d="${d}">${deptLabels[d]||d} (${n})</button>`;
}).join('');
// Event listeners
document.querySelectorAll('.param-pill').forEach(b => b.onclick = (e) => {
const g = e.target.dataset.g, v = e.target.dataset.v;
ctx[g] = v;
document.querySelectorAll(`.param-pill[data-g=${g}]`).forEach(x => x.classList.toggle('active', x.dataset.v===v));
if (g === 'size') updateSizeInfo();
if (g === 'maturity') updateMaturityInfo();
recalc();
});
document.getElementById('vertical-select').onchange = (e) => {
ctx.vertical = e.target.value;
updateVertInfo();
recalc();
};
document.querySelectorAll('[data-d]').forEach(b => b.onclick = (e) => {
deptFilter = e.target.dataset.d;
document.querySelectorAll('[data-d]').forEach(x => x.classList.toggle('active', x.dataset.d===deptFilter));
renderAgents();
});
}
function updateSizeInfo(){
const s = DATA.scaling_factors.company_size[ctx.size];
document.getElementById('size-info').textContent = s.label + ' · ' + s.employees + ' employés · mult ×' + s.multiplier;
}
function updateMaturityInfo(){
const m = DATA.scaling_factors.maturity_ai[ctx.maturity];
document.getElementById('maturity-info').textContent = m.label + ' · mult ×' + m.multiplier + ' · ' + m.note;
}
function updateVertInfo(){
if (!ctx.vertical) { document.getElementById('vert-info').textContent = 'Aucun vertical sélectionné (baseline ×1.0)'; return; }
const v = DATA.scaling_factors.verticals[ctx.vertical];
document.getElementById('vert-info').textContent = v.label + ' · mult ×' + v.multiplier + ' · depts amplifiés: ' + v.amplified_depts.join(', ');
}
function scaledSavings(agent){
const sf = DATA.scaling_factors;
let m = sf.company_size[ctx.size].multiplier * sf.maturity_ai[ctx.maturity].multiplier;
if (ctx.vertical){
const v = sf.verticals[ctx.vertical];
m *= v.multiplier;
if (v.amplified_depts.includes(agent.dept)) m *= 1.25;
}
return Math.round(agent.savings_eur_year * m);
}
function renderAgents(){
const wrap = document.getElementById('agent-list');
let list = DATA.agents;
if (deptFilter !== 'all') list = list.filter(a => a.dept === deptFilter);
wrap.innerHTML = list.map(a => {
const scaled = scaledSavings(a);
const isSel = sel.has(a.id);
return `<div class="agent-card ${isSel?'selected':''}" data-id="${a.id}">
<input aria-label="form-field" type="checkbox" ${isSel?'checked':''} data-id="${a.id}">
<div class="ag-main">
<div class="ag-name">${a.agent}</div>
<div class="ag-pain">${a.pain}</div>
<div class="ag-meta">
<span class="dept">${a.dept}</span>
<span>${a.id}</span>
<span>⚡ ${a.complexity}/5</span>
<span>⚠️ ${a.risk_of_failure}/5</span>
<span>⏱ ${a.effort_md} MD</span>
</div>
</div>
<div class="ag-sav"><div class="v">${fmtEur(scaled)}</div><div class="l">/an (scaled)</div></div>
<div class="ag-quali"><div class="v">${a.quali_normalized||'—'}</div><div class="l">/100 quali</div></div>
<div class="ag-payback"><div class="v">${a.payback_months}mo</div><div class="l">payback</div></div>
</div>`;
}).join('');
wrap.querySelectorAll('input[type=checkbox]').forEach(cb => cb.onchange = (e) => {
const id = e.target.dataset.id;
if (e.target.checked) sel.add(id); else sel.delete(id);
const card = e.target.closest('.agent-card');
card.classList.toggle('selected', e.target.checked);
recalc();
});
// Click card = toggle checkbox
wrap.querySelectorAll('.agent-card').forEach(c => {
c.onclick = (e) => {
if (e.target.tagName === 'INPUT') return;
const cb = c.querySelector('input[type=checkbox]');
cb.checked = !cb.checked;
cb.dispatchEvent(new Event('change'));
};
});
}
function selectAll(){
DATA.agents.forEach(a => sel.add(a.id));
renderAgents(); recalc();
}
function selectNone(){
sel.clear(); renderAgents(); recalc();
}
function recalc(){
const sf = DATA.scaling_factors;
let mult = sf.company_size[ctx.size].multiplier * sf.maturity_ai[ctx.maturity].multiplier;
if (ctx.vertical) mult *= sf.verticals[ctx.vertical].multiplier;
document.getElementById('mult-display').textContent = mult.toFixed(2) + '×';
// Selected agents
const selAgents = DATA.agents.filter(a => sel.has(a.id));
const nSel = selAgents.length;
document.getElementById('sel-count').textContent = nSel;
if (nSel === 0){
['qs-sav','qs-impl','qs-pay','qs-npv','pk-sav','pk-impl','pk-quali','pk-effort'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = id.includes('quali') ? '—/100' : id.includes('pay') ? '— mois' : id.includes('effort') ? '— MD' : '0€';
});
document.getElementById('qs-sav-sub').textContent = '— par an';
renderRadar(null);
renderCurve(0);
// Still show all agents list (not filtered by selection)
return;
}
const totalSav = selAgents.reduce((s,a) => s + scaledSavings(a), 0);
const totalImpl = selAgents.reduce((s,a) => s + a.implementation_cost_eur, 0);
const avgPayback = selAgents.reduce((s,a) => s + a.payback_months, 0) / nSel;
const totalEffort = selAgents.reduce((s,a) => s + a.effort_md, 0);
const avgQuali = selAgents.reduce((s,a) => s + (a.quali_normalized||0), 0) / nSel;
const npv3y = totalSav * 3 - totalImpl - totalSav * 0.2 * 3;
document.getElementById('qs-sav').textContent = fmtEur(totalSav);
document.getElementById('qs-sav-sub').textContent = totalSav.toLocaleString('fr-FR') + ' € / an';
document.getElementById('qs-impl').textContent = fmtEur(totalImpl);
document.getElementById('qs-impl-sub').textContent = totalEffort + ' MD × 1200€';
document.getElementById('qs-pay').textContent = avgPayback.toFixed(1) + ' mois';
document.getElementById('qs-npv').textContent = fmtEur(npv3y);
document.getElementById('pk-sav').textContent = fmtEur(totalSav);
document.getElementById('pk-impl').textContent = fmtEur(totalImpl);
document.getElementById('pk-quali').textContent = avgQuali.toFixed(0) + '/100';
document.getElementById('pk-effort').textContent = totalEffort + ' MD';
// Avg qualitative per axis
const axes = ['time_fte_saved','risk_reduction','compliance_uplift','cx_impact','brand_score','strategic_value'];
const avgAxes = {};
axes.forEach(ax => {
avgAxes[ax] = selAgents.reduce((s,a) => s + (a.qualitative?.[ax] || 0), 0) / nSel;
});
renderRadar(avgAxes);
renderCurve(totalSav);
}
function renderRadar(axes){
const svg = document.getElementById('radar-svg');
const cx = 130, cy = 120, r = 80;
const axesNames = ['⏱ Time saved','🛡 Risk↓','📋 Compliance','🙂 CX/NPS','✨ Brand','🎯 Strategic'];
const axesKeys = ['time_fte_saved','risk_reduction','compliance_uplift','cx_impact','brand_score','strategic_value'];
let html = `<defs><radialGradient id="rgrad"><stop offset="0%" stop-color="#14b8a6" stop-opacity="0.6"/><stop offset="100%" stop-color="#6366f1" stop-opacity="0.15"/></radialGradient></defs>`;
// Concentric grid (5 levels)
for (let lvl=1; lvl<=5; lvl++){
const rr = (r*lvl)/5;
html += `<circle cx="${cx}" cy="${cy}" r="${rr}" fill="none" stroke="#1f2436" stroke-width="1"/>`;
}
// Axis lines + labels
axesNames.forEach((name, i) => {
const angle = (Math.PI*2*i)/6 - Math.PI/2;
const x = cx + r * Math.cos(angle), y = cy + r * Math.sin(angle);
html += `<line x1="${cx}" y1="${cy}" x2="${x}" y2="${y}" stroke="#1f2436" stroke-width="1"/>`;
const lx = cx + (r+18) * Math.cos(angle), ly = cy + (r+18) * Math.sin(angle);
html += `<text x="${lx}" y="${ly}" text-anchor="middle" alignment-baseline="middle" font-size="9.5" fill="#94a3b8">${name}</text>`;
});
// Data polygon
if (axes){
const pts = axesKeys.map((k, i) => {
const val = axes[k] || 0;
const angle = (Math.PI*2*i)/6 - Math.PI/2;
const rr = (r*val)/5;
return [cx + rr * Math.cos(angle), cy + rr * Math.sin(angle)];
});
const pathD = 'M' + pts.map(p => p.map(n=>n.toFixed(1)).join(',')).join(' L') + ' Z';
html += `<path d="${pathD}" fill="url(#rgrad)" stroke="#14b8a6" stroke-width="2"/>`;
pts.forEach(p => { html += `<circle cx="${p[0]}" cy="${p[1]}" r="3" fill="#14b8a6"/>`; });
} else {
html += `<text x="${cx}" y="${cy+3}" text-anchor="middle" font-size="10" fill="#64748b">Sélectionner des agents</text>`;
}
svg.innerHTML = html;
}
function renderCurve(maxSav){
const svg = document.getElementById('curve-svg');
const W = 280, H = 130, PAD = 10;
const ramp = [0, 0.05, 0.15, 0.30, 0.45, 0.60, 0.72, 0.82, 0.90, 0.95, 0.98, 1.0];
const pts = ramp.map((p, i) => [PAD + (W-2*PAD)*i/11, H-PAD - (H-2*PAD)*p]);
const pathLine = 'M' + pts.map(p => p.map(n=>n.toFixed(1)).join(',')).join(' L');
const pathArea = pathLine + ` L ${(W-PAD).toFixed(1)} ${(H-PAD)} L ${PAD} ${(H-PAD)} Z`;
let html = `<defs><linearGradient id="lgrad" x1="0" x2="0" y1="0" y2="1"><stop offset="0%" stop-color="#14b8a6" stop-opacity="0.5"/><stop offset="100%" stop-color="#14b8a6" stop-opacity="0"/></linearGradient></defs>`;
// Grid Y
for (let i=0; i<=4; i++){
const y = PAD + (H-2*PAD)*i/4;
html += `<line x1="${PAD}" y1="${y}" x2="${W-PAD}" y2="${y}" stroke="#1f2436" stroke-width="0.5"/>`;
}
html += `<path d="${pathArea}" fill="url(#lgrad)"/>`;
html += `<path d="${pathLine}" fill="none" stroke="#14b8a6" stroke-width="2" stroke-linejoin="round"/>`;
// Labels end
if (maxSav > 0){
html += `<text x="${W-PAD-5}" y="${PAD+12}" text-anchor="end" font-size="10" font-weight="700" fill="#eab308">${fmtEur(maxSav)}/an</text>`;
html += `<text x="${W-PAD-5}" y="${PAD+24}" text-anchor="end" font-size="9" fill="#94a3b8">à M12 (run rate)</text>`;
}
svg.innerHTML = html;
}
function exportJSON(){
const selAgents = DATA.agents.filter(a => sel.has(a.id)).map(a => ({
id: a.id, agent: a.agent, dept: a.dept,
baseline_savings: a.savings_eur_year,
scaled_savings: scaledSavings(a),
impl_cost: a.implementation_cost_eur,
payback_months: a.payback_months,
quali_score: a.quali_normalized
}));
const payload = {
generated: new Date().toISOString(),
context: ctx,
selected_count: selAgents.length,
totals: {
savings: selAgents.reduce((s,a) => s+a.scaled_savings, 0),
impl_cost: selAgents.reduce((s,a) => s+a.impl_cost, 0)
},
agents: selAgents
};
const blob = new Blob([JSON.stringify(payload, null, 2)], {type:'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'weval-roi-simulation-' + Date.now() + '.json';
a.click();
URL.revokeObjectURL(url);
}
load();
</script>
<!-- === OPUS UNIVERSAL DRILL-DOWN v1 19avr — append-only, doctrine #14 === -->
<script>
(function(){
if (window.__opusUniversalDrill) return; window.__opusUniversalDrill = true;
var d = document;
var m = d.createElement('div');
m.id = 'opus-udrill';
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:99995;padding:20px;cursor:pointer';
var inner = d.createElement('div');
inner.id = 'opus-udrill-in';
inner.style.cssText = 'max-width:900px;width:100%;max-height:90vh;overflow:auto;background:#0b0d15;border:1px solid rgba(99,102,241,0.35);border-radius:14px;padding:28px;cursor:default;box-shadow:0 20px 60px rgba(0,0,0,0.6);color:#e2e8f0;font:14px/1.55 Inter,system-ui,sans-serif';
inner.addEventListener('click', function(e){ e.stopPropagation(); });
m.appendChild(inner);
m.addEventListener('click', function(){ m.style.display='none'; });
d.addEventListener('keydown', function(e){ if(e.key==='Escape') m.style.display='none'; });
(d.body || d.documentElement).appendChild(m);
function openCard(card) {
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:14px"><button id="opus-udrill-close" style="padding:6px 14px;background:#171b2a;border:1px solid rgba(99,102,241,0.25);color:#e2e8f0;border-radius:8px;cursor:pointer;font-size:12px">✕ Fermer (Esc)</button></div>';
html += '<div style="transform-origin:top left;font-size:1.05em">' + card.outerHTML + '</div>';
inner.innerHTML = html;
d.getElementById('opus-udrill-close').onclick = function(){ m.style.display='none'; };
m.style.display = 'flex';
}
function wire(root) {
var sels = '.card,[class*="card"],.kpi,[class*="kpi"],.stat,[class*="stat"],.tile,[class*="tile"],.metric,[class*="metric"],.widget,[class*="widget"]';
var cards = root.querySelectorAll(sels);
for (var i = 0; i < cards.length; i++) {
var c = cards[i];
if (c.__opusWired) continue;
if (c.closest('button, a, input, select, textarea, #opus-udrill')) continue;
var r = c.getBoundingClientRect();
if (r.width < 60 || r.height < 40) continue;
c.__opusWired = true;
c.style.cursor = 'pointer';
c.setAttribute('role','button');
c.setAttribute('tabindex','0');
c.addEventListener('click', function(ev){
if (ev.target.closest('[data-pp-id]') && window.__opusDrillInit) return;
if (ev.target.closest('a,button,input,select')) return;
ev.preventDefault(); ev.stopPropagation();
openCard(this);
});
c.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();openCard(this);} });
}
}
var initRun = function(){ wire(d.body || d.documentElement); };
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', initRun);
else initRun();
var mo = new MutationObserver(function(muts){
var newCard = false;
for (var i=0;i<muts.length;i++) if (muts[i].addedNodes.length) { newCard = true; break; }
if (newCard) initRun();
});
mo.observe(d.body || d.documentElement, {childList:true, subtree:true});
})();
</script>
<!-- === OPUS UNIVERSAL DRILL-DOWN END === -->
</body>
</html>