Files
html/dg-command-center.html
Opus-Yacine 1dca5aaf11
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
V69 DG COMMAND CENTER — Real-time pilotage DG (TOC+Conversion+Data+Marketing+CRM+Risk+Alerts). Backend /api/wevia-v69-dg-command-center.php (17.9KB) agrège V63/V64/V66/source-of-truth/em-kpi/crm-obs en 7 sections: (1) TOC Goldratt 5FS avec 6 streams value chain (Lead Gen/Lead Qualif/Sales/Close/Delivery/Cash) et bottleneck auto-detecté=Lead Qualification (4MQL vs cap 25 - MQL Scoring non déployé). (2) Conversion funnel 7 étapes (Visitors→Leads→MQL→SQL→Opps→Won→Active) avec conv rates step-to-step. (3) Data Pipeline Health 6 (Ethica HCPs/Qdrant/OSS Skills/WEVADS pool/CRM obs/NonReg). (4) Marketing 12 KPIs (HCPs/emails/warmup/seeds/inbox/open/click/conversions/CAC/LTV/deliver/campaigns). (5) CRM view (5 stages pipeline + 8 top accounts Ethica/Vistex/Huawei/OCP/Marjane/Attijariwafa/Maroc Telecom/Deloitte avec next_step/owner/value_keur). (6) Risk Management WEVAL 12 risques matrice 5x5: 4 critical (Pipeline vide/Dep Ethica/No revenue récurrent/Burnout) + 6 high + 2 medium avec agent mitigation + owner. (7) 7 Alerts DG triées priorité avec icon/deadline/action_link (Pipeline anémié/0 conversions/Cash collection/Partnerships dormants/TOC Bottleneck/Plan-action lecture/V67 simulator inutilisé). Page /dg-command-center.html (26KB) premium dark gradient: Header clock auto-refresh + alertes strip top + 4 rows (TOC+Funnel / Data+Marketing / CRM+Accounts / Risk matrix+liste) auto-refresh 20s. Integration WTP: Row 9 banner V69-DG-COMMAND-CENTER 5 mini-stats + CTA + sub-module operations/dg_command_center dans weval-technology-platform-api.php (après fix malformation double-id nesting + removal duplicate wrong-module). Playwright E2E 100%% 0 JS errors: DG alerts=7 (3 critical) toc=6 funnel=7 mkt=12 accounts=8 risk_cells=25 risk_list=8, WTP 16 modules intacts. WEVIA chat integrate-all-confirmed LIVE NonReg 153/153. Plan-action 907 lignes.
2026-04-18 01:49:47 +02:00

