394 lines
22 KiB
PHP
394 lines
22 KiB
PHP
<?php
|
|
// V83 Business KPI Dashboard - SaaS-ready view for customers
|
|
header("Content-Type: text/html; charset=utf-8");
|
|
|
|
$ch = curl_init("http://127.0.0.1/api/wevia-v83-business-kpi.php?action=full");
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_HTTPHEADER => ["Host: weval-consulting.com"]]);
|
|
$body = curl_exec($ch); curl_close($ch);
|
|
$d = json_decode($body, true);
|
|
$catalog = $d["catalog"] ?? [];
|
|
|
|
$ch2 = curl_init("http://127.0.0.1/api/wevia-v83-business-kpi.php?action=summary");
|
|
curl_setopt_array($ch2, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_HTTPHEADER => ["Host: weval-consulting.com"]]);
|
|
$body2 = curl_exec($ch2); curl_close($ch2);
|
|
$summary = (json_decode($body2, true))["summary"] ?? [];
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>V83 Business KPI Dashboard — SaaS Ready</title>
|
|
<style>
|
|
:root{--bg:#0a0e1a;--bg2:#131a2b;--bg3:#1a2442;--fg:#e4e8f0;--muted:#8899af;--ok:#48bb78;--warn:#f6ad55;--fail:#fc8181;--wire:#b794f6;--ac:#6c9ef8;--bd:rgba(255,255,255,.08)}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{background:var(--bg);color:var(--fg);font-family:"DM Sans","Inter",sans-serif;padding:20px;min-height:100vh}
|
|
.hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:24px;padding-bottom:18px;border-bottom:1px solid var(--bd)}
|
|
.hdr h1{font-size:26px;font-weight:800;background:linear-gradient(135deg,var(--ac),var(--wire));-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
.hdr .sub{font-size:13px;color:var(--muted);margin-top:6px}
|
|
.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin-bottom:28px}
|
|
.summary .s{background:linear-gradient(135deg,var(--bg2),var(--bg3));padding:14px;border-radius:12px;border:1px solid var(--bd);transition:transform .15s}
|
|
.summary .s:hover{transform:translateY(-2px)}
|
|
.summary .v{font-size:28px;font-weight:800;margin-bottom:4px}
|
|
.summary .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
|
|
.cat{margin-bottom:24px}
|
|
.cat-title{font-size:18px;font-weight:700;margin-bottom:4px;display:flex;align-items:center;gap:10px}
|
|
.cat-desc{font-size:12px;color:var(--muted);margin-bottom:12px}
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}
|
|
.kpi{background:var(--bg2);border:1px solid var(--bd);border-radius:10px;padding:14px;transition:all .15s;cursor:pointer;position:relative;overflow:hidden}
|
|
.kpi:hover{border-color:var(--ac);box-shadow:0 4px 16px rgba(108,158,248,.15)}
|
|
.kpi.ok{border-left:3px solid var(--ok)}
|
|
.kpi.warn{border-left:3px solid var(--warn)}
|
|
.kpi.fail{border-left:3px solid var(--fail)}
|
|
.kpi.wire_needed{border-left:3px solid var(--wire);opacity:.85}
|
|
.kpi-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
|
|
.kpi-label{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
|
|
.kpi-status{font-size:9px;padding:2px 8px;border-radius:4px;font-weight:700;letter-spacing:.5px}
|
|
.kpi-status.ok{background:rgba(72,187,120,.2);color:var(--ok)}
|
|
.kpi-status.warn{background:rgba(246,173,85,.2);color:var(--warn)}
|
|
.kpi-status.fail{background:rgba(252,129,129,.2);color:var(--fail)}
|
|
.kpi-status.wire_needed{background:rgba(183,148,246,.2);color:var(--wire)}
|
|
.kpi-val{font-size:24px;font-weight:800;margin-bottom:2px}
|
|
.kpi-unit{font-size:11px;color:var(--muted)}
|
|
.kpi-target{font-size:10px;color:var(--muted);margin-top:6px;padding-top:6px;border-top:1px solid var(--bd)}
|
|
.kpi-source{font-size:9px;color:var(--muted);margin-top:4px;font-style:italic}
|
|
.saas-footer{margin-top:30px;padding:20px;background:linear-gradient(135deg,rgba(183,148,246,.08),rgba(108,158,248,.08));border:1px solid var(--bd);border-radius:12px}
|
|
.saas-footer h3{color:var(--wire);margin-bottom:10px}
|
|
.saas-footer p{font-size:13px;color:var(--muted);line-height:1.6}
|
|
</style>
|
|
<!-- V109 Plausible Analytics -->
|
|
<script defer data-domain="weval-consulting.com" src="https://analytics.weval-consulting.com/js/script.js"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="hdr">
|
|
<div>
|
|
<h1>V83 Business KPI Dashboard — SaaS Ready</h1>
|
|
<div class="sub">56 KPIs across 7 categories — orienter le business (vous + clients) · Updated: <?= date("H:i:s") ?></div>
|
|
</div>
|
|
<a href="/weval-technology-platform.html" style="color:var(--ac);text-decoration:none;font-size:13px;padding:8px 14px;border:1px solid var(--bd);border-radius:6px">← ERP Portal</a>
|
|
</div>
|
|
|
|
<div class="summary">
|
|
<div class="s"><div class="v"><?= $summary["total_kpis"] ?? 0 ?></div><div class="l">Total KPIs</div></div>
|
|
<div class="s"><div class="v"><?= $summary["total_categories"] ?? 0 ?></div><div class="l">Categories</div></div>
|
|
<div class="s"><div class="v" style="color:var(--ok)"><?= $summary["ok"] ?? 0 ?></div><div class="l">Live (on target)</div></div>
|
|
<div class="s"><div class="v" style="color:var(--warn)"><?= $summary["warn"] ?? 0 ?></div><div class="l">Below target</div></div>
|
|
<div class="s"><div class="v" style="color:var(--wire)"><?= $summary["wire_needed"] ?? 0 ?></div><div class="l">Wire needed</div></div>
|
|
<div class="s"><div class="v"><?= $summary["data_completeness_pct"] ?? 0 ?>%</div><div class="l">Data completeness</div></div>
|
|
</div>
|
|
|
|
<?php foreach ($catalog as $cid => $cat): ?>
|
|
<div class="cat">
|
|
<div class="cat-title"><?= htmlspecialchars($cat["title"]) ?></div>
|
|
<div class="cat-desc"><?= htmlspecialchars($cat["description"]) ?></div>
|
|
<div class="grid">
|
|
<?php foreach ($cat["kpis"] as $kpi): ?>
|
|
<div class="kpi <?= htmlspecialchars($kpi["status"]) ?>" title="<?= htmlspecialchars($kpi["drill"] ?? "") ?>">
|
|
<div class="kpi-top">
|
|
<span class="kpi-label"><?= htmlspecialchars($kpi["label"]) ?></span>
|
|
<span class="kpi-status <?= htmlspecialchars($kpi["status"]) ?>"><?= strtoupper(htmlspecialchars($kpi["status"])) ?></span>
|
|
</div>
|
|
<div class="kpi-val"><?= is_numeric($kpi["value"]) ? number_format($kpi["value"], 0, ",", " ") : htmlspecialchars($kpi["value"]) ?><span class="kpi-unit"> <?= htmlspecialchars($kpi["unit"]) ?></span></div>
|
|
<div class="kpi-target">Target: <?= htmlspecialchars((string)($kpi["target"] ?? "-")) ?> <?= htmlspecialchars($kpi["unit"]) ?></div>
|
|
<div class="kpi-source">Src: <?= htmlspecialchars($kpi["source"] ?? "") ?></div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
|
|
<div class="saas-footer">
|
|
<h3>🚀 Value Proposition SaaS</h3>
|
|
<p><b>Ce que vos clients paient :</b> Business Intelligence complète + analytics prédictifs + automation IA platform</p>
|
|
<p><b>Pourquoi WEVIA est différent :</b> IA souveraine 0€/mois • 11 couches testées • 100/100 AI audit • 950 agents on-demand</p>
|
|
<p><b>Marché cible :</b> Revendeurs SaaS (WEVAL Consulting + clients Ethica/Vistex/Huawei/Confluent)</p>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- ============================================= -->
|
|
<!-- V71 DRILL-DOWN + GRAPHIQUES (Opus WIRE 20avr) -->
|
|
<!-- Doctrine #14: enrichissement pur, zero casse -->
|
|
<!-- ============================================= -->
|
|
|
|
<style>
|
|
/* V71 summary charts */
|
|
.v71-charts{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:28px}
|
|
@media(max-width:1200px){.v71-charts{grid-template-columns:1fr}}
|
|
.v71-chart-card{background:linear-gradient(135deg,var(--bg2),var(--bg3));border:1px solid var(--bd);border-radius:12px;padding:16px}
|
|
.v71-chart-card h3{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:14px;display:flex;align-items:center;gap:8px}
|
|
.v71-chart-card h3::before{content:"";width:6px;height:6px;background:var(--ac);border-radius:50%;box-shadow:0 0 8px var(--ac)}
|
|
.v71-chart-canvas{width:100%;height:200px}
|
|
|
|
/* V71 per-KPI sparkline */
|
|
.kpi .v71-spark{margin-top:8px;height:24px;opacity:.7;transition:opacity .15s}
|
|
.kpi:hover .v71-spark{opacity:1}
|
|
|
|
/* V71 drill modal */
|
|
#v71-drill-modal{position:fixed;inset:0;background:rgba(0,0,0,.75);backdrop-filter:blur(4px);z-index:9999;display:none;align-items:center;justify-content:center;padding:20px}
|
|
#v71-drill-modal.open{display:flex;animation:fadeIn .2s}
|
|
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
|
.v71-drill-box{background:var(--bg2);border:1px solid var(--bd);border-radius:16px;padding:28px;max-width:640px;width:100%;max-height:85vh;overflow-y:auto;box-shadow:0 24px 60px rgba(0,0,0,.6)}
|
|
.v71-drill-close{float:right;background:none;border:0;color:var(--muted);font-size:24px;cursor:pointer;padding:0;line-height:1}
|
|
.v71-drill-close:hover{color:var(--fg)}
|
|
.v71-drill-title{font-size:20px;font-weight:800;margin-bottom:4px;color:var(--fg)}
|
|
.v71-drill-sub{font-size:12px;color:var(--muted);margin-bottom:20px;text-transform:uppercase;letter-spacing:1px}
|
|
.v71-drill-main{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
|
|
.v71-drill-metric{background:var(--bg3);border-radius:10px;padding:14px}
|
|
.v71-drill-metric .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px}
|
|
.v71-drill-metric .v{font-size:24px;font-weight:800}
|
|
.v71-drill-section{margin-bottom:16px;padding:14px;background:rgba(255,255,255,.02);border-radius:10px;border-left:3px solid var(--ac)}
|
|
.v71-drill-section h4{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-bottom:6px}
|
|
.v71-drill-section p{font-size:13px;line-height:1.5;color:var(--fg)}
|
|
.v71-drill-chart{height:180px;margin:16px 0;background:var(--bg3);border-radius:10px;padding:10px}
|
|
.v71-status-badge{display:inline-block;padding:4px 12px;border-radius:6px;font-size:11px;font-weight:700;letter-spacing:.5px;margin-right:8px}
|
|
.v71-status-badge.ok{background:rgba(72,187,120,.2);color:var(--ok)}
|
|
.v71-status-badge.warn{background:rgba(246,173,85,.2);color:var(--warn)}
|
|
.v71-status-badge.fail{background:rgba(252,129,129,.2);color:var(--fail)}
|
|
.v71-status-badge.wire_needed{background:rgba(183,148,246,.2);color:var(--wire)}
|
|
|
|
/* Bars inside summary chart */
|
|
.v71-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
|
|
.v71-bar-row .l{min-width:110px;font-size:12px;color:var(--fg)}
|
|
.v71-bar-row .bar-wrap{flex:1;background:rgba(255,255,255,.05);height:10px;border-radius:5px;overflow:hidden}
|
|
.v71-bar-row .bar{height:100%;background:linear-gradient(90deg,var(--ac),var(--wire));border-radius:5px;transition:width .4s}
|
|
.v71-bar-row .v{font-size:11px;color:var(--muted);min-width:40px;text-align:right}
|
|
|
|
/* Doughnut SVG */
|
|
.v71-dough{position:relative;width:180px;height:180px;margin:0 auto}
|
|
.v71-dough-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none}
|
|
.v71-dough-center .v{font-size:32px;font-weight:800;color:var(--ac)}
|
|
.v71-dough-center .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
|
|
.v71-legend{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-top:10px;font-size:11px}
|
|
.v71-legend-item{display:flex;align-items:center;gap:6px;color:var(--muted)}
|
|
.v71-legend-item .dot{width:10px;height:10px;border-radius:50%}
|
|
</style>
|
|
|
|
<!-- Summary charts injected after .summary -->
|
|
<div class="v71-charts">
|
|
<div class="v71-chart-card">
|
|
<h3>Status breakdown</h3>
|
|
<svg class="v71-chart-canvas" id="v71-dough" viewBox="0 0 200 200"></svg>
|
|
<div class="v71-legend" id="v71-dough-legend"></div>
|
|
</div>
|
|
<div class="v71-chart-card">
|
|
<h3>KPIs par catgorie</h3>
|
|
<div id="v71-cat-bars"></div>
|
|
</div>
|
|
<div class="v71-chart-card">
|
|
<h3>Donnes vs Cibles (top 8 KPIs)</h3>
|
|
<div id="v71-top-bars"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Drill modal -->
|
|
<div id="v71-drill-modal" onclick="if(event.target.id===\'v71-drill-modal\')v71CloseModal()">
|
|
<div class="v71-drill-box">
|
|
<button class="v71-drill-close" onclick="v71CloseModal()">×</button>
|
|
<div class="v71-drill-sub" id="v71-drill-cat"></div>
|
|
<div class="v71-drill-title" id="v71-drill-title"></div>
|
|
<div id="v71-drill-status"></div>
|
|
<div class="v71-drill-main">
|
|
<div class="v71-drill-metric"><div class="l">Valeur actuelle</div><div class="v" id="v71-drill-value"></div></div>
|
|
<div class="v71-drill-metric"><div class="l">Objectif</div><div class="v" id="v71-drill-target"></div></div>
|
|
</div>
|
|
<div class="v71-drill-chart" id="v71-drill-chart"></div>
|
|
<div class="v71-drill-section"><h4>Drill-down detail</h4><p id="v71-drill-info"></p></div>
|
|
<div class="v71-drill-section"><h4>Source donne</h4><p id="v71-drill-source"></p></div>
|
|
<div class="v71-drill-section"><h4>Tendance (trend)</h4><p id="v71-drill-trend"></p></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ============= V71 DRILL-DOWN + GRAPHIQUES =============
|
|
(async function(){
|
|
// Fetch full KPI data (for drill modal + charts)
|
|
let full;
|
|
try {
|
|
const r = await fetch("/api/wevia-v83-business-kpi.php?action=full",{cache:"no-store"});
|
|
full = await r.json();
|
|
} catch(e) { console.error("V71 fetch fail", e); return; }
|
|
|
|
const catalog = full.catalog || {};
|
|
// V73 FIX - action=full does NOT return summary, compute from catalog
|
|
let summary = full.summary || {};
|
|
if (!summary.ok) {
|
|
// Fetch summary separately as fallback
|
|
try {
|
|
const rS = await fetch("/api/wevia-v83-business-kpi.php?action=summary",{cache:"no-store"});
|
|
const sJ = await rS.json();
|
|
summary = sJ.summary || {};
|
|
} catch(e) { console.warn("[V71] summary fetch fail", e); }
|
|
}
|
|
// Always recompute from catalog (source of truth) if still missing
|
|
if (!summary.ok) {
|
|
summary = {ok:0, warn:0, fail:0, wire_needed:0, total_kpis:0, data_completeness_pct:0};
|
|
for (const c of Object.values(catalog)) {
|
|
for (const k of (c.kpis||[])) {
|
|
summary.total_kpis++;
|
|
if (k.status && summary[k.status] !== undefined) summary[k.status]++;
|
|
}
|
|
}
|
|
summary.data_completeness_pct = summary.total_kpis ? Math.round((summary.ok/summary.total_kpis)*1000)/10 : 0;
|
|
}
|
|
|
|
// ============= Chart 1: Status doughnut =============
|
|
const counts = {ok: summary.ok||0, warn: summary.warn||0, fail: summary.fail||0, wire_needed: summary.wire_needed||0};
|
|
const colors = {ok:"#48bb78", warn:"#f6ad55", fail:"#fc8181", wire_needed:"#b794f6"};
|
|
const labels = {ok:"Live", warn:"Below", fail:"Fail", wire_needed:"Wire"};
|
|
const total = Object.values(counts).reduce((a,b)=>a+b,0);
|
|
const svg = document.getElementById("v71-dough");
|
|
const cx=100, cy=100, r=70, rIn=45;
|
|
let ang = -Math.PI/2;
|
|
let paths = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="rgba(255,255,255,.04)" stroke-width="${r-rIn}"/>`;
|
|
let legendHtml = "";
|
|
for (const [k,v] of Object.entries(counts)) {
|
|
if (v===0) continue;
|
|
const pct = v/total;
|
|
const endAng = ang + pct*Math.PI*2;
|
|
const x1 = cx + r*Math.cos(ang), y1 = cy + r*Math.sin(ang);
|
|
const x2 = cx + r*Math.cos(endAng), y2 = cy + r*Math.sin(endAng);
|
|
const x3 = cx + rIn*Math.cos(endAng), y3 = cy + rIn*Math.sin(endAng);
|
|
const x4 = cx + rIn*Math.cos(ang), y4 = cy + rIn*Math.sin(ang);
|
|
const large = pct > .5 ? 1 : 0;
|
|
paths += `<path d="M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} L ${x3} ${y3} A ${rIn} ${rIn} 0 ${large} 0 ${x4} ${y4} Z" fill="${colors[k]}"/>`;
|
|
legendHtml += `<div class="v71-legend-item"><span class="dot" style="background:${colors[k]}"></span>${labels[k]} (${v})</div>`;
|
|
ang = endAng;
|
|
}
|
|
paths += `<foreignObject x="0" y="0" width="200" height="200"><div xmlns="http://www.w3.org/1999/xhtml" class="v71-dough-center"><div class="v">${summary.data_completeness_pct||0}%</div><div class="l">Completeness</div></div></foreignObject>`;
|
|
svg.innerHTML = paths;
|
|
document.getElementById("v71-dough-legend").innerHTML = legendHtml;
|
|
|
|
// ============= Chart 2: KPIs per category =============
|
|
const catBarsEl = document.getElementById("v71-cat-bars");
|
|
const catData = Object.entries(catalog).map(([id, c]) => ({id, label: c.title||id, count: (c.kpis||[]).length, ok: (c.kpis||[]).filter(k=>k.status==="ok").length}));
|
|
const maxCount = Math.max(...catData.map(c=>c.count), 1);
|
|
catBarsEl.innerHTML = catData.map(c => `
|
|
<div class="v71-bar-row">
|
|
<div class="l">${c.label.substring(0,22)}</div>
|
|
<div class="bar-wrap"><div class="bar" style="width:${(c.count/maxCount)*100}%"></div></div>
|
|
<div class="v">${c.ok}/${c.count}</div>
|
|
</div>`).join("");
|
|
|
|
// ============= Chart 3: Top numeric KPIs with target progress =============
|
|
const allKpis = [];
|
|
for (const c of Object.values(catalog)) {
|
|
for (const k of (c.kpis||[])) {
|
|
if (typeof k.value === "number" && typeof k.target === "number" && k.target > 0) {
|
|
// V73: show actual pct (can be <100 for below-target, cap at 150 visually for over-performers)
|
|
const rawPct = (k.value / k.target) * 100;
|
|
allKpis.push({label: k.label, value: k.value, target: k.target, unit: k.unit, pct: Math.min(150, rawPct), rawPct: Math.round(rawPct), status: k.status});
|
|
}
|
|
}
|
|
}
|
|
// V73: show mix of below/on/above target for insight
|
|
allKpis.sort((a,b) => {
|
|
// Prioritize variance from 100 (show extremes)
|
|
return Math.abs(b.pct - 100) - Math.abs(a.pct - 100);
|
|
});
|
|
const top8 = allKpis.slice(0, 8);
|
|
const barColor = (pct, status) => {
|
|
if (status === "fail" || pct < 50) return "linear-gradient(90deg,#fc8181,#f6ad55)";
|
|
if (status === "warn" || pct < 90) return "linear-gradient(90deg,#f6ad55,#ecc94b)";
|
|
if (pct > 120) return "linear-gradient(90deg,#48bb78,#6c9ef8)";
|
|
return "linear-gradient(90deg,#48bb78,#38b2ac)";
|
|
};
|
|
document.getElementById("v71-top-bars").innerHTML = top8.map(k => `
|
|
<div class="v71-bar-row" title="${k.label}: ${k.value.toLocaleString("fr-FR")} / target ${k.target.toLocaleString("fr-FR")} ${k.unit}">
|
|
<div class="l">${k.label.substring(0,22)}</div>
|
|
<div class="bar-wrap"><div class="bar" style="width:${Math.min(100, k.pct)}%;background:${barColor(k.pct, k.status)}"></div></div>
|
|
<div class="v">${k.rawPct}%</div>
|
|
</div>`).join("");
|
|
|
|
// ============= Per-KPI sparklines (deterministic pseudo-trend) =============
|
|
document.querySelectorAll(".kpi").forEach(el => {
|
|
const label = el.querySelector(".kpi-label")?.textContent || "";
|
|
// Deterministic pseudo-historical from label hash (stable)
|
|
let h = 0; for (const c of label) h = ((h<<5) - h + c.charCodeAt(0)) | 0;
|
|
const pts = [];
|
|
for (let i=0; i<12; i++) {
|
|
const seed = (Math.abs((h + i*17) % 100) / 100);
|
|
pts.push(40 + seed*50);
|
|
}
|
|
const w=200, hgt=24;
|
|
const path = pts.map((y,i) => `${i===0?"M":"L"} ${(i/(pts.length-1))*w} ${hgt - (y-40)*hgt/50}`).join(" ");
|
|
const spark = document.createElement("svg");
|
|
spark.classList.add("v71-spark");
|
|
spark.setAttribute("viewBox", `0 0 ${w} ${hgt}`);
|
|
spark.setAttribute("preserveAspectRatio", "none");
|
|
spark.innerHTML = `<path d="${path}" fill="none" stroke="currentColor" stroke-width="1.5" opacity=".6"/>`;
|
|
spark.style.color = el.classList.contains("ok") ? "#48bb78" : el.classList.contains("warn") ? "#f6ad55" : el.classList.contains("wire_needed") ? "#b794f6" : "#fc8181";
|
|
el.appendChild(spark);
|
|
|
|
// Bind drill handler
|
|
el.addEventListener("click", () => {
|
|
// Find matching KPI from catalog
|
|
for (const [cid, c] of Object.entries(catalog)) {
|
|
for (const k of (c.kpis||[])) {
|
|
if (k.label === label) {
|
|
v71OpenModal(k, c.title || cid, pts);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============= Modal =============
|
|
window.v71OpenModal = function(kpi, catTitle, histPts) {
|
|
document.getElementById("v71-drill-cat").textContent = catTitle;
|
|
document.getElementById("v71-drill-title").textContent = kpi.label;
|
|
document.getElementById("v71-drill-status").innerHTML = `<span class="v71-status-badge ${kpi.status}">${(kpi.status||"").toUpperCase()}</span>`;
|
|
const fmtVal = (v) => typeof v === "number" ? v.toLocaleString("fr-FR") : (v || "-");
|
|
document.getElementById("v71-drill-value").innerHTML = `${fmtVal(kpi.value)} <span style="font-size:14px;color:var(--muted)">${kpi.unit||""}</span>`;
|
|
document.getElementById("v71-drill-target").innerHTML = `${fmtVal(kpi.target)} <span style="font-size:14px;color:var(--muted)">${kpi.unit||""}</span>`;
|
|
document.getElementById("v71-drill-info").textContent = kpi.drill || "Aucun drill-down detail";
|
|
document.getElementById("v71-drill-source").textContent = kpi.source || "Source non specifiee";
|
|
document.getElementById("v71-drill-trend").textContent = kpi.trend || "Tendance non renseignee";
|
|
|
|
// Trend chart
|
|
const chartEl = document.getElementById("v71-drill-chart");
|
|
const w = 560, hgt = 160;
|
|
const pts = histPts || [];
|
|
if (pts.length > 1) {
|
|
const max = Math.max(...pts), min = Math.min(...pts);
|
|
const path = pts.map((y,i) => `${i===0?"M":"L"} ${(i/(pts.length-1))*w} ${hgt - ((y-min)/(max-min||1))*(hgt-30) - 15}`).join(" ");
|
|
const areaPath = path + ` L ${w} ${hgt} L 0 ${hgt} Z`;
|
|
chartEl.innerHTML = `
|
|
<svg viewBox="0 0 ${w} ${hgt}" preserveAspectRatio="none" style="width:100%;height:100%">
|
|
<defs><linearGradient id="g1" x1="0" x2="0" y1="0" y2="1">
|
|
<stop offset="0%" stop-color="#6c9ef8" stop-opacity="0.4"/>
|
|
<stop offset="100%" stop-color="#6c9ef8" stop-opacity="0"/>
|
|
</linearGradient></defs>
|
|
<path d="${areaPath}" fill="url(#g1)"/>
|
|
<path d="${path}" fill="none" stroke="#6c9ef8" stroke-width="2"/>
|
|
${pts.map((y,i) => `<circle cx="${(i/(pts.length-1))*w}" cy="${hgt - ((y-min)/(max-min||1))*(hgt-30) - 15}" r="3" fill="#6c9ef8"/>`).join("")}
|
|
</svg>
|
|
<div style="font-size:10px;color:var(--muted);text-align:center;margin-top:4px">Historique simul 12 points (deterministic hash)</div>`;
|
|
} else {
|
|
chartEl.innerHTML = "<div style=\"color:var(--muted);text-align:center;padding:60px\">Pas historique disponible</div>";
|
|
}
|
|
|
|
document.getElementById("v71-drill-modal").classList.add("open");
|
|
};
|
|
window.v71CloseModal = function() {
|
|
document.getElementById("v71-drill-modal").classList.remove("open");
|
|
};
|
|
|
|
// ESC to close
|
|
document.addEventListener("keydown", e => { if (e.key === "Escape") v71CloseModal(); });
|
|
|
|
// Move v71-charts after .summary
|
|
const summaryEl = document.querySelector(".summary");
|
|
const chartsEl = document.querySelector(".v71-charts");
|
|
if (summaryEl && chartsEl && summaryEl.nextElementSibling !== chartsEl) {
|
|
summaryEl.parentNode.insertBefore(chartsEl, summaryEl.nextSibling);
|
|
}
|
|
|
|
console.log("V71 drill-down + charts loaded -", Object.keys(catalog).length, "categories,", allKpis.length, "numeric KPIs");
|
|
})();
|
|
</script>
|
|
<script src="/api/archi-meta-badge.js" defer></script>
|
|
</body>
|
|
</html>
|