Files
html/wtp-drilldown-charts.html
opus d5edaa769c
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
auto-sync via WEVIA git_sync_all intent 2026-04-21T14:56:43+02:00
2026-04-21 14:56:43 +02:00

394 lines
20 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 · Visual Mgmt Drillable · 9 modules graphique</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#09090b; --surface:#13131a; --surface2:#1a1a24; --border:#2a2a35;
--text:#fafafa; --muted:#94a3b8; --accent:#6366f1; --accent2:#a855f7;
--green:#22c55e; --amber:#f59e0b; --red:#ef4444; --cyan:#22d3ee;
--grad:linear-gradient(135deg,#6366f1 0%,#a855f7 50%,#ec4899 100%);
}
body{font-family:-apple-system,Inter,Segoe UI,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;line-height:1.5}
.topbar{background:var(--surface);border-bottom:1px solid var(--border);padding:14px 28px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:100;backdrop-filter:blur(20px)}
.topbar h1{font-size:1.1rem;font-weight:700;background:var(--grad);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-0.02em}
.topbar .meta{display:flex;gap:14px;align-items:center;font-size:.78rem;color:var(--muted)}
.topbar .live{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-weight:600}
.topbar .live::before{content:'';width:8px;height:8px;background:var(--green);border-radius:99px;box-shadow:0 0 12px var(--green);animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.bread{padding:14px 28px;background:var(--surface);font-size:.78rem;color:var(--muted);border-bottom:1px solid var(--border)}
.bread a{color:var(--accent);text-decoration:none;margin-right:6px}
main{padding:28px;max-width:1700px;margin:0 auto}
.hero{background:linear-gradient(135deg,rgba(99,102,241,.1),rgba(168,85,247,.05));border:1px solid rgba(99,102,241,.2);border-radius:16px;padding:24px 28px;margin-bottom:24px;position:relative;overflow:hidden}
.hero::before{content:'';position:absolute;top:-50%;right:-10%;width:300px;height:300px;background:radial-gradient(circle,rgba(99,102,241,.15),transparent 70%);pointer-events:none}
.hero h2{font-size:1.4rem;margin-bottom:6px;background:var(--grad);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.hero p{color:var(--muted);font-size:.85rem;line-height:1.6}
.modules-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(330px,1fr));gap:18px;margin-bottom:24px}
.module-card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:0;position:relative;overflow:hidden;cursor:pointer;transition:.25s;display:flex;flex-direction:column}
.module-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--grad);opacity:.6}
.module-card:hover{border-color:var(--accent);transform:translateY(-2px);box-shadow:0 14px 35px rgba(99,102,241,.18)}
.module-card.expanded{grid-column:span 2;cursor:default;border-color:var(--accent);box-shadow:0 14px 40px rgba(99,102,241,.25)}
@media(max-width:900px){.module-card.expanded{grid-column:span 1}}
.module-header{padding:18px 20px;display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
.module-title{font-size:1rem;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px}
.module-title .icon{font-size:1.4rem;line-height:1}
.module-status{font-size:.65rem;font-weight:700;padding:3px 8px;border-radius:99px;text-transform:uppercase;letter-spacing:.5px}
.status-green{background:rgba(34,197,94,.15);color:var(--green)}
.status-amber{background:rgba(245,158,11,.15);color:var(--amber)}
.status-red{background:rgba(239,68,68,.15);color:var(--red)}
.status-grey{background:rgba(148,163,184,.15);color:var(--muted)}
.module-body{padding:0 20px 16px}
.kpi-row{display:flex;gap:14px;margin-bottom:10px;flex-wrap:wrap}
.kpi-mini{flex:1;min-width:90px;background:var(--surface2);border-radius:8px;padding:8px 10px}
.kpi-mini .lbl{font-size:.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;font-weight:600}
.kpi-mini .val{font-size:1.2rem;font-weight:700;color:var(--text);line-height:1.2;font-variant-numeric:tabular-nums}
.expand-hint{font-size:.7rem;color:var(--muted);text-align:right;padding:8px 16px;border-top:1px solid var(--border);background:var(--surface2);font-weight:500}
.expand-hint::before{content:'▾ ';color:var(--accent)}
.expanded .expand-hint::before{content:'▴ '}
.drill-content{display:none;padding:16px 20px 20px;border-top:1px solid var(--border);background:#0c0c12;border-radius:0 0 14px 14px}
.expanded .drill-content{display:block;animation:slideIn .3s ease}
@keyframes slideIn{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
.drill-section-title{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;color:var(--muted);margin:14px 0 8px;display:flex;align-items:center;gap:6px}
.drill-section-title::after{content:'';flex:1;height:1px;background:var(--border)}
.chart-wrap{position:relative;height:180px;margin:8px 0}
.chart-wrap-sm{position:relative;height:130px;margin:8px 0}
.dual-charts{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:700px){.dual-charts{grid-template-columns:1fr}}
.row{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px dashed var(--border);font-size:.78rem}
.row:last-child{border-bottom:none}
.row .lbl{color:var(--muted)}
.row .v{color:var(--text);font-weight:600;font-variant-numeric:tabular-nums}
.module-actions{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
.btn-mini{padding:5px 10px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--muted);font-size:.7rem;cursor:pointer;font-weight:600;transition:.2s;text-decoration:none}
.btn-mini:hover{background:var(--accent);color:#fff;border-color:var(--accent)}
footer{padding:32px 28px;text-align:center;color:var(--muted);font-size:.75rem;border-top:1px solid var(--border);margin-top:32px}
footer a{color:var(--accent);text-decoration:none;margin:0 8px}
.refresh{padding:5px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:99px;color:var(--muted);font-size:.7rem;cursor:pointer}
.refresh:hover{border-color:var(--accent);color:var(--text)}
</style>
</head>
<body>
<div class="topbar">
<h1>📊 WEVAL · Visual Mgmt Drillable · 9 modules</h1>
<div class="meta">
<span>Source: <a href="/api/wevia-kpi-feeders.php" style="color:var(--cyan)">kpi-feeders</a> + <a href="/api/wevia-truth-registry.json" style="color:var(--cyan)">truth-registry</a></span>
<span>Refresh: <span id="ts"></span></span>
<button class="refresh" onclick="loadAll()"></button>
<span class="live">LIVE</span>
</div>
</div>
<div class="bread">
<a href="/">Home</a> /
<a href="/weval-technology-platform.html">WTP (point d'entrée)</a> /
<a href="/wevia-erp-v2.html">ERP V2</a> /
<span style="color:var(--text)">Visual Mgmt Drillable</span>
</div>
<main>
<div class="hero">
<h2>📊 Management Visuel Drillable · 9 Modules ERP</h2>
<p>Click sur module pour drill-down · graphiques Chart.js premium · IDs uniques par module · auto-refresh 60s · zero hardcode (live data) · zero écrasement (nouvelle page complémentaire) · doctrine 60 UX premium · TOUT graphique pas que tableaux</p>
</div>
<div class="modules-grid" id="modules-grid">
<!-- 9 cards rendered by JS, each with id="dd-<module>" -->
</div>
</main>
<footer>
WEVAL Visual Mgmt Drillable · v1.0 · Chart.js 4.4.0 · 9 modules · IDs uniques dd-* ·
<a href="/weval-technology-platform.html">WTP</a> ·
<a href="/wevia-erp-v2.html">ERP V2</a> ·
<a href="/wevia-erp-unified.html">ERP V1</a> ·
<a href="/visual-management.html">Visual Mgmt v1</a> ·
<a href="/api/wevia-kpi-feeders.php">API KPI Feeders</a>
</footer>
<script>
Chart.defaults.color = '#94a3b8';
Chart.defaults.borderColor = '#2a2a35';
Chart.defaults.font.family = '-apple-system,Inter,sans-serif';
Chart.defaults.font.size = 10;
const COLORS = ['#6366f1','#a855f7','#ec4899','#22d3ee','#22c55e','#f59e0b','#ef4444','#8b5cf6','#06b6d4'];
let DATA = null;
let CHARTS = {};
const MODULES = [
{id: 'finance', icon: '💰', label: 'Finance', kpis: ['revenue_eur','costs_eur','margin_pct','cash_runway_months']},
{id: 'sales', icon: '📞', label: 'Sales', kpis: ['companies','contacts_b2b','pipeline_deals','pipeline_value']},
{id: 'supply', icon: '📦', label: 'Supply (Office Acc)', kpis: ['office_accounts_total','office_accounts_active','office_accounts_suspended','health_pct']},
{id: 'manufacturing', icon: '🏭', label: 'Manufacturing', kpis: []},
{id: 'rd', icon: '🧠', label: 'R&D', kpis: ['agents','intents','brains','doctrines','autonomy_score']},
{id: 'hr', icon: '👥', label: 'HR', kpis: ['consultants','candidates']},
{id: 'marketing', icon: '🏥', label: 'Marketing (Ethica)', kpis: ['ethica_hcps_total','ethica_dz','ethica_ma','ethica_tn']},
{id: 'it', icon: '🖥', label: 'IT', kpis: ['machines_online','gpu_providers','docker_containers','systemd_services','fpm_workers']},
{id: 'quality', icon: '🏆', label: 'Quality', kpis: ['nonreg_pass','nonreg_total','l99_pass','l99_total','seven_sigma_pass','seven_sigma_total','dpmo','sigma_level']},
];
function fmt(n) {
if (n === null || n === undefined) return '—';
if (typeof n !== 'number') return n;
if (n >= 1000000) return (n/1000000).toFixed(1)+'M';
if (n >= 1000) return (n/1000).toFixed(1)+'k';
return n.toLocaleString('fr-FR');
}
function statusOf(modKey, modData) {
if (modKey === 'manufacturing') return 'grey';
if (modKey === 'finance') return (modData.revenue_eur || 0) > 0 ? 'green' : 'amber';
if (modKey === 'quality') {
const ok = (modData.nonreg_pass === modData.nonreg_total) && (modData.l99_pass === modData.l99_total);
return ok ? 'green' : 'red';
}
if (modKey === 'rd') return (modData.autonomy_score || 0) >= 80 ? 'green' : 'amber';
return 'green';
}
async function loadAll() {
document.getElementById('ts').textContent = new Date().toLocaleTimeString('fr-FR');
try {
const r = await fetch('/api/wevia-kpi-feeders.php');
DATA = await r.json();
renderModules();
} catch (e) {
console.error(e);
document.getElementById('modules-grid').innerHTML = '<div style="text-align:center;color:var(--muted);padding:40px">Erreur fetch KPI feeders: ' + e.message + '</div>';
}
}
function renderModules() {
if (!DATA) return;
const modules = DATA.modules || {};
// Map module keys (capitalized in API) to lowercase ids
const modKeyMap = {
finance: 'Finance', sales: 'Sales', supply: 'Supply', manufacturing: 'Manufacturing',
rd: 'RD', hr: 'HR', marketing: 'Marketing', it: 'IT', quality: 'Quality'
};
const grid = document.getElementById('modules-grid');
const wasExpanded = {};
document.querySelectorAll('.module-card.expanded').forEach(el => wasExpanded[el.id] = true);
grid.innerHTML = MODULES.map(m => {
const apiKey = modKeyMap[m.id] || m.label;
const modData = modules[apiKey] || {};
const status = statusOf(m.id, modData);
// Top 4 KPIs in summary
const summaryKpis = m.kpis.slice(0, 4).map(k => {
const v = modData[k];
return `<div class="kpi-mini"><div class="lbl">${k.replace(/_/g,' ')}</div><div class="val">${fmt(v)}</div></div>`;
}).join('');
return `
<div class="module-card" id="dd-${m.id}" onclick="toggleExpand('dd-${m.id}')">
<div class="module-header">
<div class="module-title"><span class="icon">${m.icon}</span> ${m.label}</div>
<span class="module-status status-${status}">${status === 'green' ? '✓ OK' : status === 'amber' ? '⚠ TBD' : status === 'red' ? '✗ ERR' : 'N/A'}</span>
</div>
${summaryKpis ? `<div class="module-body"><div class="kpi-row">${summaryKpis}</div></div>` : `<div class="module-body"><div style="color:var(--muted);font-size:.78rem;padding:6px 0">${modData._note || 'Pas de données'}</div></div>`}
<div class="drill-content" id="dd-${m.id}-drill">
<div class="drill-section-title">📊 Détails graphiques</div>
<div class="dual-charts">
<div><div class="chart-wrap-sm"><canvas id="dd-${m.id}-chart-1"></canvas></div></div>
<div><div class="chart-wrap-sm"><canvas id="dd-${m.id}-chart-2"></canvas></div></div>
</div>
<div class="drill-section-title">🔢 Tous les KPIs</div>
${m.kpis.map(k => `<div class="row"><span class="lbl">${k.replace(/_/g,' ')}</span><span class="v">${fmt(modData[k])}</span></div>`).join('') || '<div style="color:var(--muted);font-size:.75rem">Aucun KPI</div>'}
${modData._source ? `<div class="row"><span class="lbl">source</span><span class="v" style="font-size:.7rem">${modData._source}</span></div>` : ''}
<div class="module-actions">
<a class="btn-mini" href="/api/wevia-kpi-feeders.php" target="_blank">📡 API JSON</a>
<a class="btn-mini" href="/wevia-erp-v2.html#${m.id}" target="_blank">📈 ERP V2</a>
<a class="btn-mini" href="/wevia-master.html?q=${encodeURIComponent(m.label.toLowerCase())}" target="_blank">💬 Ask WEVIA</a>
</div>
</div>
<div class="expand-hint">Click pour drill-down</div>
</div>`;
}).join('');
// Re-expand previously expanded
Object.keys(wasExpanded).forEach(id => {
const el = document.getElementById(id);
if (el) { el.classList.add('expanded'); renderCharts(id.replace('dd-','')); }
});
}
function toggleExpand(cardId) {
const card = document.getElementById(cardId);
if (!card) return;
const wasExpanded = card.classList.contains('expanded');
card.classList.toggle('expanded');
if (!wasExpanded) {
renderCharts(cardId.replace('dd-',''));
} else {
// Destroy charts to free memory
['chart-1','chart-2'].forEach(s => {
const k = `dd-${cardId.replace('dd-','')}-${s}`;
if (CHARTS[k]) { CHARTS[k].destroy(); delete CHARTS[k]; }
});
}
}
function renderCharts(modId) {
const apiKey = {finance:'Finance',sales:'Sales',supply:'Supply',manufacturing:'Manufacturing',rd:'RD',hr:'HR',marketing:'Marketing',it:'IT',quality:'Quality'}[modId];
const modData = (DATA?.modules || {})[apiKey] || {};
const m = MODULES.find(x => x.id === modId);
if (!m) return;
setTimeout(() => { // wait for canvas to mount
// Chart 1 : bar of all numeric KPIs
const numericKpis = m.kpis.filter(k => typeof modData[k] === 'number');
if (numericKpis.length > 0) {
const c1 = document.getElementById(`dd-${modId}-chart-1`);
if (c1) {
const k1 = `dd-${modId}-chart-1`;
if (CHARTS[k1]) CHARTS[k1].destroy();
CHARTS[k1] = new Chart(c1, {
type: 'bar',
data: {
labels: numericKpis.map(k => k.replace(/_/g,' ').slice(0,15)),
datasets: [{
label: 'Value',
data: numericKpis.map(k => modData[k]),
backgroundColor: COLORS.slice(0, numericKpis.length),
borderRadius: 5,
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: {display: false}, title: {display:true, text: 'KPIs Bar', font:{size:10}}},
scales: {
y: { beginAtZero: true, type: numericKpis.some(k => modData[k] > 100000) ? 'logarithmic' : 'linear', grid:{color:'#1a1a24'}, ticks:{font:{size:9}}},
x: { grid: {display: false}, ticks:{font:{size:9},maxRotation:35}}
}
}
});
}
}
// Chart 2 : donut of distribution if applicable
const c2 = document.getElementById(`dd-${modId}-chart-2`);
if (c2 && numericKpis.length > 1) {
const k2 = `dd-${modId}-chart-2`;
if (CHARTS[k2]) CHARTS[k2].destroy();
// For specific modules, custom donut data
let labels, data;
if (modId === 'marketing') {
labels = ['DZ','MA','TN','INTL'];
data = [modData.ethica_dz||0, modData.ethica_ma||0, modData.ethica_tn||0, (modData.ethica_hcps_total||0) - (modData.ethica_dz||0) - (modData.ethica_ma||0) - (modData.ethica_tn||0)];
} else if (modId === 'supply') {
labels = ['Active','Suspended','Other'];
data = [modData.office_accounts_active||0, modData.office_accounts_suspended||0, (modData.office_accounts_total||0) - (modData.office_accounts_active||0) - (modData.office_accounts_suspended||0)];
} else if (modId === 'quality') {
labels = ['NonReg','L99','7σ'];
data = [modData.nonreg_pass||0, modData.l99_pass||0, modData.seven_sigma_pass||0];
} else {
labels = numericKpis.slice(0,5);
data = labels.map(k => modData[k]||0);
}
CHARTS[k2] = new Chart(c2, {
type: 'doughnut',
data: {
labels,
datasets: [{
data,
backgroundColor: COLORS.slice(0, labels.length),
borderWidth: 1, borderColor: '#0c0c12'
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: {
legend: {position: 'right', labels: {boxWidth: 9, padding: 6, font:{size: 9}}},
title: {display:true, text:'Distribution', font:{size:10}}
}
}
});
}
}, 50);
}
loadAll();
setInterval(loadAll, 60000);
</script>
<!-- === OPUS UNIVERSAL DRILL-DOWN v1 19avr — append-only, doctrine #14 === -->
<script>
(function(){
if (window.__opusUniversalDrill) return; window.__opusUniversalDrill = true;
var d = document;
var m = d.createElement('div');
m.id = 'opus-udrill';
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:99995;padding:20px;cursor:pointer';
var inner = d.createElement('div');
inner.id = 'opus-udrill-in';
inner.style.cssText = 'max-width:900px;width:100%;max-height:90vh;overflow:auto;background:#0b0d15;border:1px solid rgba(99,102,241,0.35);border-radius:14px;padding:28px;cursor:default;box-shadow:0 20px 60px rgba(0,0,0,0.6);color:#e2e8f0;font:14px/1.55 Inter,system-ui,sans-serif';
inner.addEventListener('click', function(e){ e.stopPropagation(); });
m.appendChild(inner);
m.addEventListener('click', function(){ m.style.display='none'; });
d.addEventListener('keydown', function(e){ if(e.key==='Escape') m.style.display='none'; });
(d.body || d.documentElement).appendChild(m);
function openCard(card) {
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:14px"><button id="opus-udrill-close" style="padding:6px 14px;background:#171b2a;border:1px solid rgba(99,102,241,0.25);color:#e2e8f0;border-radius:8px;cursor:pointer;font-size:12px">✕ Fermer (Esc)</button></div>';
html += '<div style="transform-origin:top left;font-size:1.05em">' + card.outerHTML + '</div>';
inner.innerHTML = html;
d.getElementById('opus-udrill-close').onclick = function(){ m.style.display='none'; };
m.style.display = 'flex';
}
function wire(root) {
var sels = '.card,[class*="card"],.kpi,[class*="kpi"],.stat,[class*="stat"],.tile,[class*="tile"],.metric,[class*="metric"],.widget,[class*="widget"]';
var cards = root.querySelectorAll(sels);
for (var i = 0; i < cards.length; i++) {
var c = cards[i];
if (c.__opusWired) continue;
if (c.closest('button, a, input, select, textarea, #opus-udrill')) continue;
var r = c.getBoundingClientRect();
if (r.width < 60 || r.height < 40) continue;
c.__opusWired = true;
c.style.cursor = 'pointer';
c.setAttribute('role','button');
c.setAttribute('tabindex','0');
c.addEventListener('click', function(ev){
if (ev.target.closest('[data-pp-id]') && window.__opusDrillInit) return;
if (ev.target.closest('a,button,input,select')) return;
ev.preventDefault(); ev.stopPropagation();
openCard(this);
});
c.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();openCard(this);} });
}
}
var initRun = function(){ wire(d.body || d.documentElement); };
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', initRun);
else initRun();
var mo = new MutationObserver(function(muts){
var newCard = false;
for (var i=0;i<muts.length;i++) if (muts[i].addedNodes.length) { newCard = true; break; }
if (newCard) initRun();
});
mo.observe(d.body || d.documentElement, {childList:true, subtree:true});
})();
</script>
<!-- === OPUS UNIVERSAL DRILL-DOWN END === -->
<script src="/api/a11y-auto-enhancer.js" defer></script>
<!-- WTP_UDOCK_V1 (Opus 21-avr t33b6) --><script src="/wtp-unified-dock.js" defer></script>
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
</body>
</html>