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>
105 lines
5.9 KiB
HTML
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 ({ '&':'&','<':'<','>':'>','"':'"' })[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>
|