191 lines
10 KiB
JavaScript
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()">×</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");
|
|
})();
|