auto-sync-1600

This commit is contained in:
opus
2026-04-18 16:00:03 +02:00
parent 3267ef694f
commit 794645b24b
2 changed files with 151 additions and 1929 deletions

File diff suppressed because it is too large Load Diff

151
dsh-vm-alerts-widget.js Normal file
View File

@@ -0,0 +1,151 @@
/**
* DG Command Center Widget · Visual Management enrichment · 18avr2026
* Self-contained · zero external deps · safe include anywhere.
* Fetches live /api/wevia-v69-dg-command-center.php and renders:
* - 7 business alerts DG (critical/high/medium)
* - 5 structured risks (likelihood × impact)
* - Conversion funnel + pipeline value
* Pattern: glass-card premium, follows dsh-predict-widget.js
* Yacine Mahboub · WEVAL Consulting
*/
(function(){
if (window.__DSH_VM_ALERTS_LOADED__) return;
window.__DSH_VM_ALERTS_LOADED__ = true;
const API_DG = '/api/wevia-v69-dg-command-center.php';
const REFRESH_MS = 60000; // 1 min
// ── Styles (scoped .dsh-vm prefix) ──
const css = `
.dsh-vm-wrap{position:relative;margin:24px 0;font-family:-apple-system,'Segoe UI',Roboto,sans-serif;color:#e6e9f0}
.dsh-vm-card{
background:linear-gradient(135deg,rgba(20,25,40,.78),rgba(30,15,55,.6));
border:1px solid rgba(120,140,200,.25);
border-radius:14px;padding:22px;backdrop-filter:blur(12px);
box-shadow:0 10px 40px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,255,255,.05);
}
.dsh-vm-head{display:flex;align-items:center;gap:12px;margin-bottom:16px;padding-bottom:14px;border-bottom:1px dashed rgba(120,140,200,.2)}
.dsh-vm-head h3{margin:0;font-size:17px;font-weight:600;color:#c4d0ff;letter-spacing:.3px}
.dsh-vm-head .badge{padding:4px 10px;border-radius:99px;font-size:10.5px;font-weight:600;letter-spacing:.4px;text-transform:uppercase}
.dsh-vm-head .badge.live{background:rgba(16,185,129,.2);color:#6ee7b7;border:1px solid rgba(16,185,129,.4)}
.dsh-vm-head .meta{margin-left:auto;font-size:11px;color:#8b95b8;font-family:'SF Mono',Monaco,monospace}
.dsh-vm-stats{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:10px;margin-bottom:18px}
.dsh-vm-stat{background:rgba(255,255,255,.03);border:1px solid rgba(120,140,200,.15);border-radius:9px;padding:11px 13px}
.dsh-vm-stat .k{font-size:10px;color:#8b95b8;text-transform:uppercase;letter-spacing:.4px;margin-bottom:4px}
.dsh-vm-stat .v{font-size:20px;font-weight:700;color:#e6e9f0;font-family:'SF Mono',Monaco,monospace}
.dsh-vm-stat .v.crit{color:#fca5a5}
.dsh-vm-stat .v.high{color:#fbbf24}
.dsh-vm-stat .v.good{color:#6ee7b7}
.dsh-vm-alerts{display:flex;flex-direction:column;gap:9px;margin-bottom:16px}
.dsh-vm-alert{display:flex;gap:14px;align-items:flex-start;padding:13px 15px;border-radius:9px;background:rgba(255,255,255,.02);border-left:4px solid #6ba3ff;transition:all .2s}
.dsh-vm-alert:hover{background:rgba(255,255,255,.05);transform:translateX(2px)}
.dsh-vm-alert.critical{border-left-color:#ef4444;background:linear-gradient(90deg,rgba(239,68,68,.08),rgba(255,255,255,.02))}
.dsh-vm-alert.high{border-left-color:#f59e0b;background:linear-gradient(90deg,rgba(245,158,11,.06),rgba(255,255,255,.02))}
.dsh-vm-alert.medium{border-left-color:#6ba3ff}
.dsh-vm-alert .icon{font-size:22px;line-height:1;flex-shrink:0;margin-top:2px}
.dsh-vm-alert .body{flex:1}
.dsh-vm-alert .title{font-size:13.5px;font-weight:600;color:#e6e9f0;margin-bottom:4px}
.dsh-vm-alert .detail{font-size:12px;color:#a8b2d4;line-height:1.5}
.dsh-vm-alert .meta-row{display:flex;gap:12px;margin-top:6px;font-size:10.5px;color:#8b95b8}
.dsh-vm-alert .deadline{font-family:'SF Mono',Monaco,monospace;color:#fbbf24}
.dsh-vm-alert a{color:#93c5fd;text-decoration:none;border-bottom:1px dashed rgba(147,197,253,.3)}
.dsh-vm-alert a:hover{color:#dbeafe;border-bottom-color:#dbeafe}
.dsh-vm-risks{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px}
.dsh-vm-risk{background:rgba(255,255,255,.03);border:1px solid rgba(120,140,200,.18);border-radius:9px;padding:11px 13px;position:relative}
.dsh-vm-risk.critical{border-color:rgba(239,68,68,.4)}
.dsh-vm-risk.high{border-color:rgba(245,158,11,.4)}
.dsh-vm-risk .id{font-size:10px;color:#8b95b8;font-family:'SF Mono',Monaco,monospace;letter-spacing:.5px}
.dsh-vm-risk .t{font-size:13px;font-weight:600;color:#e6e9f0;margin:3px 0 5px}
.dsh-vm-risk .m{font-size:11px;color:#a8b2d4;line-height:1.5}
.dsh-vm-loading{text-align:center;padding:28px;color:#8b95b8;font-size:12px}
.dsh-vm-error{padding:15px;background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.3);border-radius:9px;color:#fca5a5;font-size:12px}
`;
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// ── HTML skeleton ──
const wrap = document.createElement('div');
wrap.className = 'dsh-vm-wrap';
wrap.innerHTML = `
<div class="dsh-vm-card">
<div class="dsh-vm-head">
<h3>🎯 DG Command Center · Alertes business live</h3>
<span class="badge live">● LIVE</span>
<span class="meta" id="dsh-vm-ts">—</span>
</div>
<div id="dsh-vm-body" class="dsh-vm-loading">Chargement alertes DG…</div>
</div>`;
// Inject at target or body end
const target = document.getElementById('dsh-vm-alerts-mount') || document.body;
target.appendChild(wrap);
// ── Fetch + render ──
async function refresh(){
try {
const r = await fetch(API_DG, { cache: 'no-store' });
const d = await r.json();
const body = document.getElementById('dsh-vm-body');
const ts = document.getElementById('dsh-vm-ts');
const s = d.summary || {};
ts.textContent = (s.timestamp || '').replace('T',' ').split('.')[0] + ' UTC';
// Stats row
const statsHtml = `
<div class="dsh-vm-stats">
<div class="dsh-vm-stat"><div class="k">Bottleneck TOC</div><div class="v high">${s.toc_bottleneck_label||'—'}</div></div>
<div class="dsh-vm-stat"><div class="k">Conversion</div><div class="v">${((s.conversion_overall_pct||0)*100).toFixed(2)}%</div></div>
<div class="dsh-vm-stat"><div class="k">Pipeline k€</div><div class="v">${s.pipeline_value_keur||0}</div></div>
<div class="dsh-vm-stat"><div class="k">Clients actifs</div><div class="v">${s.active_clients||0}</div></div>
<div class="dsh-vm-stat"><div class="k">Alertes</div><div class="v crit">${s.alerts_dg_count||0}</div></div>
<div class="dsh-vm-stat"><div class="k">Risques critiques</div><div class="v crit">${s.risks_critical||0}</div></div>
<div class="dsh-vm-stat"><div class="k">Funnel leaks</div><div class="v high">${s.funnel_leaks_count||0}</div></div>
<div class="dsh-vm-stat"><div class="k">Data pipeline</div><div class="v good">${s.data_pipeline_health_pct||0}%</div></div>
</div>`;
const alertsArr = d.alerts_dg || [];
const alertsHtml = alertsArr.length ? `
<h4 style="margin:6px 0 10px;color:#c4d0ff;font-size:13px;font-weight:600">🚨 Alertes DG (${alertsArr.length})</h4>
<div class="dsh-vm-alerts">
${alertsArr.map(a => `
<div class="dsh-vm-alert ${a.level||'medium'}">
<div class="icon">${a.icon||'•'}</div>
<div class="body">
<div class="title">${escapeHtml(a.title||'')}</div>
<div class="detail">${escapeHtml(a.detail||'')}</div>
<div class="meta-row">
${a.owner?`<span>👤 ${escapeHtml(a.owner)}</span>`:''}
${a.deadline?`<span class="deadline">⏰ ${escapeHtml(a.deadline)}</span>`:''}
${a.action_link && a.action_link!=='#'?`<a href="${escapeAttr(a.action_link)}" target="_blank">→ Action</a>`:''}
</div>
</div>
</div>`).join('')}
</div>` : '';
const risksArr = d.risks || [];
const risksHtml = risksArr.length ? `
<h4 style="margin:16px 0 10px;color:#c4d0ff;font-size:13px;font-weight:600">⚠️ Risques (${risksArr.length})</h4>
<div class="dsh-vm-risks">
${risksArr.slice(0,6).map(r => `
<div class="dsh-vm-risk ${r.priority||''}">
<div class="id">${escapeHtml(r.id||'')} · ${escapeHtml(r.category||'')}</div>
<div class="t">${escapeHtml(r.title||'')}</div>
<div class="m">${escapeHtml(r.mitigation||'')}</div>
</div>`).join('')}
</div>` : '';
body.innerHTML = statsHtml + alertsHtml + risksHtml;
body.classList.remove('dsh-vm-loading');
} catch(e) {
document.getElementById('dsh-vm-body').innerHTML =
`<div class="dsh-vm-error">⚠ Erreur fetch DG : ${escapeHtml(e.message)}</div>`;
}
}
function escapeHtml(s){return String(s||'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
function escapeAttr(s){return escapeHtml(s);}
// First render + periodic refresh
refresh();
setInterval(refresh, REFRESH_MS);
})();