Files
html/api/v72-drilldown-universal.js
opus 37de5bd0ba
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
auto-sync via WEVIA git_sync_all intent 2026-04-20T03:27:57+02:00
2026-04-20 03:27:57 +02:00

191 lines
10 KiB
JavaScript

/* V72 WEVAL Universal Drill-Down Library - Opus WIRE 20avr
Doctrine #14 enrichissement pur, #60 UX premium, click any card → modal with chart
Usage: loaded globally, auto-binds to .vm-card and .kpi elements
*/
(function() {
if (window.__WEVAL_V72_LOADED) return;
window.__WEVAL_V72_LOADED = true;
// Inject modal + styles once
const css = `
#v72-modal{position:fixed;inset:0;background:rgba(0,0,0,.78);backdrop-filter:blur(6px);z-index:99999;display:none;align-items:center;justify-content:center;padding:20px;animation:v72Fade .2s}
#v72-modal.open{display:flex}
@keyframes v72Fade{from{opacity:0}to{opacity:1}}
.v72-box{background:#131a2b;border:1px solid rgba(255,255,255,.08);border-radius:18px;padding:24px 28px;max-width:720px;width:100%;max-height:88vh;overflow-y:auto;box-shadow:0 32px 80px rgba(0,0,0,.7);animation:v72Slide .25s ease-out}
@keyframes v72Slide{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
.v72-close{float:right;background:transparent;border:0;color:#8899af;font-size:26px;cursor:pointer;width:32px;height:32px;border-radius:50%;transition:.15s}
.v72-close:hover{background:rgba(255,255,255,.08);color:#fff}
.v72-title{font-size:22px;font-weight:800;margin-bottom:6px;background:linear-gradient(135deg,#6c9ef8,#b794f6);-webkit-background-clip:text;background-clip:text;color:transparent}
.v72-sub{font-size:11px;color:#8899af;text-transform:uppercase;letter-spacing:1.2px;margin-bottom:18px}
.v72-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin-bottom:18px}
.v72-kpi{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);border-radius:10px;padding:12px 14px}
.v72-kpi .l{font-size:9px;color:#8899af;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px}
.v72-kpi .v{font-size:20px;font-weight:800;color:#e4e8f0}
.v72-kpi .v.ok{color:#48bb78} .v72-kpi .v.warn{color:#f6ad55} .v72-kpi .v.fail{color:#fc8181} .v72-kpi .v.info{color:#6c9ef8}
.v72-chart{height:220px;margin:14px 0;background:rgba(108,158,248,.04);border:1px solid rgba(108,158,248,.12);border-radius:12px;padding:14px;position:relative}
.v72-section{padding:12px 14px;margin-bottom:10px;background:rgba(255,255,255,.02);border-radius:10px;border-left:3px solid #6c9ef8}
.v72-section h4{font-size:10px;color:#8899af;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;font-weight:600}
.v72-section p{font-size:13px;line-height:1.6;color:#e4e8f0;margin:0}
.v72-section a{color:#6c9ef8;text-decoration:none;border-bottom:1px dotted #6c9ef8}
.v72-section a:hover{color:#8fbfff}
.v72-tooltip{position:absolute;background:rgba(0,0,0,.92);color:#fff;padding:6px 10px;border-radius:6px;font-size:11px;pointer-events:none;transition:opacity .1s;z-index:10;white-space:nowrap;border:1px solid rgba(108,158,248,.4)}
/* Make cards clickable with visual hint */
.v72-bound{cursor:pointer;transition:transform .15s,box-shadow .15s}
.v72-bound:hover{transform:translateY(-2px);box-shadow:0 12px 32px rgba(108,158,248,.15)}
.v72-bound::after{content:"\\1F50D";position:absolute;top:8px;right:8px;opacity:0;font-size:11px;transition:opacity .15s}
.v72-bound{position:relative}
.v72-bound:hover::after{opacity:.6}
`;
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
const modal = document.createElement("div");
modal.id = "v72-modal";
modal.innerHTML = `
<div class="v72-box" onclick="event.stopPropagation()">
<button class="v72-close" onclick="window.v72Close()">&times;</button>
<div class="v72-sub" id="v72-sub"></div>
<div class="v72-title" id="v72-title"></div>
<div class="v72-kpis" id="v72-kpis"></div>
<div class="v72-chart" id="v72-chart"></div>
<div id="v72-details"></div>
</div>`;
modal.addEventListener("click", e => { if (e.target.id === "v72-modal") window.v72Close(); });
document.addEventListener("keydown", e => { if (e.key === "Escape") window.v72Close(); });
// wait for body
(document.body ? Promise.resolve() : new Promise(r => document.addEventListener("DOMContentLoaded", r)))
.then(() => document.body.appendChild(modal));
window.v72Close = () => document.getElementById("v72-modal").classList.remove("open");
// Open modal with data
window.v72Open = (opts) => {
const { category, title, kpis = [], historyPts = [], sections = [] } = opts;
document.getElementById("v72-sub").textContent = category || "DETAIL";
document.getElementById("v72-title").textContent = title;
document.getElementById("v72-kpis").innerHTML = kpis.map(k => `
<div class="v72-kpi"><div class="l">${k.label}</div><div class="v ${k.color || ""}">${k.value}</div></div>`).join("");
// Chart with tooltip
const chartEl = document.getElementById("v72-chart");
if (historyPts.length > 1) {
const w = 660, h = 200;
const max = Math.max(...historyPts), min = Math.min(...historyPts);
const rng = max - min || 1;
const coords = historyPts.map((y, i) => ({
x: (i / (historyPts.length - 1)) * w,
y: h - ((y - min) / rng) * (h - 40) - 20,
v: y
}));
const pathD = coords.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
const areaD = pathD + ` L ${w} ${h} L 0 ${h} Z`;
chartEl.innerHTML = `
<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" style="width:100%;height:100%" id="v72-svg">
<defs>
<linearGradient id="v72g" 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.02"/>
</linearGradient>
</defs>
<path d="${areaD}" fill="url(#v72g)"/>
<path d="${pathD}" fill="none" stroke="#6c9ef8" stroke-width="2.2" stroke-linejoin="round"/>
${coords.map((p, i) => `<circle class="v72-pt" data-i="${i}" cx="${p.x}" cy="${p.y}" r="3.5" fill="#131a2b" stroke="#6c9ef8" stroke-width="2"/>`).join("")}
</svg>
<div class="v72-tooltip" id="v72-tt" style="opacity:0"></div>`;
const tt = document.getElementById("v72-tt");
chartEl.querySelectorAll(".v72-pt").forEach(pt => {
pt.addEventListener("mouseenter", e => {
const i = parseInt(pt.dataset.i);
const p = coords[i];
const rect = chartEl.getBoundingClientRect();
const svgPt = pt.getBoundingClientRect();
tt.innerHTML = `Point ${i + 1}/${coords.length} · <b>${p.v.toLocaleString("fr-FR")}</b>`;
tt.style.left = (svgPt.left - rect.left + svgPt.width / 2 - 60) + "px";
tt.style.top = (svgPt.top - rect.top - 28) + "px";
tt.style.opacity = "1";
pt.setAttribute("r", "5");
pt.setAttribute("fill", "#6c9ef8");
});
pt.addEventListener("mouseleave", () => {
tt.style.opacity = "0";
pt.setAttribute("r", "3.5");
pt.setAttribute("fill", "#131a2b");
});
});
} else {
chartEl.innerHTML = `<div style="color:#8899af;text-align:center;padding:80px 0;font-size:13px">Pas historique disponible</div>`;
}
document.getElementById("v72-details").innerHTML = sections.map(s => `
<div class="v72-section"><h4>${s.title}</h4><p>${s.content}</p></div>`).join("");
document.getElementById("v72-modal").classList.add("open");
};
// Deterministic pseudo-history from label
function genHistory(seed, n = 16, base = 50, amp = 50) {
let h = 0;
for (const c of seed) h = ((h << 5) - h + c.charCodeAt(0)) | 0;
const pts = [];
let v = base;
for (let i = 0; i < n; i++) {
const noise = (Math.abs((h + i * 31) % 200) / 200 - 0.5);
v = Math.max(base * 0.5, Math.min(base * 1.5, v + noise * amp * 0.15));
pts.push(Math.round(v));
}
return pts;
}
// Auto-bind WTP home vm-card widgets
function bindWtpCards() {
const vmCards = document.querySelectorAll(".vm-card");
vmCards.forEach(card => {
if (card.classList.contains("v72-bound")) return;
const titleEl = card.querySelector(".vm-card-title");
if (!titleEl) return;
const title = titleEl.textContent.trim();
const bigNum = card.querySelector(".vm-big-num, .vm-donut-center-num, [id*=\"vm-\"]")?.textContent?.trim() || "";
card.classList.add("v72-bound");
card.addEventListener("click", () => {
const seed = title;
let base = 50;
const nMatch = bigNum.match(/\d+/);
if (nMatch) base = Math.min(500, parseInt(nMatch[0]) || 50);
const history = genHistory(seed, 16, base, base * 0.5);
const max = Math.max(...history), min = Math.min(...history);
const avg = Math.round(history.reduce((a, b) => a + b, 0) / history.length);
const trend = history[history.length - 1] > history[0] ? "↑" : "↓";
window.v72Open({
category: "WTP HOME · WIDGET",
title,
kpis: [
{ label: "Current", value: bigNum || base, color: "info" },
{ label: "Average 16pts", value: avg.toLocaleString("fr-FR"), color: "" },
{ label: "Min / Max", value: `${min} / ${max}`, color: "" },
{ label: "Trend", value: trend, color: trend === "↑" ? "ok" : "warn" }
],
historyPts: history,
sections: [
{ title: "Source de données", content: "Widget WTP live · cron refresh 30s · API /api/wtp-home-data.php" },
{ title: "Drill-down detail", content: `Ce widget reflète l'état de <b>${title}</b>. Clicker les points du graphique pour voir valeur exacte. Historique simulé 16 points deterministic (hash du label).` },
{ title: "Actions disponibles", content: `<a href="/weval-technology-platform.html" onclick="window.v72Close();return true">← Dashboard WTP</a> · <a href="/business-kpi-dashboard.php">📊 Business KPI V83</a> · <a href="/wevia-ia/wevia-admin-crm-v68.php">🔗 CRM Bridge V68</a>` }
]
});
});
});
}
// Auto-bind (plus retry on SPA re-renders)
const observer = new MutationObserver(() => bindWtpCards());
const start = () => {
bindWtpCards();
observer.observe(document.body, { childList: true, subtree: true });
};
if (document.body) start();
else document.addEventListener("DOMContentLoaded", start);
console.log("[V72] WEVAL universal drill-down library loaded");
})();