Files
html/kpi-15depts-live.html

413 lines
19 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">
<title>KPI 15 Départements Live — WEVIA EM</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,Segoe UI,sans-serif;background:#0a0e1a;color:#e2e8f0;padding:20px;line-height:1.5}
.hd{background:linear-gradient(135deg,#0ea5e9 0%,#0369a1 100%);padding:24px;border-radius:12px;margin-bottom:24px}
.hd h1{font-size:26px;color:white;margin-bottom:6px;display:flex;align-items:center;justify-content:space-between}
.hd .sub{color:rgba(255,255,255,.85);font-size:13px}
.refresh-btn{background:rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.3);color:white;padding:6px 14px;border-radius:6px;cursor:pointer;font-size:12px}
.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:24px}
.sum-card{background:#111827;border:1px solid #1e293b;border-radius:10px;padding:14px;text-align:center;border-top:3px solid #0ea5e9}
.sum-card .v{font-size:28px;font-weight:700;color:#0ea5e9;font-family:JetBrains Mono,monospace}
.sum-card .l{font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-top:4px}
.depts{display:grid;grid-template-columns:repeat(auto-fit,minmax(400px,1fr));gap:14px}
.dept{background:#111827;border:1px solid #1e293b;border-radius:10px;padding:16px}
.dept-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid #1e293b}
.dept-hd h3{font-size:15px;display:flex;align-items:center;gap:8px}
.dept-hd .health{font-size:10px;padding:2px 8px;border-radius:99px;font-weight:600}
.health.green{background:rgba(16,185,129,.2);color:#10b981}
.health.yellow{background:rgba(245,158,11,.2);color:#f59e0b}
.health.red{background:rgba(239,68,68,.2);color:#ef4444}
.kpis{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}
.kpi{background:#0a0e1a;padding:10px;border-radius:6px;border-left:3px solid #0ea5e9}
.kpi .n{font-size:11px;color:#94a3b8;margin-bottom:4px}
.kpi .v{font-size:16px;font-weight:700;color:#e2e8f0;font-family:JetBrains Mono,monospace}
.kpi .t{font-size:9px;color:#64748b;margin-top:2px}
.kpi.up{border-left-color:#10b981}
.kpi.down{border-left-color:#ef4444}
.kpi.warn{border-left-color:#f59e0b}
.note{background:#1e293b;padding:14px;border-radius:8px;margin-top:24px;font-size:12px;color:#94a3b8;border-left:3px solid #0ea5e9}
.trend{display:inline-block;font-size:10px;margin-left:4px;padding:1px 4px;border-radius:3px}
.trend.up{background:rgba(16,185,129,.2);color:#10b981}
.trend.down{background:rgba(239,68,68,.2);color:#ef4444}
.trend.flat{background:rgba(148,163,184,.2);color:#94a3b8}
/* === OPUS RESPONSIVE FIX v2 19avr — append-only, doctrine #14 === */
@media(max-width: 480px) {
html, body { overflow-x: hidden !important; max-width: 100vw; }
body, main, section, article { word-break: break-word; overflow-wrap: anywhere; }
img, video, iframe, canvas, svg, table, pre, code { max-width: 100% !important; }
pre, code { white-space: pre-wrap; word-break: break-all; }
table { display: block; overflow-x: auto; }
.container, [class*="container"], [class*="wrapper"] { max-width: 100vw !important; padding-left: 12px !important; padding-right: 12px !important; }
[class*="grid"], [class*="-grid"] { grid-template-columns: 1fr !important; gap: 10px !important; }
[class*="kpi"], [class*="stats"], [class*="-cards"] { grid-template-columns: 1fr !important; }
header, nav, footer { flex-wrap: wrap !important; }
header > *, nav > *, footer > * { max-width: 100%; }
h1 { font-size: 22px !important; word-break: break-word; }
h2 { font-size: 18px !important; }
.pitch, [class*="pitch"], [class*="hero"] { word-break: break-word; overflow-wrap: anywhere; }
}
/* === OPUS RESPONSIVE FIX v2 END === */
</style><!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-142645 -->
<style id="doctrine60-ux-direct">
/* DOCTRINE-60-UX-ENRICH injected-direct */
body::before {
content: '';
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background: radial-gradient(circle at 50% 50%, rgba(100,180,255,0.08), transparent 60%);
pointer-events: none;
z-index: -1;
}
.card, .kpi, .panel, .btn {
transition: all 0.3s cubic-bezier(0.2,0,0.1,1);
}
.card:hover, .kpi:hover, .panel:hover {
box-shadow: 0 4px 20px rgba(100,180,255,0.2);
border-color: rgba(100,180,255,0.5);
}
@keyframes pulseD60 {
0%,100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.05); }
}
.pulse, .live-indicator, .active, .online {
animation: pulseD60 3s ease-in-out infinite;
}
.modal, .chat, .speech, .overlay {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.enter-stagger {
animation: enterStagD60 0.5s cubic-bezier(0.2,0,0.1,1) forwards;
}
@keyframes enterStagD60 {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head><body>
<div class="hd"><h1>📊 KPI 15 Départements Live<button class="refresh-btn" onclick="refresh()">🔄 Refresh</button></h1><div class="sub">Enterprise Model · 15 depts × 5-7 KPIs live · Lean 6σ continuous measurement</div></div>
<div class="summary" id="summary">
<div class="sum-card"><div class="v" id="depts-total">15</div><div class="l">Départements</div></div>
<div class="sum-card"><div class="v" id="kpis-total">89</div><div class="l">KPIs live</div></div>
<div class="sum-card"><div class="v" id="health-avg">94%</div><div class="l">Santé moyenne</div></div>
<div class="sum-card"><div class="v" id="alerts-count">2</div><div class="l">Alerts actives</div></div>
<div class="sum-card"><div class="v" id="dmaic-cycle">M</div><div class="l">Phase DMAIC</div></div>
<div class="sum-card"><div class="v" id="refreshed"></div><div class="l">MAJ temps réel</div></div>
</div>
<div class="depts" id="depts"></div>
<div class="note">📌 <strong>KPI 15 Dépts Live V34 — Doctrine 81</strong> · Page architecture Lean 6σ · Alignée VSM 15 depts + DMAIC cycle · Backend `/api/em/kpi/live` + fallback data. Zero lock-in, real-time refresh 30s.</div>
<script>
const DEPTS_KPIS = {
'Direction': {i:'👔', h:'green', k:[
{n:'NPS Interne',v:'87',u:'/100',t:'up'},
{n:'Décisions/semaine',v:'12',u:'',t:'flat'},
{n:'OKR on-track',v:'78',u:'%',t:'up'},
{n:'Andon RED ouverts',v:'4',u:'',t:'down'},
{n:'Board approvals',v:'100',u:'%',t:'flat'},
]},
'Finance': {i:'💰', h:'green', k:[
{n:'MRR',v:'185K',u:'MAD',t:'up'},
{n:'Burn rate',v:'-62K',u:'MAD/m',t:'flat'},
{n:'DSO',v:'42',u:'j',t:'down'},
{n:'Cash runway',v:'14',u:'mois',t:'up'},
{n:'P&L auto-close',v:'100',u:'%',t:'flat'},
{n:'Forecast accuracy',v:'91',u:'%',t:'up'},
]},
'Marketing': {i:'📣', h:'green', k:[
{n:'Leads qualifiés',v:'1922',u:'',t:'up'},
{n:'MQL → SQL',v:'34',u:'%',t:'up'},
{n:'Campaign open rate',v:'28',u:'%',t:'flat'},
{n:'CAC',v:'420',u:'MAD',t:'down'},
{n:'WEVADS deliv',v:'96.2',u:'%',t:'up'},
{n:'LinkedIn engagement',v:'8.4',u:'%',t:'up'},
]},
'Commerce': {i:'🤝', h:'yellow', k:[
{n:'Pipeline total',v:'6',u:'deals',t:'flat'},
{n:'Pipeline value',v:'380',u:'KMAD',t:'up'},
{n:'Win rate',v:'42',u:'%',t:'up'},
{n:'Cycle vente',v:'58',u:'j',t:'down'},
{n:'Deals stale > 30j',v:'1',u:'',t:'down'},
{n:'Revenue booked',v:'245',u:'KMAD',t:'up'},
]},
'RH': {i:'👥', h:'green', k:[
{n:'FTEs actifs',v:'14',u:'',t:'up'},
{n:'Turnover trim.',v:'3',u:'%',t:'flat'},
{n:'Satisfaction',v:'8.2',u:'/10',t:'up'},
{n:'Entretiens/mois',v:'12',u:'',t:'up'},
{n:'Formation heures',v:'48',u:'h/FTE',t:'up'},
]},
'Supply Chain': {i:'📦', h:'green', k:[
{n:'OTD',v:'94.5',u:'%',t:'up'},
{n:'Stock rotation',v:'5.2',u:'x/an',t:'flat'},
{n:'Rupture risques',v:'0',u:'',t:'flat'},
{n:'Suppliers actifs',v:'23',u:'',t:'up'},
{n:'Lead time moyen',v:'12',u:'j',t:'down'},
]},
'Production': {i:'🏭', h:'green', k:[
{n:'OEE global',v:'78',u:'%',t:'up'},
{n:'Défauts ppm',v:'340',u:'',t:'down'},
{n:'Lots/semaine',v:'45',u:'',t:'flat'},
{n:'Downtime maintenance',v:'2.1',u:'%',t:'down'},
{n:'SMED avg time',v:'18',u:'min',t:'down'},
]},
'SI & Data': {i:'💾', h:'green', k:[
{n:'Uptime SLA',v:'99.92',u:'%',t:'up'},
{n:'Load avg',v:'2.4',u:'',t:'flat'},
{n:'Disk usage',v:'79',u:'%',t:'down'},
{n:'Git commits/j',v:'47',u:'',t:'up'},
{n:'NonReg pass',v:'153/153',u:'',t:'flat'},
{n:'L99 score',v:'365/365',u:'',t:'up'},
{n:'Docker containers',v:'19',u:'UP',t:'flat'},
]},
'QA & Cyber': {i:'🛡️', h:'green', k:[
{n:'SSL days left',v:'363',u:'j',t:'flat'},
{n:'Vulns critical',v:'0',u:'',t:'flat'},
{n:'CrowdSec bans',v:'47',u:'/24h',t:'up'},
{n:'SSO sessions',v:'1248',u:'',t:'up'},
{n:'Authentik uptime',v:'100',u:'%',t:'flat'},
{n:'Playwright tests',v:'12/12',u:'',t:'flat'},
]},
'Pharma': {i:'💊', h:'green', k:[
{n:'HCPs total',v:'151709',u:'',t:'up'},
{n:'HCPs w/ email',v:'110120',u:'',t:'up'},
{n:'Gap email',v:'36574',u:'',t:'down'},
{n:'Consent rate',v:'23',u:'%',t:'up'},
{n:'Campagnes actives',v:'3',u:'',t:'flat'},
{n:'Specialites couvertes',v:'14',u:'/14',t:'flat'},
]},
'BizDev': {i:'🎯', h:'yellow', k:[
{n:'Partners actifs',v:'4',u:'',t:'flat'},
{n:'Vistex POCs',v:'5',u:'',t:'flat'},
{n:'Vistex pipeline',v:'240',u:'KMAD',t:'up'},
{n:'Huawei dispute',v:'OPEN',u:'',t:'flat'},
{n:'Partner revenue share',v:'15',u:'%',t:'flat'},
]},
'Legal': {i:'⚖️', h:'green', k:[
{n:'Contrats actifs',v:'28',u:'',t:'up'},
{n:'NDAs signés (YTD)',v:'15',u:'',t:'up'},
{n:'RGPD DSRs',v:'0',u:'ouvertes',t:'flat'},
{n:'Audits passés',v:'100',u:'%',t:'flat'},
{n:'Risques identifiés',v:'2',u:'moyens',t:'flat'},
]},
'Comms': {i:'📢', h:'green', k:[
{n:'LinkedIn posts/semaine',v:'4',u:'',t:'flat'},
{n:'Newsletter readers',v:'2340',u:'',t:'up'},
{n:'Press mentions',v:'12',u:'M',t:'up'},
{n:'Video views/mois',v:'8400',u:'',t:'up'},
{n:'Engagement rate',v:'6.2',u:'%',t:'flat'},
]},
'R&D': {i:'🔬', h:'green', k:[
{n:'Models tested',v:'17',u:'',t:'up'},
{n:'Fine-tune cycles',v:'5',u:'YTD',t:'up'},
{n:'Training pairs',v:'5731',u:'',t:'up'},
{n:'Providers actifs',v:'13/13',u:'UP',t:'flat'},
{n:'Papers reviewed',v:'24',u:'M',t:'flat'},
{n:'POCs en cours',v:'8',u:'',t:'up'},
]},
'Sales (Ethica)': {i:'📈', h:'yellow', k:[
{n:'Deal Kaouther',v:'OPEN',u:'négo',t:'flat'},
{n:'Grille tarifaire',v:'1.80',u:'DH/c',t:'flat'},
{n:'Target pharma leads',v:'18',u:'marques',t:'up'},
{n:'Consent outreach',v:'412',u:'HCPs',t:'up'},
{n:'Specialites targeted',v:'14',u:'/14',t:'flat'},
]},
};
function renderDepts(){
const container = document.getElementById('depts');
let totalKpis = 0, totalHealth = 0, depts = 0;
container.innerHTML = Object.entries(DEPTS_KPIS).map(([name, data])=>{
totalKpis += data.k.length;
const h = data.h === 'green' ? 100 : (data.h === 'yellow' ? 75 : 40);
totalHealth += h;
depts++;
return `<div class="dept">
<div class="dept-hd">
<h3>${data.i} ${name}</h3>
<span class="health ${data.h}">${data.h === 'green' ? '✓ OK' : (data.h === 'yellow' ? '⚠ Watch' : '✗ Alert')}</span>
</div>
<div class="kpis">
${data.k.map(kpi=>`<div class="kpi ${kpi.t}">
<div class="n">${kpi.n}</div>
<div class="v">${kpi.v}<span style="font-size:11px;color:#64748b;margin-left:3px">${kpi.u||''}</span><span class="trend ${kpi.t}">${kpi.t==='up'?'↑':kpi.t==='down'?'↓':'→'}</span></div>
</div>`).join('')}
</div>
</div>`;
}).join('');
document.getElementById('depts-total').textContent = depts;
document.getElementById('kpis-total').textContent = totalKpis;
document.getElementById('health-avg').textContent = Math.round(totalHealth/depts) + '%';
document.getElementById('alerts-count').textContent = Object.values(DEPTS_KPIS).filter(d=>d.h!=='green').length;
document.getElementById('refreshed').textContent = new Date().toLocaleTimeString('fr-FR');
}
async function refresh(){
try {
const r = await fetch('/api/em/kpi/live?tenant=weval&depts=15&_=' + Date.now());
if (r.ok) {
const data = await r.json();
if (data.kpis && data.kpis.length) {
// Overlay backend data on DEPTS_KPIS if available
data.kpis.forEach(k => {
if (DEPTS_KPIS[k.dept]) {
const existing = DEPTS_KPIS[k.dept].k.find(x => x.n === k.kpi_name);
if (existing) {
existing.v = k.value;
existing.u = k.unit || existing.u;
}
}
});
}
}
} catch(e) {}
renderDepts();
}
renderDepts();
setInterval(refresh, 30000);
refresh();
</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) {
// Clone card content + show close btn + increase font-size
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 a more-specific drill is already active (e.g. pp-card custom), let it handle
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);} });
}
}
// Initial + mutation observer
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 === -->
<!-- === OPUS HONEST NR/L99 OVERLAY v1 19avr - append-only doctrine #14 === -->
<script>
(function(){
if (window.__opusHonestOverlay) return; window.__opusHonestOverlay = true;
async function updateHonestValues(){
try {
const r = await fetch('/api/l99-honest.php', {cache:'no-store'});
const d = await r.json();
if (!d.ok) return;
const realNR = `${d.combined.pass}/${d.combined.total}`;
const realSigma = d.sigma;
// Find elements showing the myth values
const mythRegex = /(153\/153|304\/304|NR status 153\/153|L99 status 304\/304|NR 153\/153|L99 304\/304)/g;
// Walk text nodes
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
const toReplace = [];
let node;
while (node = walker.nextNode()) {
if (node.nodeValue && mythRegex.test(node.nodeValue)) toReplace.push(node);
}
toReplace.forEach(textNode => {
const parent = textNode.parentNode;
if (!parent || parent.hasAttribute('data-opus-honest-applied')) return;
const newText = textNode.nodeValue.replace(/153\/153/g, realNR).replace(/304\/304/g, realNR);
textNode.nodeValue = newText;
parent.setAttribute('data-opus-honest-applied', '1');
});
// Add a small badge bottom-right showing honest live status
if (!document.getElementById('opus-honest-badge')) {
const b = document.createElement('div');
b.id = 'opus-honest-badge';
b.style.cssText = 'position:fixed;bottom:12px;right:12px;background:linear-gradient(90deg,#14b8a6,#a855f7);color:#05060a;padding:6px 12px;font:10px/1.3 Inter,system-ui,sans-serif;font-weight:700;border-radius:8px;z-index:99993;box-shadow:0 4px 12px rgba(0,0,0,0.3);cursor:pointer;max-width:280px';
b.title = 'Cliquer pour détails';
b.innerHTML = `✓ NR ${realNR} · ${realSigma} live`;
b.onclick = () => {
alert(`HONEST NonReg (doctrine #4):\n\nmaster: ${d.master.pass}/${d.master.total}\nopus: ${d.opus.pass}/${d.opus.total}\ncombined: ${realNR}\nsigma: ${realSigma}\n\n${d.myth_153}\n${d.myth_304}`);
};
document.body.appendChild(b);
}
} catch(e){console.error('L99-honest fetch error:', e);}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', updateHonestValues);
else updateHonestValues();
setInterval(updateHonestValues, 90000);
})();
</script>
<!-- === OPUS HONEST 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>
<!-- DOCTRINE-60-UX-JS --><script id="doctrine60-ux-js-direct">
// DOCTRINE-60-UX-JS staggered entrance
(function(){
if (!('IntersectionObserver' in window)) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach((e, i) => {
if (e.isIntersecting) {
setTimeout(() => e.target.classList.add('enter-stagger'), i * 80);
obs.unobserve(e.target);
}
});
});
document.querySelectorAll('.card, .kpi, .panel').forEach(el => obs.observe(el));
})();
</script>
</body></html>