Files
html/visual-management.html
2026-04-19 21:20:03 +02:00

378 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">
<title>Visual Management · WEVAL Consulting</title>
<style>
:root {
--bg:#0a0e27; --panel:#141933; --border:#263161; --text:#e4e8f7; --muted:#9ca8d3;
--green:#10b981; --amber:#f59e0b; --red:#ef4444; --blue:#6ba3ff; --purple:#c084fc;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); padding: 20px; min-height: 100vh; line-height: 1.4; }
.header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 16px; border-bottom: 2px solid #1e3a8a; margin-bottom: 20px; }
.header h1 { color: var(--blue); font-size: 26px; }
.header .sub { color: var(--muted); font-size: 12px; margin-top: 4px; }
.health-badge { padding: 14px 28px; border-radius: 12px; font-size: 28px; font-weight: bold; text-align: center; min-width: 180px; }
.health-badge .label { font-size: 11px; display: block; text-transform: uppercase; opacity: 0.8; margin-bottom: 4px; }
.health-green { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: #fff; }
.health-amber { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #fff; }
.health-red { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); color: #fff; }
.section { margin: 24px 0; }
.section h2 { color: var(--purple); font-size: 18px; margin-bottom: 12px; padding: 8px 0; border-bottom: 1px dashed var(--border); display: flex; align-items: center; gap: 8px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.kpi { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 16px; position: relative; overflow: hidden; }
.kpi::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: var(--blue); }
.kpi.green::before { background: var(--green); }
.kpi.amber::before { background: var(--amber); }
.kpi.red::before { background: var(--red); }
.kpi .label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
.kpi .value { font-size: 28px; font-weight: bold; color: var(--text); font-family: 'SF Mono', Monaco, monospace; }
.kpi .unit { font-size: 13px; color: var(--muted); margin-left: 4px; font-weight: normal; }
.kpi .sub { font-size: 11px; color: var(--muted); margin-top: 4px; }
.andon { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--panel); border: 1px solid var(--border); border-left-width: 5px; border-radius: 6px; margin-bottom: 8px; }
.andon.RED { border-left-color: var(--red); background: linear-gradient(90deg, rgba(239,68,68,0.1) 0%, var(--panel) 100%); }
.andon.ORANGE { border-left-color: var(--amber); background: linear-gradient(90deg, rgba(245,158,11,0.1) 0%, var(--panel) 100%); }
.andon .sev { font-weight: bold; padding: 3px 10px; border-radius: 4px; font-size: 11px; }
.andon.RED .sev { background: var(--red); color: #fff; }
.andon.ORANGE .sev { background: var(--amber); color: #000; }
.andon .msg { flex: 1; margin-left: 16px; font-size: 13px; }
.andon .kpi-name { color: var(--muted); font-size: 11px; font-family: 'SF Mono', monospace; }
.footer { color: var(--muted); font-size: 11px; margin-top: 28px; text-align: right; padding-top: 16px; border-top: 1px dashed var(--border); }
.links { display: flex; gap: 12px; flex-wrap: wrap; margin: 10px 0; }
.links a { color: var(--blue); text-decoration: none; font-size: 12px; padding: 4px 10px; background: var(--panel); border-radius: 4px; border: 1px solid var(--border); }
.links a:hover { background: var(--border); }
.skeleton { display: inline-block; width: 60px; height: 24px; background: linear-gradient(90deg, var(--border), #1e2549, var(--border)); background-size: 200% 100%; animation: skel 1.5s infinite; border-radius: 4px; }
@keyframes skel { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
#toast { position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; background: var(--red); color: #fff; border-radius: 6px; display: none; z-index: 1000; font-size: 13px; }
.no-andon { color: var(--green); padding: 20px; text-align: center; font-size: 14px; background: var(--panel); border-radius: 6px; border: 1px dashed var(--green); }
</style>
</head>
<body>
<div class="header">
<div>
<h1>📊 Visual Management · WEVAL</h1>
<div class="sub">Doctrine 65 — Lean Six Sigma · Andon alerts · Auto-refresh 30s · 0 hardcode</div>
</div>
<div class="health-badge health-amber" id="health-badge">
<span class="label">Health Score</span>
<span id="health-value"></span>
</div>
</div>
<div class="links">
<a href="/dashboards-hub.html">🏠 Hub</a>
<a href="/crm-dashboard-live.html">💼 CRM</a>
<a href="/contacts-segmentation-dashboard.html">🎯 B2B/B2C</a>
<a href="/ethica-dashboard-live.html">🏥 Ethica</a>
<a href="/office-365-dashboard-live.html">📮 O365</a>
<a href="/infra-dashboard-live.html">⚙️ Infra</a>
<a href="/database-dashboard-live.html">🗄️ DB</a>
<a href="/wevia-master.html">🤖 WEVIA Chat</a>
</div>
<div class="section">
<h2>🚨 Andon Alerts</h2>
<div id="andon-list"><div class="skeleton" style="width:100%;height:60px"></div></div>
</div>
<div class="section">
<h2>💼 Business KPIs — CRM Pipeline</h2>
<div class="grid" id="kpi-business"></div>
</div>
<div class="section">
<h2>🌊 Flux KPIs — Flow Detection (doctrine 55 anti-staleness)</h2>
<div class="grid" id="kpi-flux"></div>
</div>
<div class="section">
<h2>🏥 Ethica HCPs</h2>
<div class="grid" id="kpi-ethica"></div>
</div>
<div class="section">
<h2>📮 Office 365 Email Infrastructure</h2>
<div class="grid" id="kpi-office"></div>
</div>
<div class="section">
<h2>✅ Quality — Lean Six Sigma</h2>
<div class="grid" id="kpi-quality"></div>
</div>
<div class="section">
<h2>🎯 B2B/B2C Classification (doctrine 63)</h2>
<div class="grid" id="kpi-classif"></div>
</div>
<div class="section">
<h2>⚙️ Infra Live</h2>
<div class="grid" id="kpi-infra"></div>
</div>
<div class="footer" id="footer">Connecting...</div>
<div id="toast"></div>
<script>
const fmt = n => (n == null) ? '—' : Number(n).toLocaleString('fr-FR');
const fmtPct = n => n == null ? '—' : Number(n).toFixed(1) + '%';
const fmtMoney = n => n == null ? '—' : (Math.round(n/1000) + 'k€');
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.style.display = 'block';
setTimeout(() => t.style.display = 'none', 5000);
}
function kpiCard(label, value, unit='', sub='', status='') {
return `<div class="kpi ${status}">
<div class="label">${label}</div>
<div class="value">${value}<span class="unit">${unit}</span></div>
${sub ? `<div class="sub">${sub}</div>` : ''}
</div>`;
}
async function loadData() {
try {
const r = await fetch('/api/visual-management-live.php');
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
// Health badge
const badge = document.getElementById('health-badge');
badge.className = 'health-badge health-' + (d.health_status || 'amber').toLowerCase();
document.getElementById('health-value').textContent = (d.health_score || 0) + '/100';
// Andons
const alist = document.getElementById('andon-list');
alist.innerHTML = '';
if (!d.andons || d.andons.length === 0) {
alist.innerHTML = '<div class="no-andon">✅ Aucune alerte Andon — système healthy</div>';
} else {
d.andons.forEach(a => {
alist.innerHTML += `<div class="andon ${a.severity}">
<span class="sev">${a.severity}</span>
<span class="msg">${a.message}</span>
<span class="kpi-name">${a.kpi}</span>
</div>`;
});
}
// Business
const b = d.business || {};
const statDeals = b.crm_deals < 5 ? 'red' : 'green';
document.getElementById('kpi-business').innerHTML =
kpiCard('Deals', fmt(b.crm_deals), '', `Valeur: ${fmtMoney(b.crm_deals_amount_eur)}`, statDeals) +
kpiCard('Companies', fmt(b.crm_companies), '', 'Pipeline B2B', 'green') +
kpiCard('Contacts B2B', fmt(b.crm_contacts_b2b), '', `${fmt(b.crm_contacts_linked)} linked`, 'green') +
kpiCard('Activities', fmt(b.crm_activities), '', `${fmt(b.crm_activities_last_7d)} (7j)`, b.crm_activities < 100 ? 'amber' : 'green') +
kpiCard('Leads (7.3M)', fmt(b.crm_contacts + 7354710), '', 'leads + contacts pool');
// Flux
const f = d.flux || {};
const dateLastSend = f.send_contacts_last_created ? f.send_contacts_last_created.split(' ')[0] : '—';
const dateLastGraph = f.graph_send_last_created ? f.graph_send_last_created.split(' ')[0] : '—';
document.getElementById('kpi-flux').innerHTML =
kpiCard('send_contacts 7j', fmt(f.send_contacts_last_7d), '', `Dernière: ${dateLastSend}`, f.send_contacts_last_7d === 0 ? 'red' : 'green') +
kpiCard('send_contacts 30j', fmt(f.send_contacts_last_30d), '', 'Flux legacy CRM', 'amber') +
kpiCard('graph_send 7j', fmt(f.graph_send_last_7d), '', `Dernière: ${dateLastGraph}`, f.graph_send_last_7d < 100 ? 'amber' : 'green') +
kpiCard('weval_leads 7j', fmt(f.weval_leads_last_7d), '', 'B2B scraper flux', f.weval_leads_last_7d > 100 ? 'green' : 'amber') +
kpiCard('pipeline_deals 30j', fmt(f.pipeline_deals_last_30d), '', 'Nouveaux deals', f.pipeline_deals_last_30d === 0 ? 'red' : 'green') +
kpiCard('pipeline_contacts 24h', fmt(f.pipeline_contacts_last_24h), '', 'Import récent', 'green');
// Ethica
document.getElementById('kpi-ethica').innerHTML =
kpiCard('HCPs Total', fmt(b.ethica_hcps), '', '3 pays Maghreb+INT') +
kpiCard('DZ Algérie', fmt(b.ethica_hcps_dz), '', 'Campaign ready', 'green') +
kpiCard('MA Maroc', fmt(b.ethica_hcps_ma), '', 'Active market', 'green') +
kpiCard('TN Tunisie', fmt(b.ethica_hcps_tn), '', 'Secondary market', 'green');
// Office
document.getElementById('kpi-office').innerHTML =
kpiCard('Accounts', fmt(b.office_accounts), '', '9 tenants') +
kpiCard('Actifs', fmt(b.office_active), '', 'Ready to send', 'green') +
kpiCard('Warming', fmt(b.office_warming), '', 'En préchauffe', 'amber');
// Quality
const qu = d.quality || {};
document.getElementById('kpi-quality').innerHTML =
kpiCard('NonReg', fmt(qu.nonreg_pass) + '/' + fmt(qu.nonreg_total), '', fmtPct(qu.nonreg_score) + ' score', qu.nonreg_score === 100 ? 'green' : 'red') +
kpiCard('L99', fmt(qu.l99_pass) + '/' + fmt(qu.l99_total), '', fmtPct(qu.l99_score) + ' score', qu.l99_score === 100 ? 'green' : 'red');
// Classification
const c = d.classification || {};
document.getElementById('kpi-classif').innerHTML =
kpiCard('Leads classified', fmtPct(c.leads_classified_pct), '', `B2B: ${fmt(c.leads_b2b)}`, c.leads_classified_pct === 100 ? 'green' : 'amber') +
kpiCard('Send contacts classified', fmtPct(c.send_contacts_classified_pct), '', `B2B: ${fmt(c.send_contacts_b2b)}`, c.send_contacts_classified_pct === 100 ? 'green' : 'amber');
// Infra
const i = d.infra || {};
document.getElementById('kpi-infra').innerHTML =
kpiCard('Load 1m', i.load_1m, '', `5m: ${i.load_5m}`, i.load_1m < 4 ? 'green' : 'amber') +
kpiCard('Memory', fmtPct(i.mem_pct), '', 'S204', i.mem_pct < 70 ? 'green' : 'amber') +
kpiCard('Disk', fmtPct(i.disk_pct), '', '150G total', i.disk_pct < 80 ? 'green' : 'amber') +
kpiCard('Docker', fmt(i.docker_containers), '', 'Containers UP', 'green') +
kpiCard('Uptime', fmt(i.uptime_hours), 'h', 'Since last reboot');
// Footer
document.getElementById('footer').textContent =
`Last refresh: ${new Date(d.ts).toLocaleString('fr-FR')} · Andons: ${d.andons_count || 0} · API: /api/visual-management-live.php · Auto-refresh 30s`;
} catch (e) {
showToast('Erreur: ' + e.message);
document.getElementById('footer').textContent = 'Error: ' + e.message;
}
}
loadData();
setInterval(loadData, 30000);
</script>
<!-- KPI HISTORY CHART 30J (wired 17avr by Opus doctrine 60 UX premium) -->
<div id="kpi-history-chart-30d" style="margin:2rem 1rem;padding:1.5rem;background:linear-gradient(135deg,rgba(20,25,40,0.9),rgba(30,40,60,0.85));border-radius:16px;border:1px solid rgba(100,140,200,0.2);backdrop-filter:blur(12px);">
<h3 style="color:#e0e8ff;font:600 1.1rem system-ui;margin:0 0 1rem;">Historique KPI 30 jours</h3>
<canvas id="kpiHistCanvas" style="max-height:320px;"></canvas>
<div id="kpiHistStatus" style="color:#8899bb;font:400 0.85rem system-ui;margin-top:0.5rem;"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script>
(async function(){
try {
const r = await fetch('/api/kpi-history-30d.php');
const j = await r.json();
if (!j.data || !j.data.length) { document.getElementById('kpiHistStatus').textContent = 'Snapshot quotidien actif (0 2 * * *). Données à partir de demain.'; return; }
const labels = j.data.map(d => d.snap_date);
const ds = [
{label:'CRM contacts', data: j.data.map(d=>+d.crm_contacts), borderColor:'#4a9eff', tension:0.3},
{label:'CRM companies', data: j.data.map(d=>+d.crm_companies), borderColor:'#50e090', tension:0.3},
{label:'CRM activities', data: j.data.map(d=>+d.crm_activities), borderColor:'#f0a050', tension:0.3},
{label:'Ethica HCPs', data: j.data.map(d=>+d.ethica_hcps), borderColor:'#b070ff', tension:0.3},
{label:'Office active', data: j.data.map(d=>+d.office_active), borderColor:'#ff6090', tension:0.3},
{label:'Health score', data: j.data.map(d=>+d.health_score), borderColor:'#ffc040', tension:0.3, yAxisID:'y2'}
];
new Chart(document.getElementById('kpiHistCanvas').getContext('2d'), {
type:'line',
data:{labels, datasets:ds},
options:{
responsive:true, maintainAspectRatio:false,
plugins:{legend:{labels:{color:'#c0c8d8',font:{size:11}}}},
scales:{
x:{ticks:{color:'#8899bb'},grid:{color:'rgba(100,120,160,0.1)'}},
y:{type:'logarithmic',ticks:{color:'#8899bb'},grid:{color:'rgba(100,120,160,0.1)'}},
y2:{position:'right',min:0,max:100,ticks:{color:'#ffc040'},grid:{display:false}}
}
}
});
document.getElementById('kpiHistStatus').textContent = `${j.count} points, généré ${j.generated_at}`;
} catch(e){ document.getElementById('kpiHistStatus').textContent = 'Erreur chargement: '+e.message; }
})();
</script>
<!-- END KPI HISTORY CHART 30J -->
<!-- WTP-GAP-FILL-V1 (doctrine 90-v2 gap-fill showcase, 18avr 2026) -->
<style>
.wtp-gapfill-banner{position:fixed;bottom:0;left:0;right:0;z-index:99999;background:linear-gradient(90deg,#05060a,#0b0d15 20%,#181d2e 50%,#0b0d15 80%,#05060a);border-top:2px solid #14b8a6;color:#e2e8f0;padding:10px 16px;font-family:Inter,system-ui,-apple-system,sans-serif;font-size:11.5px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;box-shadow:0 -10px 30px rgba(20,184,166,.28)}
.wtp-gapfill-banner a{color:#5eead4;text-decoration:none;font-weight:600;transition:color .15s}
.wtp-gapfill-banner a:hover{color:#22d3ee}
.wtp-gapfill-banner .pill{padding:2px 9px;background:rgba(99,102,241,.14);color:#a5b4fc;border-radius:10px;font-size:10.5px;font-family:JetBrains Mono,monospace;font-weight:600}
.wtp-gapfill-banner .pill.new{background:rgba(20,184,166,.22);color:#5eead4}
.wtp-gapfill-banner .pill.hot{background:rgba(236,72,153,.22);color:#f472b6}
.wtp-gapfill-banner .close{margin-left:auto;cursor:pointer;color:#64748b;padding:0 8px;font-size:16px;line-height:1;border:1px solid #334155;border-radius:4px}
.wtp-gapfill-banner .close:hover{color:#e2e8f0;border-color:#64748b}
.wtp-gapfill-banner.hidden{display:none}
@media(max-width:768px){.wtp-gapfill-banner{font-size:10px;padding:7px 10px;gap:8px}}
</style>
<div class="wtp-gapfill-banner" id="wtpGapFillBanner">
<span>🎯 <strong>WEVAL Agents Gap-Fill ERP</strong></span>
<span class="pill hot">45 gaps</span>
<span class="pill">SAP · Oracle · NetSuite · Dynamics</span>
<span class="pill new">🆕 Meeting Rooms</span>
<span class="pill new">🆕 Lean 6 Sigma</span>
<span id="wtp-gfb-metrics" class="pill">— chargement —</span>
<a href="/weval-technology-platform.html">→ WTP Portal (16 mod)</a>
<a href="/enterprise-model.html">Enterprise Model</a>
<a href="/api/weval-agents-gap-fill-manifest.json" target="_blank">📋 Manifest</a>
<span class="close" onclick="document.getElementById("wtpGapFillBanner").classList.add("hidden");localStorage.setItem("wtpGapFillHidden","1")">×</span>
</div>
<script>
(async()=>{
if(localStorage.getItem("wtpGapFillHidden")==="1"){document.getElementById("wtpGapFillBanner").classList.add("hidden");return;}
try{
const r=await fetch("/api/source-of-truth.json?t="+Date.now());
const d=await r.json();
const el=document.getElementById("wtp-gfb-metrics");
if(el)el.textContent=(d.ethica_total||"?")+" HCPs · "+(d.nonreg||"?")+" · "+(d.providers_count||"?")+" IA · "+(d.agents_count||"?")+" agents · "+(d.docker_running||"?")+" 🐳";
}catch(e){}
})();
</script>
<!-- VM Alerts DG Widget 18avr Opus Yacine · enriches with 7 alerts + 6 risks + 8 stats from /api/wevia-v69-dg-command-center.php -->
<div id="dsh-vm-alerts-mount"></div>
<script src="/dsh-vm-alerts-widget.js" defer></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 === -->
</body>
</html>