gigafibre-fsm/services/targo-hub/lib/chatwoot-panel.html
louispaulb 512c4a5f1b feat(ops): dispatch auto complet + perf Boîte/rapports + fix session
Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré /
juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis
par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts,
sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle
week-end + placeholder « en attente du quart », clustering + lasso + filtre-date
sur la carte, accès rapide « À assigner » (badge).

Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à
l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive
+ timeout fetch global + authFetch durci → fin des rechargements manuels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:49:35 -04:00

105 lines
5.9 KiB
HTML

<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fiche TARGO</title>
<style>
:root { --ink:#1f2733; --muted:#6b7785; --line:#e6e9ee; --brand:#5b3df5; --red:#d23b3b; --green:#1a9d5a; --bg:#fff; }
* { box-sizing:border-box; }
html,body { margin:0; background:var(--bg); color:var(--ink); font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }
.wrap { padding:14px; }
.state { color:var(--muted); padding:24px 12px; text-align:center; }
.spin { display:inline-block; width:18px; height:18px; border:2px solid var(--line); border-top-color:var(--brand); border-radius:50%; animation:r .7s linear infinite; vertical-align:middle; }
@keyframes r { to { transform:rotate(360deg); } }
.name { font-size:18px; font-weight:700; line-height:1.2; }
.sub { color:var(--muted); font-size:12px; margin-top:2px; word-break:break-word; }
.row { display:flex; align-items:center; gap:8px; }
.bal { margin:12px 0; padding:12px; border-radius:10px; border:1px solid var(--line); }
.bal .lbl { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); }
.bal .amt { font-size:22px; font-weight:700; margin-top:2px; }
.bal.due { background:#fdeaea; border-color:#f6c9c9; } .bal.due .amt { color:var(--red); }
.bal.ok { background:#eafaf1; border-color:#bfead0; } .bal.ok .amt { color:var(--green); }
.sec { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); margin:14px 0 6px; }
.inv { display:flex; justify-content:space-between; gap:8px; padding:7px 0; border-bottom:1px solid var(--line); font-size:13px; }
.inv:last-child { border-bottom:0; } .inv .d { color:var(--muted); }
.inv .a { font-weight:600; color:var(--red); white-space:nowrap; }
.btn { display:block; width:100%; text-align:center; text-decoration:none; padding:9px 12px; border-radius:8px; font-weight:600; font-size:13px; margin-top:8px; }
.btn.p { background:var(--brand); color:#fff; }
.btn.s { background:#f1f2f6; color:var(--ink); }
.chip { display:inline-block; background:#f1f2f6; border-radius:20px; padding:2px 9px; font-size:12px; margin:2px 4px 2px 0; }
.cand a { color:var(--brand); text-decoration:none; }
a.lnk { color:var(--brand); text-decoration:none; }
</style>
</head>
<body>
<div class="wrap"><div id="app"><div class="state"><span class="spin"></span> En attente du contexte…</div></div></div>
<script>
var KEY = new URLSearchParams(location.search).get('key') || '';
var app = document.getElementById('app');
var lastQ = '';
function esc (s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' })[c]; }); }
function money (n) { try { return Number(n || 0).toLocaleString('fr-CA', { style:'currency', currency:'CAD' }); } catch (e) { return (n || 0) + ' $'; } }
function setState (html) { app.innerHTML = '<div class="state">' + html + '</div>'; }
function render (r) {
if (!r || !r.found) {
var q = (r && r.query) || {};
var hint = (q.email || q.phone) ? ('Aucune fiche pour <b>' + esc(q.email || q.phone) + '</b>.') : 'Aucun courriel/téléphone sur ce contact.';
setState(hint + '<br><br><a class="lnk" target="_blank" href="' + ESC_OPS + '/#/clients">Chercher dans OPS →</a>');
return;
}
var c = r.customer, h = '';
h += '<div class="name">' + esc(c.customer_name) + '</div>';
h += '<div class="sub">' + esc(c.territory || '—') + (c.email ? ' · ' + esc(c.email) : '') + (c.phone ? ' · ' + esc(c.phone) : '') + '</div>';
var due = (r.balance || 0) > 0;
h += '<div class="bal ' + (due ? 'due' : 'ok') + '"><div class="lbl">Solde</div><div class="amt">' + money(r.balance) + '</div></div>';
if (r.invoices && r.invoices.length) {
h += '<div class="sec">Factures impayées (' + r.invoices.length + ')</div>';
r.invoices.forEach(function (i) {
h += '<div class="inv"><span class="d">' + esc(i.posting_date || '') + ' · ' + esc(i.name) + '</span><span class="a">' + money(i.outstanding_amount) + '</span></div>';
});
}
if (r.candidates && r.candidates.length) {
h += '<div class="sec">Autres fiches possibles</div><div class="cand">';
r.candidates.forEach(function (m) { h += '<a class="chip" target="_blank" href="' + esc(m.opsUrl) + '">' + esc(m.customer_name) + '</a>'; });
h += '</div>';
}
h += '<a class="btn p" target="_blank" href="' + esc(r.opsUrl) + '">Ouvrir la fiche OPS →</a>';
h += '<a class="btn s" target="_blank" href="' + esc(r.opsUrl) + '">+ Devis · + Commande</a>';
app.innerHTML = h;
}
var ESC_OPS = 'https://erp.gigafibre.ca/ops';
function doLookup (email, phone) {
var q = (email || '') + '|' + (phone || '');
if (q === lastQ) return; lastQ = q;
if (!email && !phone) { render({ found:false, query:{} }); return; }
setState('<span class="spin"></span> Chargement de la fiche…');
fetch('/chatwoot/lookup?key=' + encodeURIComponent(KEY) + '&email=' + encodeURIComponent(email || '') + '&phone=' + encodeURIComponent(phone || ''))
.then(function (r) { return r.json(); })
.then(function (r) { render(r); })
.catch(function (e) { setState('Erreur de chargement : ' + esc(e.message)); });
}
function handleContact (c) {
c = c || {};
var email = c.email || '';
var phone = c.phone_number || '';
doLookup(email, phone);
}
function isJSON (s) { try { JSON.parse(s); return true; } catch (e) { return false; } }
window.addEventListener('message', function (e) {
if (typeof e.data !== 'string' || !isJSON(e.data)) return;
var d = JSON.parse(e.data);
if (d.event !== 'appContext') return;
handleContact((d.data && d.data.contact) || {});
});
// Demande le contexte (au cas où il a déjà été poussé avant l'attache du listener).
try { window.parent.postMessage('chatwoot-dashboard-app:fetch-info', '*'); } catch (e) {}
</script>
</body>
</html>