462 lines
26 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 · DG Command Center — Real-time Pilotage</title>
<style>
:root {
--bg-0:#05060a; --bg-1:#0b0d15; --bg-2:#11141f; --bg-3:#171b2a;
--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 60%);color:var(--text);min-height:100vh;font-size:13px;line-height:1.5}
.container{max-width:1760px;margin:0 auto;padding:24px 28px 80px}
/* HEADER */
header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:20px;padding-bottom:16px;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;display:flex;align-items:center;gap:10px}
header .sub{color:var(--dim);font-size:12.5px;margin-top:5px}
header .clock{font-family:'JetBrains Mono',monospace;color:var(--accent);font-size:11px;margin-top:4px}
.actions{display:flex;gap:8px}
.btn{padding:7px 13px;background:var(--bg-2);border:1px solid var(--border);color:var(--text);border-radius:8px;font-size:11.5px;cursor:pointer;text-decoration:none;font-family:inherit;transition:all .2s}
.btn:hover{border-color:var(--accent);color:var(--accent)}
.pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 0 rgba(34,197,94,.7);animation:pulse 2s infinite}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(34,197,94,.7)}70%{box-shadow:0 0 0 8px rgba(34,197,94,0)}}
/* LAYOUT GRID */
.row{display:grid;gap:14px;margin-bottom:14px}
.row-4{grid-template-columns:repeat(4,1fr)}
.row-3{grid-template-columns:repeat(3,1fr)}
.row-2{grid-template-columns:2fr 1fr}
.row-2e{grid-template-columns:1fr 1fr}
@media(max-width:1200px){.row-4,.row-3,.row-2,.row-2e{grid-template-columns:1fr 1fr}}
@media(max-width:720px){.row-4,.row-3,.row-2,.row-2e{grid-template-columns:1fr}}
/* CARDS */
.card{background:var(--bg-1);border:1px solid var(--border);border-radius:12px;padding:16px;position:relative;overflow:hidden}
.card.span-2{grid-column:span 2}
.card.span-3{grid-column:span 3}
.card.span-4{grid-column:span 4}
.card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.card-title{font-size:11.5px;color:var(--dim);text-transform:uppercase;letter-spacing:0.6px;font-weight:700;display:flex;align-items:center;gap:6px}
.card-badge{font-size:9.5px;padding:2px 7px;border-radius:8px;font-weight:700;letter-spacing:0.3px;background:rgba(20,184,166,0.15);color:#5eead4}
.card-badge.warn{background:rgba(245,158,11,.18);color:#fbbf24}
.card-badge.danger{background:rgba(239,68,68,.18);color:#fca5a5}
.card-badge.info{background:rgba(99,102,241,.18);color:#a5b4fc}
/* KPI big */
.kpi-big{font-size:32px;font-weight:800;letter-spacing:-0.5px;line-height:1}
.kpi-big.gold{background:linear-gradient(135deg,var(--gold),var(--warn));-webkit-background-clip:text;background-clip:text;color:transparent}
.kpi-big.ok{color:var(--ok)}
.kpi-big.warn{color:var(--warn)}
.kpi-big.danger{color:var(--err)}
.kpi-sub{color:var(--dim);font-size:11px;margin-top:4px}
/* ALERTS DG STRIP */
.alerts-strip{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:10px;margin-bottom:20px}
.alert-card{background:var(--bg-1);border:1px solid var(--border);border-radius:10px;padding:14px 16px;border-left:4px solid var(--warn);position:relative;transition:all .2s}
.alert-card:hover{border-color:var(--border-h);transform:translateY(-2px)}
.alert-card.critical{border-left-color:var(--err);background:linear-gradient(135deg,rgba(239,68,68,0.06),var(--bg-1))}
.alert-card.high{border-left-color:var(--warn)}
.alert-card.medium{border-left-color:var(--cyan)}
.alert-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px}
.alert-title{font-size:13px;font-weight:700;color:var(--text);display:flex;gap:7px;align-items:flex-start}
.alert-lvl{font-size:9px;padding:2px 6px;border-radius:6px;font-weight:700;letter-spacing:0.3px;text-transform:uppercase;flex-shrink:0}
.alert-lvl.critical{background:rgba(239,68,68,0.2);color:#fca5a5}
.alert-lvl.high{background:rgba(245,158,11,0.2);color:#fbbf24}
.alert-lvl.medium{background:rgba(6,182,212,0.18);color:#7dd3fc}
.alert-detail{font-size:11.5px;color:var(--dim);margin:6px 0;line-height:1.45}
.alert-foot{display:flex;justify-content:space-between;align-items:center;margin-top:8px;font-size:10.5px}
.alert-foot .deadline{color:var(--warn);font-weight:600}
.alert-foot a{color:var(--accent);text-decoration:none;font-weight:600}
/* TOC streams */
.toc-wrap{display:flex;flex-direction:column;gap:8px}
.toc-stream{display:grid;grid-template-columns:28px 1fr 60px 80px 1fr;gap:10px;align-items:center;padding:8px 10px;background:var(--bg-2);border-radius:8px;border-left:3px solid var(--dim)}
.toc-stream.bottleneck{border-left-color:var(--err);background:linear-gradient(135deg,rgba(239,68,68,0.06),var(--bg-2));box-shadow:0 0 0 1px rgba(239,68,68,0.25)}
.toc-stream.flow{border-left-color:var(--ok)}
.toc-stream.starved{border-left-color:var(--cyan)}
.toc-icon{font-size:18px;text-align:center}
.toc-label{font-size:12px;font-weight:600;color:var(--text)}
.toc-label .small{color:var(--dim);font-size:10px;font-weight:400;margin-top:2px}
.toc-throughput{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:800;color:var(--text);text-align:center}
.toc-bar-wrap{background:var(--bg-3);height:10px;border-radius:5px;overflow:hidden}
.toc-bar-fill{height:100%;background:linear-gradient(90deg,#14b8a6,#6366f1);transition:width 1.2s cubic-bezier(.4,0,.2,1)}
.toc-bar-fill.bot{background:linear-gradient(90deg,#ef4444,#f59e0b)}
.toc-util{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--dim);text-align:right;font-weight:600}
.toc-constraint{font-size:10px;color:var(--mute);font-style:italic;grid-column:1/-1;padding-left:38px;margin-top:-3px}
/* Funnel */
.funnel-wrap{display:flex;flex-direction:column;gap:6px;align-items:center;padding:10px 0}
.funnel-row{display:grid;grid-template-columns:160px 1fr 70px 50px;gap:10px;align-items:center;width:100%;font-size:12px}
.funnel-label{color:var(--text);font-weight:500;font-size:11.5px}
.funnel-bar-wrap{background:var(--bg-3);height:28px;border-radius:4px;overflow:hidden;position:relative}
.funnel-bar{height:100%;transition:width 1.2s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;padding-left:10px;font-size:11.5px;font-weight:700;color:white}
.funnel-count{font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700;color:var(--text);text-align:right}
.funnel-conv{font-family:'JetBrains Mono',monospace;font-size:10.5px;color:var(--dim);text-align:right}
.funnel-conv.warn{color:var(--warn)}
.funnel-conv.danger{color:var(--err)}
/* Data pipelines */
.dp-wrap{display:grid;grid-template-columns:1fr;gap:6px}
.dp-row{display:grid;grid-template-columns:1fr 80px 1fr 60px;gap:10px;align-items:center;padding:7px 10px;background:var(--bg-2);border-radius:6px;font-size:11.5px}
.dp-name{color:var(--text);font-weight:500}
.dp-vol{font-family:'JetBrains Mono',monospace;font-weight:700;text-align:right;color:var(--text)}
.dp-bar-wrap{background:var(--bg-3);height:8px;border-radius:4px;overflow:hidden}
.dp-bar-fill{height:100%;background:linear-gradient(90deg,var(--ok),var(--cyan));transition:width 1.2s}
.dp-status{font-size:10.5px;text-align:right;font-family:'JetBrains Mono',monospace}
.dp-status.ok{color:var(--ok)} .dp-status.warn{color:var(--warn)} .dp-status.danger{color:var(--err)}
/* Marketing grid */
.mkt-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
@media(max-width:900px){.mkt-grid{grid-template-columns:repeat(2,1fr)}}
.mkt-cell{background:var(--bg-2);border-radius:6px;padding:10px;border-left:2px solid var(--purple)}
.mkt-cell .l{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:0.4px;font-weight:600}
.mkt-cell .v{font-size:17px;font-weight:800;color:var(--text);font-family:'JetBrains Mono',monospace;margin-top:3px;line-height:1}
.mkt-cell .u{font-size:10.5px;color:var(--dim);margin-left:2px}
/* CRM view */
.crm-stage-row{display:grid;grid-template-columns:110px 50px 1fr 80px;gap:10px;align-items:center;padding:7px 10px;background:var(--bg-2);border-radius:6px;font-size:11.5px;margin-bottom:5px}
.stage-label{font-weight:600;color:var(--text)}
.stage-count{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--accent);text-align:center}
.stage-bar-wrap{background:var(--bg-3);height:10px;border-radius:4px;overflow:hidden}
.stage-bar-fill{height:100%;background:linear-gradient(90deg,var(--accent2),var(--purple));transition:width 1.2s}
.stage-val{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--gold);text-align:right;font-size:11px}
.accounts-wrap{display:flex;flex-direction:column;gap:5px}
.acc-row{display:grid;grid-template-columns:1fr 110px 60px;gap:10px;padding:7px 10px;background:var(--bg-2);border-radius:6px;font-size:11px;align-items:center;border-left:2px solid var(--accent2)}
.acc-row:hover{background:var(--bg-3)}
.acc-name{font-weight:600;color:var(--text)}
.acc-name .step{display:block;font-size:10px;color:var(--dim);font-weight:400;margin-top:2px}
.acc-stage{font-size:10px;color:var(--dim);font-family:'JetBrains Mono',monospace}
.acc-val{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--gold);text-align:right}
/* Risk matrix 5x5 */
.rm-wrap{display:grid;grid-template-columns:80px 1fr;gap:10px}
.rm-grid-5x5{display:grid;grid-template-columns:20px repeat(5,1fr);gap:3px}
.rm-cell{aspect-ratio:1.4;display:flex;align-items:center;justify-content:center;border-radius:4px;font-weight:800;font-size:14px;cursor:help;position:relative}
.rm-header{font-size:9px;color:var(--dim);text-align:center;display:flex;align-items:center;justify-content:center}
.rm-sev1{background:rgba(34,197,94,0.15);color:#86efac}
.rm-sev2{background:rgba(132,204,22,0.18);color:#d9f99d}
.rm-sev3{background:rgba(234,179,8,0.2);color:#fef08a}
.rm-sev4{background:rgba(249,115,22,0.22);color:#fed7aa}
.rm-sev5{background:rgba(239,68,68,0.3);color:#fca5a5}
.rm-sev-empty{background:var(--bg-3);color:var(--mute);font-weight:400;font-size:10px}
.risk-list{display:flex;flex-direction:column;gap:5px;margin-top:10px}
.risk-row{display:grid;grid-template-columns:40px 1fr auto;gap:8px;align-items:center;padding:6px 10px;background:var(--bg-2);border-radius:5px;font-size:11px;border-left:2px solid var(--warn)}
.risk-row.critical{border-left-color:var(--err)}
.risk-row.high{border-left-color:var(--warn)}
.risk-row.medium{border-left-color:var(--cyan)}
.risk-id{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--mute)}
.risk-title{color:var(--text);font-weight:500}
.risk-title .mit{display:block;font-size:10px;color:var(--dim);font-style:italic;margin-top:2px}
.risk-score{font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:800;color:var(--text);text-align:right}
.loading{text-align:center;padding:50px;color:var(--dim)}
.spinner{width:38px;height:38px;border:3px solid var(--bg-3);border-top-color:var(--accent);border-radius:50%;margin:0 auto 14px;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body>
<div class="container">
<header>
<div>
<h1><span>🎖️</span>DG Command Center <span class="pulse"></span></h1>
<div class="sub">Real-time pilotage — TOC · Conversion · Data · Marketing · CRM · Risk · Alertes</div>
<div class="clock" id="clock"></div>
</div>
<div class="actions">
<a href="/weval-technology-platform.html" class="btn">🏠 WTP</a>
<a href="/agent-roi-simulator.html" class="btn">🧮 ROI Sim</a>
<a href="/crm.html" class="btn">💼 CRM</a>
<button class="btn" id="btn-refresh" onclick="load()">↻ Refresh</button>
</div>
</header>
<!-- ALERTS DG TOP -->
<div class="card" style="margin-bottom:20px;border-left:4px solid var(--err)">
<div class="card-head">
<div class="card-title">🚨 Alertes DG — à traiter maintenant <span class="card-badge danger" id="alerts-count">— alertes</span></div>
<div class="card-badge" id="alerts-critical"></div>
</div>
<div class="alerts-strip" id="alerts-strip"><div class="loading"><div class="spinner"></div></div></div>
</div>
<!-- ROW: TOC + Conversion Funnel -->
<div class="row row-2">
<div class="card" id="toc-card">
<div class="card-head">
<div class="card-title">🎯 TOC Theory of Constraints — Goldratt</div>
<div class="card-badge danger" id="toc-bot-badge">— bottleneck</div>
</div>
<div class="toc-wrap" id="toc-streams"><div class="loading"><div class="spinner"></div></div></div>
<div style="margin-top:12px;padding:10px 12px;background:var(--bg-2);border-radius:6px;font-size:10.5px;color:var(--dim);line-height:1.5">
<strong style="color:var(--accent)">5 Focusing Steps (Goldratt):</strong>
1. Identifier la contrainte · 2. Exploiter (max) · 3. Subordonner tout le reste · 4. Élever la contrainte · 5. Si brisée → reprendre au 1
</div>
</div>
<div class="card">
<div class="card-head">
<div class="card-title">🎚️ Conversion Funnel</div>
<div class="card-badge info" id="conv-overall">— %</div>
</div>
<div class="funnel-wrap" id="funnel-wrap"><div class="loading"><div class="spinner"></div></div></div>
</div>
</div>
<!-- ROW: Data pipelines + Marketing KPIs -->
<div class="row row-2e">
<div class="card">
<div class="card-head"><div class="card-title">🔌 Data Pipelines Health</div><div class="card-badge" id="dp-badge">live</div></div>
<div class="dp-wrap" id="dp-wrap"><div class="loading"><div class="spinner"></div></div></div>
</div>
<div class="card">
<div class="card-head"><div class="card-title">📣 Marketing KPIs</div><div class="card-badge info">WEVADS + Ethica</div></div>
<div class="mkt-grid" id="mkt-grid"><div class="loading"><div class="spinner"></div></div></div>
</div>
</div>
<!-- ROW: CRM pipeline + Top accounts -->
<div class="row row-2e">
<div class="card">
<div class="card-head">
<div class="card-title">💼 CRM Pipeline by Stage</div>
<div class="card-badge info" id="pipe-val">— k€</div>
</div>
<div id="crm-stages"><div class="loading"><div class="spinner"></div></div></div>
<div style="margin-top:12px;padding-top:10px;border-top:1px dashed var(--border);display:grid;grid-template-columns:repeat(4,1fr);gap:8px;font-size:10.5px;color:var(--dim)">
<div><strong style="color:var(--text);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-opps"></strong>Opps actives</div>
<div><strong style="color:var(--ok);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-won"></strong>Won ce mois</div>
<div><strong style="color:var(--err);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-lost"></strong>Lost</div>
<div><strong style="color:var(--purple);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-cycle"></strong>Cycle (j)</div>
</div>
</div>
<div class="card">
<div class="card-head"><div class="card-title">🎯 Top Accounts &amp; Next Steps</div><div class="card-badge info" id="acc-badge"></div></div>
<div class="accounts-wrap" id="accounts-wrap"><div class="loading"><div class="spinner"></div></div></div>
</div>
</div>
<!-- ROW: Risk Management 5x5 + Risk list -->
<div class="row row-2e">
<div class="card">
<div class="card-head">
<div class="card-title">⚠️ Risk Management WEVAL — Matrice 5×5</div>
<div class="card-badge danger" id="risk-count"></div>
</div>
<div class="rm-wrap">
<div style="display:flex;flex-direction:column;justify-content:space-around;font-size:10px;color:var(--dim);text-align:right;font-weight:600">
<div>Likelihood</div>
<div>L=5</div><div>L=4</div><div>L=3</div><div>L=2</div><div>L=1</div>
</div>
<div>
<div class="rm-grid-5x5" id="risk-matrix"></div>
<div style="display:grid;grid-template-columns:20px repeat(5,1fr);gap:3px;margin-top:4px">
<div></div>
<div class="rm-header">Impact 1</div><div class="rm-header">2</div><div class="rm-header">3</div><div class="rm-header">4</div><div class="rm-header">5</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-head"><div class="card-title">📋 Top 8 Risques à traiter</div><div class="card-badge danger" id="risks-prio"></div></div>
<div class="risk-list" id="risk-list"><div class="loading"><div class="spinner"></div></div></div>
</div>
</div>
</div>
<script>
const API = '/api/wevia-v69-dg-command-center.php';
let DATA = null;
function clockTick(){
const d = new Date();
document.getElementById('clock').textContent = d.toLocaleDateString('fr-FR') + ' · ' + d.toLocaleTimeString('fr-FR') + ' · auto-refresh 20s';
}
setInterval(clockTick, 1000); clockTick();
async function load(){
try {
const r = await fetch(API + '?t=' + Date.now());
DATA = await r.json();
render();
} catch(e) { console.error(e); }
}
function fmt(n){
if (!n && n !== 0) return '—';
if (Math.abs(n) >= 1000000) return (n/1000000).toFixed(2)+'M';
if (Math.abs(n) >= 1000) return (n/1000).toFixed(1)+'k';
return Math.round(n);
}
function render(){
if (!DATA) return;
const s = DATA.summary;
// Alerts
const alerts = DATA.alerts_dg || [];
document.getElementById('alerts-count').textContent = alerts.length + ' alertes';
document.getElementById('alerts-critical').textContent = s.alerts_critical + ' critical';
document.getElementById('alerts-critical').className = 'card-badge ' + (s.alerts_critical > 0 ? 'danger' : 'info');
document.getElementById('alerts-strip').innerHTML = alerts.map(a => `
<div class="alert-card ${a.level}">
<div class="alert-head">
<div class="alert-title"><span>${a.icon}</span>${a.title}</div>
<div class="alert-lvl ${a.level}">${a.level}</div>
</div>
<div class="alert-detail">${a.detail}</div>
<div class="alert-foot">
<span class="deadline">⏱ ${a.deadline}</span>
<a href="${a.action_link}">→ Action</a>
</div>
</div>
`).join('');
// TOC
const streams = (DATA.toc && DATA.toc.streams) || [];
document.getElementById('toc-bot-badge').textContent = '🔴 ' + (s.toc_bottleneck_label || '—');
document.getElementById('toc-streams').innerHTML = streams.map(st => {
const isBot = st.id === DATA.toc.bottleneck;
const pct = Math.min(100, st.utilization_pct);
return `<div class="toc-stream ${st.status} ${isBot?'bottleneck':''}">
<div class="toc-icon">${st.icon}</div>
<div class="toc-label">${st.label}${isBot?' <span class="card-badge danger" style="margin-left:4px">GOULET</span>':''}<div class="small">${st.constraint}</div></div>
<div class="toc-throughput">${st.throughput}<div style="font-size:9px;color:var(--mute);font-weight:400">${st.unit}</div></div>
<div><div class="toc-bar-wrap"><div class="toc-bar-fill ${isBot?'bot':''}" style="width:0%" data-w="${pct}"></div></div></div>
<div class="toc-util">${pct.toFixed(0)}%<div style="font-size:9px;color:var(--mute)">cap ${st.capacity}</div></div>
</div>`;
}).join('');
setTimeout(()=>document.querySelectorAll('.toc-bar-fill').forEach(el=>el.style.width=el.dataset.w+'%'), 80);
// Funnel
const funnel = DATA.conversion_funnel || [];
document.getElementById('conv-overall').textContent = s.conversion_overall_pct.toFixed(3) + '% overall';
const maxCount = Math.max(...funnel.map(f=>f.count), 1);
document.getElementById('funnel-wrap').innerHTML = funnel.map((f,i) => {
const w = (f.count/maxCount)*100;
const cls = (f.conv_pct||100) < 15 ? 'danger' : (f.conv_pct||100) < 35 ? 'warn' : '';
return `<div class="funnel-row">
<div class="funnel-label">${f.step}</div>
<div class="funnel-bar-wrap"><div class="funnel-bar" style="width:0%;background:${f.color}" data-w="${w}">${f.count}</div></div>
<div class="funnel-count">${fmt(f.count)}</div>
<div class="funnel-conv ${cls}">${f.conv_pct||100}%</div>
</div>`;
}).join('');
setTimeout(()=>document.querySelectorAll('.funnel-bar').forEach(el=>el.style.width=el.dataset.w+'%'), 100);
// Data pipelines
const dp = DATA.data_pipeline || [];
document.getElementById('dp-wrap').innerHTML = dp.map(d => {
const pct = d.target ? Math.min(100, (d.volume/d.target)*100) : 100;
return `<div class="dp-row">
<div class="dp-name">${d.name}</div>
<div class="dp-vol">${fmt(d.volume)} ${d.unit||''}</div>
<div class="dp-bar-wrap"><div class="dp-bar-fill" style="width:0%" data-w="${pct}"></div></div>
<div class="dp-status ${d.status}">${d.status}</div>
</div>`;
}).join('');
setTimeout(()=>document.querySelectorAll('.dp-bar-fill').forEach(el=>el.style.width=el.dataset.w+'%'), 120);
// Marketing
const m = DATA.marketing || {};
const mktCells = [
{l:'HCPs Maghreb', v:fmt(m.ethica_hcps), u:''},
{l:'Emails valides', v:fmt(m.emails_validated), u:''},
{l:'Warmup accts', v:fmt(m.warmup_accounts), u:''},
{l:'Seeds actifs', v:fmt(m.seed_accounts), u:''},
{l:'Inbox rate', v:m.inbox_rate_pct, u:'%'},
{l:'Open rate', v:m.open_rate_pct, u:'%'},
{l:'Click rate', v:m.click_rate_pct, u:'%'},
{l:'Conversions', v:m.conversions_month, u:'/mois'},
{l:'CAC', v:m.cac_eur, u:'€'},
{l:'LTV', v:m.ltv_eur, u:'€'},
{l:'Deliver. mean wk', v:m.email_deliverability_mean_week, u:'%'},
{l:'Campaigns live', v:2, u:''}
];
document.getElementById('mkt-grid').innerHTML = mktCells.map(c => `<div class="mkt-cell"><div class="l">${c.l}</div><div class="v">${c.v}<span class="u">${c.u}</span></div></div>`).join('');
// CRM pipeline by stage
const crm = DATA.crm || {};
const stages = crm.pipeline_by_stage || [];
document.getElementById('pipe-val').textContent = fmt(crm.pipeline_value_keur) + ' k€';
document.getElementById('crm-opps').textContent = crm.opportunities_active;
document.getElementById('crm-won').textContent = crm.deals_won_month;
document.getElementById('crm-lost').textContent = crm.deals_lost_month;
document.getElementById('crm-cycle').textContent = crm.avg_cycle_days;
const maxVal = Math.max(...stages.map(s=>s.value_keur), 1);
document.getElementById('crm-stages').innerHTML = stages.map(st => {
const w = (st.value_keur/maxVal)*100;
return `<div class="crm-stage-row">
<div class="stage-label">${st.stage}</div>
<div class="stage-count">${st.count}</div>
<div><div class="stage-bar-wrap"><div class="stage-bar-fill" style="width:0%" data-w="${w}"></div></div></div>
<div class="stage-val">${fmt(st.value_keur)} k€</div>
</div>`;
}).join('');
setTimeout(()=>document.querySelectorAll('.stage-bar-fill').forEach(el=>el.style.width=el.dataset.w+'%'), 130);
// Top accounts
const accs = crm.top_accounts || [];
document.getElementById('acc-badge').textContent = accs.length + ' accounts';
document.getElementById('accounts-wrap').innerHTML = accs.map(a => `
<div class="acc-row">
<div class="acc-name">${a.name}<span class="step">→ ${a.next_step}</span></div>
<div class="acc-stage">${a.stage}</div>
<div class="acc-val">${a.value_keur ? fmt(a.value_keur)+'k€' : '—'}</div>
</div>
`).join('');
// Risk matrix 5x5
const risks = DATA.risks || [];
document.getElementById('risk-count').textContent = s.risks_critical + ' critical · ' + s.risks_high + ' high';
const grid = {};
risks.forEach(r => { const k=r.likelihood+'_'+r.impact; grid[k]=(grid[k]||[]); grid[k].push(r); });
let rmHtml = '<div></div>'; // corner top-left
for (let imp=1; imp<=5; imp++) rmHtml += `<div class="rm-header" style="height:14px"></div>`; // column headers actually placed below
// Rows L=5 → L=1 (high likelihood at top)
rmHtml = '';
for (let l=5; l>=1; l--) {
rmHtml += `<div class="rm-header" style="font-size:10px">L=${l}</div>`;
for (let i=1; i<=5; i++) {
const cells = grid[l+'_'+i] || [];
const sev = l*i;
let cls = 'rm-sev-empty';
if (sev >= 20) cls = 'rm-sev5';
else if (sev >= 15) cls = 'rm-sev5';
else if (sev >= 10) cls = 'rm-sev4';
else if (sev >= 6) cls = 'rm-sev3';
else if (sev >= 3) cls = 'rm-sev2';
else cls = 'rm-sev1';
rmHtml += `<div class="rm-cell ${cls}" title="${cells.map(c=>c.id+': '+c.title).join(' · ')}">${cells.length || '·'}</div>`;
}
}
document.getElementById('risk-matrix').innerHTML = rmHtml;
// Risk list (top 8 by severity)
const sorted = [...risks].sort((a,b) => (b.likelihood*b.impact) - (a.likelihood*a.impact)).slice(0, 8);
document.getElementById('risks-prio').textContent = risks.length + ' risques total';
document.getElementById('risk-list').innerHTML = sorted.map(r => `
<div class="risk-row ${r.priority}">
<div class="risk-id">${r.id}</div>
<div class="risk-title">${r.title}<span class="mit">🛡 ${r.mitigation}</span></div>
<div class="risk-score">${r.likelihood}×${r.impact}=<strong>${r.likelihood*r.impact}</strong></div>
</div>
`).join('');
}
load();
setInterval(load, 20000);
</script>
</body>
</html>