auto-sync via WEVIA git_sync_all intent 2026-04-20T03:38:46+02:00
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled

This commit is contained in:
opus
2026-04-20 03:38:46 +02:00
parent cd844a7499
commit 910c76bd7c
2 changed files with 41 additions and 9 deletions

View File

@@ -1,7 +1,7 @@
{
"ok": true,
"version": "V83-business-kpi",
"ts": "2026-04-20T01:36:20+00:00",
"ts": "2026-04-20T01:38:27+00:00",
"summary": {
"total_categories": 7,
"total_kpis": 56,

View File

@@ -208,7 +208,27 @@ body{background:var(--bg);color:var(--fg);font-family:"DM Sans","Inter",sans-ser
} catch(e) { console.error("V71 fetch fail", e); return; }
const catalog = full.catalog || {};
const summary = (full.summary || {});
// 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};
@@ -252,18 +272,30 @@ body{background:var(--bg);color:var(--fg);font-family:"DM Sans","Inter",sans-ser
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 && k.value > 0) {
allKpis.push({label: k.label, value: k.value, target: k.target, unit: k.unit, pct: Math.min(100, (k.value/k.target)*100)});
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});
}
}
}
allKpis.sort((a,b) => b.pct - a.pct);
// 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">
<div class="l" title="${k.label}">${k.label.substring(0,22)}</div>
<div class="bar-wrap"><div class="bar" style="width:${k.pct}%"></div></div>
<div class="v">${Math.round(k.pct)}%</div>
<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) =============