(() => {
/* stats.groupeverlaine.com — 16/09/2026 (Felix : « un site qui donne les mêmes stats que l'hebdo et le quotidien, avec le détail
   et le rapprochement compta exact ; seuls dylan.attia et salomon.hazan peuvent s'y connecter »).
   Synthèse = EXACTEMENT le code des e-mails COMEX (kpi_common.js) branché sur les mêmes ponts. Détails = listes CRM / compta / Jarvis. */
const { useState, useEffect, useMemo, useRef } = React;
const K = window.KPI;
const API = window.STATS_API;
const SEG_NAMES = { pv: 'Panneaux photovoltaïques', ite: 'Isolation extérieure', pac_airair: 'PAC air-air', pac_aireau: 'PAC air-eau', pac: 'PAC (air-air + air-eau)', clim: 'PAC air-air', autres: 'Autres', remises: 'Remises / RRR', global: 'Global', non_ventile: 'Non ventilé' };
const MO = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'];
const COST_MAP = { '62221': 'A · Commercial PV', '62222': 'A · Commercial ITE', '62223': 'A · Commercial PAC', '6011': 'B · Matériel PV', '6012': 'B · Matériel ITE', '6013': 'B · Matériel PAC', '60405': 'C · Installateur PV', '60403': 'C · Installateur ITE', '60404': 'C · Installateur PAC' };

/* ---------- utilitaires ---------- */
const pad = (n) => String(n).padStart(2, '0');
const fmtD = (d) => d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
const parseD = (s) => { const [y, m, d] = s.split('-').map(Number); return new Date(y, m - 1, d); };
const addDays = (s, n) => { const d = parseD(s); d.setDate(d.getDate() + n); return fmtD(d); };
const frDate = (s) => { if (!s || s.startsWith('0000')) return '—'; const d = parseD(s.slice(0, 10)); return pad(d.getDate()) + '/' + pad(d.getMonth() + 1) + '/' + d.getFullYear() + (s.length > 10 ? ' ' + s.slice(11, 16) : ''); };
const E = (n) => K.E(n);
const N = (n) => Number(n || 0).toLocaleString('fr-FR');
const PCT = (n, d) => K.PCT(n, d);
const cls = (n) => (Number(n) < 0 ? 'neg' : '');
async function api(action, body) {
  const session = localStorage.getItem('stats_session') || '';
  const r = await fetch(API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ action, session }, body || {})) });
  let j = null; try { j = await r.json(); } catch (e) {}
  if (r.status === 401 && j && j.error === 'SESSION_INVALID') { localStorage.removeItem('stats_session'); localStorage.removeItem('stats_user'); location.reload(); }
  if (!j) throw new Error('Réponse invalide (' + r.status + ')');
  if (!j.ok && j.error) throw new Error(j.detail || j.error);
  return j;
}
function periodOf(preset, ref) {
  const t = ref ? parseD(ref) : new Date(); const today = fmtD(t);
  const dow = t.getDay() === 0 ? 6 : t.getDay() - 1;
  const mon = addDays(today, -dow);
  const m1 = today.slice(0, 8) + '01';
  const prevM = new Date(t.getFullYear(), t.getMonth() - 1, 1);
  const prevMs = fmtD(prevM), prevMe = fmtD(new Date(t.getFullYear(), t.getMonth(), 0));
  switch (preset) {
    case 'today': return { ds: today, de: today, label: 'aujourd’hui · ' + frDate(today) };
    case 'yesterday': { const y = addDays(today, -1); return { ds: y, de: y, label: 'hier · ' + frDate(y) }; }
    case 'week': return { ds: mon, de: addDays(mon, 6), label: 'semaine en cours · du ' + frDate(mon) + ' au ' + frDate(addDays(mon, 6)) };
    case 'lastweek': return { ds: addDays(mon, -7), de: addDays(mon, -1), label: 'semaine dernière · du ' + frDate(addDays(mon, -7)) + ' au ' + frDate(addDays(mon, -1)) };
    case 'month': return { ds: m1, de: today, label: 'mois en cours · du ' + frDate(m1) + ' au ' + frDate(today) };
    case 'lastmonth': return { ds: prevMs, de: prevMe, label: MO[prevM.getMonth()] + ' ' + prevM.getFullYear() };
    case 'year': return { ds: t.getFullYear() + '-01-01', de: today, label: 'année ' + t.getFullYear() + ' · du 01/01 au ' + frDate(today) };
    default: return { ds: today, de: today, label: frDate(today) };
  }
}
function useSort(rows, def, desc) {
  const [s, setS] = useState({ k: def, d: desc !== false });
  const sorted = useMemo(() => { const a = (rows || []).slice(); a.sort((x, y) => { const vx = x[s.k], vy = y[s.k]; const nx = parseFloat(vx), ny = parseFloat(vy); const c = (!isNaN(nx) && !isNaN(ny) && typeof vx !== 'string') ? nx - ny : String(vx || '').localeCompare(String(vy || ''), 'fr'); return s.d ? -c : c; }); return a; }, [rows, s]);
  const th = (k, label, extra) => <th className={extra || ''} onClick={() => setS(o => ({ k, d: o.k === k ? !o.d : true }))}>{label}{s.k === k ? (s.d ? ' ▾' : ' ▴') : ''}</th>;
  return [sorted, th];
}
function Loading({ txt }) { return <div className="loading"><span className="spin"></span>{txt || 'Chargement des données…'}</div>; }

/* ---------- connexion ---------- */
function Login({ onOk }) {
  const [email, setEmail] = useState(localStorage.getItem('stats_email') || ''); const [code, setCode] = useState(''); const [step, setStep] = useState(1); const [busy, setBusy] = useState(false); const [err, setErr] = useState('');
  const send = async () => { setBusy(true); setErr(''); try { await api('otp_send', { email }); localStorage.setItem('stats_email', email); setStep(2); } catch (e) { setErr(e.message === 'NOT_ALLOWED' || /accès/.test(e.message) ? 'Cette adresse n’a pas accès aux statistiques.' : 'Envoi impossible : ' + e.message); } setBusy(false); };
  const verify = async () => { setBusy(true); setErr(''); try { const j = await api('otp_verify', { email, code }); localStorage.setItem('stats_session', j.session); localStorage.setItem('stats_user', JSON.stringify(j.user)); onOk(j.user); } catch (e) { setErr(e.message === 'BAD_CODE' ? 'Code incorrect.' : e.message === 'CODE_EXPIRED' ? 'Code expiré, demandez-en un nouveau.' : e.message); } setBusy(false); };
  return <div className="login">
    <h1>Statistiques Groupe Verlaine</h1>
    <p>Accès réservé. Un code de connexion vous est envoyé par e-mail.</p>
    {err && <div className="err">{err}</div>}
    {step === 1 ? <>
      <input type="email" placeholder="prenom.nom@groupeverlaine.fr" value={email} onChange={e => setEmail(e.target.value)} onKeyDown={e => e.key === 'Enter' && send()} autoFocus />
      <button className="btn" disabled={busy || !email} onClick={send}>{busy ? 'Envoi…' : 'Recevoir mon code'}</button>
    </> : <>
      <div className="ok">Code envoyé à <b>{email}</b>. Valable 10 minutes.</div>
      <input inputMode="numeric" placeholder="Code à 6 chiffres" value={code} onChange={e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} onKeyDown={e => e.key === 'Enter' && verify()} autoFocus />
      <button className="btn" disabled={busy || code.length !== 6} onClick={verify}>{busy ? 'Vérification…' : 'Se connecter'}</button>
      <div style={{ height: 8 }} />
      <button className="btn sec" onClick={() => setStep(1)}>Changer d’adresse / renvoyer un code</button>
    </>}
  </div>;
}

/* ---------- synthèse (code des e-mails) ---------- */
function Synthese({ ds, de, label, data }) {
  const html = useMemo(() => {
    if (!data) return '';
    const fam = (data.ca && data.ca.data) || {}; const sv = K.spendView(data.spend); const qv = K.quotedView(data.quoted); const lv = K.leadsView(data.leads);
    const auto = data.ytd && data.ytd.data ? K.autoRatesFrom([data.ytd.data], data.ytd_label) : null;
    const cm = K.blocsCoutsMarge(fam, sv, [], de.slice(0, 7), label, auto);
    const alerts = !fam.global ? K.note('Ventes indisponibles (pont ca_signe muet).', 'err') : '';
    const partial = fam.compta && fam.compta.partial ? K.note('Compta : synchronisation PennyLane incomplète sur la période (' + (fam.compta.failed_days || []).join(', ') + ').', 'warn') : '';
    return [alerts + partial + K.blocSigned(fam, label), K.blocInvoiced(fam, label, qv), cm.html3, cm.html4, K.blocSpend(sv, lv, '5 · Dépenses publicitaires · ' + label, 'Google + Meta (Jarvis) ; leads = leads CRM valides rattachés aux campagnes')]
      .map(h => '<div class="card email-block">' + h + '</div>').join('');
  }, [data, label]);
  if (!data) return <Loading />;
  const g = (data.ca && data.ca.data && data.ca.data.global) || {}; const sv = K.spendView(data.spend); const lv = K.leadsView(data.leads);
  const spend = sv.ok ? K.num(sv.s.global.total_spend) : null; const leads = lv.ok ? lv.valid : null;
  return <>
    <div className="kpi-grid">
      <div className="kpi"><div className="l">CA signé HT</div><div className="v">{E(g.ca_signed_ht)}</div><div className="s">{N(g.nb_signed)} devis signés</div></div>
      <div className="kpi"><div className="l">CA facturé HT</div><div className="v">{E(g.fac_ht)}</div><div className="s">compta PennyLane · {N(g.nb_invoiced)} factures CRM</div></div>
      <div className="kpi"><div className="l">Dépense pub</div><div className="v">{spend === null ? 'n/d' : E(spend)}</div><div className="s">{sv.ok ? 'Google ' + E(sv.s.global.google_ads) + ' · Meta ' + E(sv.s.global.meta_ads) : 'Jarvis indisponible'}</div></div>
      <div className="kpi"><div className="l">Leads valides</div><div className="v">{leads === null ? 'n/d' : N(leads)}</div><div className="s">{spend !== null && leads ? 'coût / lead ' + E(spend / leads) : 'CRM, doublons exclus'}</div></div>
    </div>
    <div className="note" style={{ marginBottom: 10 }}>Mêmes calculs et mêmes sources que les e-mails COMEX (synthèse hebdo / rapport du jour). Coûts A/B/C = taux compta {data.ytd_label || ''} × CA facturé HT ; aucune saisie manuelle de taux n’est prise en compte ici.</div>
    <div dangerouslySetInnerHTML={{ __html: html }} />
  </>;
}

/* ---------- détails : devis signés / devisés ---------- */
function Devis({ ds, de, kind }) {
  const [seg, setSeg] = useState(''); const [q, setQ] = useState(''); const [data, setData] = useState(null); const [err, setErr] = useState('');
  useEffect(() => { setData(null); setErr(''); api(kind === 'signed' ? 'detail_signed' : 'detail_quoted', { date_start: ds, date_end: de, seg }).then(setData).catch(e => setErr(e.message)); }, [ds, de, seg, kind]);
  const rows = useMemo(() => { if (!data) return []; const s = q.trim().toLowerCase(); return s ? data.rows.filter(r => (r.client + ' ' + r.ville + ' ' + r.commercial + ' ' + r.type_label + ' ' + r.id_project).toLowerCase().includes(s)) : data.rows; }, [data, q]);
  const [sorted, th] = useSort(rows, kind === 'signed' ? 'date_signature' : 'date_devis');
  const tot = rows.reduce((a, r) => ({ ht: a.ht + r.ht, ttc: a.ttc + r.ttc, nb: a.nb + 1 }), { ht: 0, ttc: 0, nb: 0 });
  const byCom = useMemo(() => { const m = {}; rows.forEach(r => { const k = r.commercial || '—'; m[k] = m[k] || { commercial: k, nb: 0, ht: 0 }; m[k].nb++; m[k].ht += r.ht; }); return Object.values(m).sort((a, b) => b.ht - a.ht); }, [rows]);
  return <div className="card">
    <h2>{kind === 'signed' ? 'Devis signés' : 'Devis émis (CA devisé)'} · {N(tot.nb)} · {E(tot.ht)} HT · {E(tot.ttc)} TTC</h2>
    <div className="bar">
      {[['', 'Tous'], ['pv', 'PV'], ['ite', 'ITE'], ['pac_aireau', 'PAC air-eau'], ['pac_airair', 'PAC air-air'], ['autres', 'Autres']].map(([k, l]) => <button key={k} className={'chip' + (seg === k ? ' on' : '')} onClick={() => setSeg(k)}>{l}</button>)}
      <input className="filter" placeholder="Rechercher client, ville, commercial…" value={q} onChange={e => setQ(e.target.value)} />
    </div>
    {err && <div className="err">{err}</div>}
    {!data && !err ? <Loading /> : <>
      <div className="note" style={{ marginBottom: 8 }}>{kind === 'signed' ? 'Devis dont la date de signature est dans la période (montants contrat HT / TTC, dossiers isolation classés en ITE) — définition du bloc « Ventes signées » des e-mails.' : 'Devis dont la date d’émission est dans la période (montant HT > 0) — base CRM du CA devisé de ca_signe ; le tableau 2 bis des e-mails utilise la définition « devis envoyés » du tableau de bord CRM (onglet Commerciaux).'}</div>
      <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr)', gap: 10 }}>
        <table className="dt"><thead><tr>{th('date_signature', kind === 'signed' ? 'Signature' : 'Émis', 'l')}{th('client', 'Client', 'l')}{th('ville', 'Ville', 'l')}{th('type_label', 'Produit', 'l')}{th('commercial', 'Commercial', 'l')}{th('ht', 'HT')}{th('ttc', 'TTC')}</tr></thead>
          <tbody>{sorted.map(r => <tr key={r.id_prestation}><td className="l">{frDate(kind === 'signed' ? r.date_signature : r.date_devis)}</td><td className="l"><a href={'https://crm.groupeverlaine.net/crm/contact.php?id_contact=' + r.id_contact} target="_blank" rel="noreferrer">{r.client || '(sans nom)'}</a> <span className="muted">#{r.id_project}</span></td><td className="l">{r.ville} {r.cp}</td><td className="l">{r.type_label}</td><td className="l">{r.commercial}</td><td className={cls(r.ht)}>{E(r.ht)}</td><td>{E(r.ttc)}</td></tr>)}
            <tr className="tot"><td className="l" colSpan={5}>Total {N(tot.nb)} devis</td><td>{E(tot.ht)}</td><td>{E(tot.ttc)}</td></tr></tbody></table>
        <div><h2 style={{ marginTop: 10 }}>Par commercial</h2><table className="dt"><thead><tr><th className="l">Commercial</th><th className="c">Devis</th><th>HT</th><th>HT moyen</th></tr></thead><tbody>{byCom.map(c => <tr key={c.commercial}><td className="l">{c.commercial}</td><td className="c">{c.nb}</td><td>{E(c.ht)}</td><td>{E(c.ht / c.nb)}</td></tr>)}</tbody></table></div>
      </div>
    </>}
  </div>;
}

/* ---------- détails compta (70x facturé / 6xx coûts) + lignes PennyLane ---------- */
function Ledger({ d, account, onClose }) {
  const [data, setData] = useState(null); const [err, setErr] = useState('');
  useEffect(() => { api('ledger_lines', { d, account: account || '' }).then(setData).catch(e => setErr(e.message)); }, [d, account]);
  const accs = data ? Object.keys(data.by_account).sort() : [];
  return <div className="modal-bg" onClick={onClose}><div className="modal" onClick={e => e.stopPropagation()}>
    <button className="x" onClick={onClose}>Fermer ✕</button>
    <h3>Grand livre PennyLane · {frDate(d)}{account ? ' · compte ' + account : ''}</h3>
    <div className="note">Lignes lues en direct dans PennyLane (mandant AMG Façades). Journal de situation (SIT) affiché barré et exclu des totaux. 70x : HT = crédit − débit ; 6xx : HT = débit − crédit.</div>
    {err && <div className="err">{err}</div>}
    {!data && !err ? <Loading txt="Lecture du grand livre PennyLane…" /> : data && <>
      <h3 style={{ marginTop: 12 }}>Rapprochement par compte</h3>
      <table className="dt"><thead><tr><th className="l">Compte</th><th>Σ lignes PennyLane</th><th>Valeur du rapport (cache)</th><th className="c">Écart</th></tr></thead><tbody>
        {accs.map(a => { const l = data.by_account[a] || 0, c = data.cache[a]; const ok = c !== undefined && Math.abs(l - c) < 0.01; return <tr key={a}><td className="l">{a}</td><td className={cls(l)}>{E(l)}</td><td>{c === undefined ? <span className="muted">non synchronisé</span> : E(c)}</td><td className="c">{ok ? <span className="pos">✓ exact</span> : <span className="neg">{c === undefined ? '—' : E(l - c)}</span>}</td></tr>; })}
        {Object.keys(data.cache).filter(a => !(a in data.by_account)).map(a => <tr key={'c' + a}><td className="l">{a}</td><td className="muted">aucune ligne</td><td>{E(data.cache[a])}</td><td className="c neg">{E(-data.cache[a])}</td></tr>)}
        <tr className="tot"><td className="l">Total 70x (produits)</td><td colSpan={3}>{E(data.total_70_ht)}</td></tr>
        {data.total_6_ht !== 0 && <tr className="tot"><td className="l">Total 6xx (charges)</td><td colSpan={3}>{E(data.total_6_ht)}</td></tr>}
      </tbody></table>
      <h3 style={{ marginTop: 14 }}>{N(data.lines.length)} lignes</h3>
      <table className="dt"><thead><tr><th className="l">Compte</th><th className="l">Libellé compte</th><th className="l">Journal</th><th className="l">Écriture</th><th>Débit</th><th>Crédit</th><th>HT retenu</th></tr></thead><tbody>
        {data.lines.map(l => <tr key={l.id} style={l.excluded ? { textDecoration: 'line-through', opacity: .55 } : {}}><td className="l">{l.account}</td><td className="l">{l.account_label}</td><td className="l">{l.journal || '—'}</td><td className="l"><a href={'https://app.pennylane.com/companies/' + '#/ledger_entries/' + l.entry_id} target="_blank" rel="noreferrer" className="muted">#{l.entry_id}</a>{l.label ? ' ' + l.label : ''}</td><td>{l.debit ? E(l.debit) : ''}</td><td>{l.credit ? E(l.credit) : ''}</td><td className={cls(l.ht)}>{E(l.ht)}</td></tr>)}
      </tbody></table>
      {!data.complete && <div className="warn">Journée très volumineuse : lecture tronquée.</div>}
    </>}
  </div></div>;
}
function Compta({ ds, de, kind }) {
  const [data, setData] = useState(null); const [err, setErr] = useState(''); const [modal, setModal] = useState(null);
  useEffect(() => { setData(null); setErr(''); api(kind === 'costs' ? 'detail_costs' : 'detail_invoiced', { date_start: ds, date_end: de }).then(setData).catch(e => setErr(e.message)); }, [ds, de, kind]);
  const days = useMemo(() => { if (!data) return []; const m = {}; data.rows.forEach(r => { m[r.d] = m[r.d] || { d: r.d, ht: 0, n: 0, fam: {} }; m[r.d].ht += r.ht; m[r.d].n++; m[r.d].fam[r.famille] = (m[r.d].fam[r.famille] || 0) + r.ht; }); return Object.values(m).sort((a, b) => a.d < b.d ? 1 : -1); }, [data]);
  const fams = useMemo(() => { if (!data) return {}; const m = {}; data.accounts.forEach(a => { m[a.famille] = (m[a.famille] || 0) + a.ht; }); return m; }, [data]);
  const total = data ? data.accounts.reduce((a, x) => a + x.ht, 0) : 0;
  const synced = data ? data.sync.length : 0; const nDays = Math.round((parseD(de) - parseD(ds)) / 86400000) + 1;
  return <div className="card">
    <h2>{kind === 'costs' ? 'Charges 6xx (compta PennyLane)' : 'CA facturé (compta PennyLane, comptes 70x)'} · {E(total)} HT</h2>
    {err && <div className="err">{err}</div>}
    {!data && !err ? <Loading /> : data && <>
      {synced < nDays && <div className="warn">{nDays - synced} jour(s) de la période non synchronisé(s) dans le cache (jours futurs ou synchro en attente) : les totaux des e-mails déclenchent la synchro automatiquement.</div>}
      <div className="note" style={{ marginBottom: 8 }}>{data.note} Chaque jour est synchronisé depuis le grand livre puis re-synchronisé jusqu’à J+2 (écritures tardives). Cliquez un jour pour voir les lignes PennyLane et le rapprochement exact.</div>
      <div className="kpi-grid">{Object.keys(fams).sort().map(f => <div className="kpi" key={f}><div className="l">{SEG_NAMES[f] || f}</div><div className={'v ' + cls(fams[f])} style={{ fontSize: 17 }}>{E(fams[f])}</div></div>)}</div>
      <h2>Par compte</h2>
      <table className="dt"><thead><tr><th className="l">Compte</th><th className="l">Libellé</th><th className="l">{kind === 'costs' ? 'Poste COMEX' : 'Famille'}</th><th className="c">Jours</th><th>HT</th></tr></thead><tbody>
        {data.accounts.slice().sort((a, b) => Math.abs(b.ht) - Math.abs(a.ht)).map(a => <tr key={a.account}><td className="l">{a.account}</td><td className="l">{a.label}</td><td className="l">{kind === 'costs' ? (COST_MAP[a.account] || <span className="muted">non ventilé</span>) : (SEG_NAMES[a.famille] || a.famille)}</td><td className="c">{a.days}</td><td className={cls(a.ht)}>{E(a.ht)}</td></tr>)}
        <tr className="tot"><td className="l" colSpan={4}>Total</td><td className={cls(total)}>{E(total)}</td></tr></tbody></table>
      <h2 style={{ marginTop: 14 }}>Par jour</h2>
      <table className="dt"><thead><tr><th className="l">Jour</th><th className="c">Comptes</th>{['pv', 'ite', 'pac', 'autres', 'remises'].filter(f => kind !== 'costs').map(f => <th key={f}>{SEG_NAMES[f]}</th>)}<th>HT</th><th className="c">Synchro</th></tr></thead><tbody>
        {days.map(dd => { const s = data.sync.find(x => x.d === dd.d); return <tr key={dd.d} className="click" onClick={() => setModal({ d: dd.d })}><td className="l">{frDate(dd.d)} <span className="muted">▸ lignes</span></td><td className="c">{dd.n}</td>{kind !== 'costs' && ['pv', 'ite', 'pac', 'autres', 'remises'].map(f => <td key={f} className={cls(dd.fam[f])}>{dd.fam[f] ? E(dd.fam[f]) : ''}</td>)}<td className={cls(dd.ht)}>{E(dd.ht)}</td><td className="c muted">{s ? frDate(s.synced_at.slice(0, 10)) + ' ' + s.synced_at.slice(11, 16) : '—'}</td></tr>; })}
      </tbody></table>
    </>}
    {modal && <Ledger d={modal.d} onClose={() => setModal(null)} />}
  </div>;
}

/* ---------- détails pub (campagnes Jarvis) ---------- */
function Pub({ ds, de }) {
  const [data, setData] = useState(null); const [err, setErr] = useState('');
  useEffect(() => { setData(null); api('detail_spend', { date_start: ds, date_end: de }).then(setData).catch(e => setErr(e.message)); }, [ds, de]);
  const [sorted, th] = useSort(data ? data.rows : [], 'spend');
  const bySeg = useMemo(() => { const m = {}; (data ? data.rows : []).forEach(r => { m[r.segment] = m[r.segment] || { seg: r.segment, google: 0, meta: 0, n: 0 }; m[r.segment][r.source === 'google_ads' ? 'google' : 'meta'] += r.spend; m[r.segment].n++; }); return Object.values(m); }, [data]);
  const tot = (data ? data.rows : []).reduce((a, r) => a + r.spend, 0);
  return <div className="card"><h2>Campagnes publicitaires (Jarvis) · {E(tot)}</h2>
    {err && <div className="err">{err}</div>}
    {!data && !err ? <Loading /> : data && <>
      <div className="note" style={{ marginBottom: 8 }}>{data.note} Dernier jour consolidé : {frDate(data.latest_snapshot)}.</div>
      <div className="kpi-grid">{bySeg.map(s => <div className="kpi" key={s.seg}><div className="l">{SEG_NAMES[s.seg === 'pac' ? 'pac_aireau' : s.seg] || s.seg}</div><div className="v" style={{ fontSize: 17 }}>{E(s.google + s.meta)}</div><div className="s">Google {E(s.google)} · Meta {E(s.meta)} · {s.n} camp.</div></div>)}</div>
      <table className="dt"><thead><tr>{th('name', 'Campagne', 'l')}{th('source', 'Canal', 'l')}{th('segment', 'Segment', 'l')}{th('days', 'Jours', 'c')}{th('spend', 'Dépense')}{th('leads_jarvis', 'Leads Jarvis', 'c')}<th>CPL Jarvis</th></tr></thead><tbody>
        {sorted.map(r => <tr key={r.campaign_id + r.source}><td className="l">{r.name || r.campaign_id} <span className="muted">{r.campaign_id}</span></td><td className="l">{r.source === 'google_ads' ? 'Google' : r.source === 'facebook_ads' ? 'Meta' : r.source}</td><td className="l">{SEG_NAMES[r.segment === 'pac' ? 'pac_aireau' : r.segment] || r.segment}</td><td className="c">{r.days}</td><td>{E(r.spend)}</td><td className="c">{r.leads_jarvis}</td><td>{r.leads_jarvis ? E(r.spend / r.leads_jarvis) : '—'}</td></tr>)}
        <tr className="tot"><td className="l" colSpan={4}>Total</td><td>{E(tot)}</td><td colSpan={2}></td></tr></tbody></table>
    </>}
  </div>;
}

/* ---------- détails leads ---------- */
function Leads({ ds, de }) {
  const [data, setData] = useState(null); const [err, setErr] = useState(''); const [q, setQ] = useState('');
  useEffect(() => { setData(null); api('detail_leads', { date_start: ds, date_end: de }).then(setData).catch(e => setErr(e.message)); }, [ds, de]);
  const rows = useMemo(() => { if (!data) return []; const s = q.trim().toLowerCase(); return s ? data.rows.filter(r => (r.nom + ' ' + r.campagne + ' ' + r.source + ' ' + r.page).toLowerCase().includes(s)) : data.rows; }, [data, q]);
  const [sorted, th] = useSort(rows, 'date');
  const cross = useMemo(() => { const m = {}; rows.forEach(r => { m[r.segment] = m[r.segment] || { google: 0, meta: 0, other: 0 }; m[r.segment][r.canal]++; }); return m; }, [rows]);
  return <div className="card"><h2>Leads valides (CRM) · {data ? N(data.valid) : '…'}</h2>
    <div className="bar"><input className="filter" placeholder="Rechercher nom, campagne, source…" value={q} onChange={e => setQ(e.target.value)} /></div>
    {err && <div className="err">{err}</div>}
    {!data && !err ? <Loading /> : data && <>
      <div className="note" style={{ marginBottom: 8 }}>Leads au statut « valide » (doublons et fraudes exclus : {N(data.duplicates_skipped)} écartés). Canal = utm_source ; segment = tag Jarvis de la campagne (id ou nom), sinon mot-clé du nom de campagne.{data.truncated ? ' Liste limitée à 2 500 lignes.' : ''}</div>
      <table className="dt" style={{ marginBottom: 12 }}><thead><tr><th className="l">Segment</th><th className="c">Google</th><th className="c">Meta</th><th className="c">Autre</th><th className="c">Total</th></tr></thead><tbody>
        {Object.keys(cross).map(s => <tr key={s}><td className="l">{SEG_NAMES[s === 'pac' ? 'pac_aireau' : s] || s}</td><td className="c">{cross[s].google}</td><td className="c">{cross[s].meta}</td><td className="c">{cross[s].other}</td><td className="c"><b>{cross[s].google + cross[s].meta + cross[s].other}</b></td></tr>)}</tbody></table>
      <table className="dt"><thead><tr>{th('date', 'Date', 'l')}{th('nom', 'Lead', 'l')}{th('source', 'Source', 'l')}{th('campagne', 'Campagne', 'l')}{th('segment', 'Segment', 'l')}{th('page', 'Page / origine', 'l')}</tr></thead><tbody>
        {sorted.map((r, i) => <tr key={r.id || i}><td className="l">{frDate(r.date)}</td><td className="l">{r.contact_id ? <a href={'https://crm.groupeverlaine.net/crm/contact.php?id_contact=' + r.contact_id} target="_blank" rel="noreferrer">{r.nom || '—'}</a> : (r.nom || '—')}</td><td className="l">{r.source || '—'}{r.medium ? ' / ' + r.medium : ''}</td><td className="l">{r.campagne || '—'}</td><td className="l">{SEG_NAMES[r.segment === 'pac' ? 'pac_aireau' : r.segment] || r.segment} <span className="muted">{r.mapping !== '—' ? '(' + r.mapping + ')' : ''}</span></td><td className="l muted">{r.page}{r.fournisseur ? ' · ' + r.fournisseur : ''}</td></tr>)}
      </tbody></table>
    </>}
  </div>;
}

/* ---------- commerciaux (tableau de bord CRM /sales/performance) ---------- */
function Commerciaux({ ds, de }) {
  const [data, setData] = useState(null); const [err, setErr] = useState('');
  useEffect(() => { setData(null); api('perf', { date_start: ds, date_end: de }).then(setData).catch(e => setErr(e.message)); }, [ds, de]);
  const rows = data && data.data ? data.data.rows.map(r => Object.assign({}, r, { leads_assigned: +r.leads_assigned, quotes_sent: +r.quotes_sent, quotes_signed: +r.quotes_signed, ca_sent: +r.ca_sent, ca_signed: +r.ca_signed, quotes_pending: +r.quotes_pending })) : [];
  const [sorted, th] = useSort(rows, 'ca_sent');
  const t = data && data.data ? data.data.totals : null;
  return <div className="card"><h2>Performance commerciale (définition du tableau de bord CRM)</h2>
    {err && <div className="err">{err}</div>}
    {!data && !err ? <Loading /> : data && <>
      <div className="note" style={{ marginBottom: 8 }}>Même source que le tableau « CA devisé et taux de facturation » des e-mails (API CRM /sales/performance, montants TTC) : devis envoyés et signés sur la période, par commercial / agence.</div>
      <table className="dt"><thead><tr>{th('entity_name', 'Commercial', 'l')}{th('entity_category', 'Type', 'l')}{th('leads_assigned', 'Leads', 'c')}{th('quotes_sent', 'Devis envoyés', 'c')}{th('ca_sent', 'CA devisé TTC')}{th('quotes_signed', 'Signés', 'c')}{th('ca_signed', 'CA signé TTC')}{th('quotes_pending', 'En attente', 'c')}{th('conversion_rate', 'Conv. %', 'c')}</tr></thead><tbody>
        {sorted.map(r => <tr key={r.entity_id}><td className="l">{r.entity_name}</td><td className="l muted">{r.entity_category}</td><td className="c">{r.leads_assigned}</td><td className="c">{r.quotes_sent}</td><td>{E(r.ca_sent)}</td><td className="c">{r.quotes_signed}</td><td>{E(r.ca_signed)}</td><td className="c">{r.quotes_pending}</td><td className="c">{PCT(r.conversion_rate, 1)}</td></tr>)}
        {t && <tr className="tot"><td className="l" colSpan={2}>Total</td><td className="c">{t.leads_assigned}</td><td className="c">{t.quotes_sent}</td><td>{E(t.ca_sent)}</td><td className="c">{t.quotes_signed}</td><td>{E(t.ca_signed)}</td><td className="c">{t.quotes_pending}</td><td className="c">{PCT(t.conversion_rate, 1)}</td></tr>}
      </tbody></table>
    </>}
  </div>;
}

/* ---------- photographie mois par mois (mêmes formules que le bloc 6 de l'hebdo) ---------- */
function Mois({ year }) {
  const [data, setData] = useState(null); const [err, setErr] = useState(''); const [prog, setProg] = useState('');
  useEffect(() => {
    let stop = false; setData(null); setErr('');
    (async () => {
      const now = new Date(); const maxM = year < now.getFullYear() ? 12 : (year > now.getFullYear() ? 0 : now.getMonth() + 1); const out = [];
      try { for (let m = 1; m <= maxM; m++) { if (stop) return; setProg(m + ' / ' + maxM); out.push(await api('month', { year, month: m })); } if (!stop) setData({ months: out }); }
      catch (e) { if (!stop) setErr(e.message); }
    })();
    return () => { stop = true; };
  }, [year]);
  if (err) return <div className="card"><div className="err">{err}</div></div>;
  if (!data) return <div className="card"><Loading txt={'Photographie ' + year + ' : calcul mois par mois (' + prog + ') — jusqu’à 2 minutes la première fois, instantané ensuite…'} /></div>;
  if (!data.months.length) return <div className="card"><div className="warn">Aucun mois disponible pour {year}.</div></div>;
  const T = { sht: 0, sttc: 0, nb: 0, nbq: 0, fht: 0, fttc: 0, nbf: 0, g: 0, m: 0, sp: 0, ld: 0 }; let spendOk = true, quotedOk = true, leadsOk = true;
  const rows = data.months.map(mo => {
    const gl = (mo.ca && mo.ca.data && mo.ca.data.global) || {}; const sv = K.spendView(mo.spend); const sp = sv.ok ? sv.s.global : null; const qv = K.quotedView(mo.quoted); const q = qv.ok ? qv.s.global : null; const ld = mo.leads_stats && K.has(mo.leads_stats.valid) ? K.num(mo.leads_stats.valid) : null;
    const r = { label: mo.label, end: mo.end, sht: K.num(gl.ca_signed_ht), sttc: K.num(gl.ca_signed), nb: K.num(gl.nb_signed), nbq: q ? K.num(q.quotes_sent) : null, fht: K.num(gl.fac_ht), fttc: K.num(gl.fac_ttc), nbf: K.num(gl.nb_invoiced), g: sp ? K.num(sp.google_ads) : null, m: sp ? K.num(sp.meta_ads) : null, sp: sp ? K.num(sp.total_spend) : null, ld };
    if (r.nbq === null) quotedOk = false; if (!sp) spendOk = false; if (ld === null) leadsOk = false;
    T.sht += r.sht; T.sttc += r.sttc; T.nb += r.nb; T.nbq += r.nbq || 0; T.fht += r.fht; T.fttc += r.fttc; T.nbf += r.nbf; T.g += r.g || 0; T.m += r.m || 0; T.sp += r.sp || 0; T.ld += ld || 0;
    return r;
  });
  const nd = <span className="muted">n/d</span>;
  return <div className="card"><h2>Photographie {year} — mois par mois</h2>
    <div className="note" style={{ marginBottom: 8 }}>Du 1er janvier à ce jour · facturé = compta PennyLane (brut HT), hors journal de situation · mêmes formules que le bloc 6 de la synthèse hebdo. Mois en cours arrêté au {frDate(data.months[data.months.length - 1].end)}.</div>
    <h2 style={{ fontSize: 13 }}>Ventes signées et devis envoyés</h2>
    <table className="dt"><thead><tr><th className="l">Mois</th><th>Signé HT</th><th>Signé TTC</th><th className="c">Devis signés</th><th className="c">Devis envoyés</th><th>HT moyen</th></tr></thead><tbody>
      {rows.map(r => <tr key={r.label}><td className="l" style={{ textTransform: 'capitalize' }}>{r.label}</td><td><b>{E(r.sht)}</b></td><td>{E(r.sttc)}</td><td className="c">{r.nb}</td><td className="c">{r.nbq === null ? nd : r.nbq}</td><td>{r.nb ? E(r.sht / r.nb) : '—'}</td></tr>)}
      <tr className="tot"><td className="l">TOTAL {year}</td><td>{E(T.sht)}</td><td>{E(T.sttc)}</td><td className="c">{T.nb}</td><td className="c">{quotedOk ? T.nbq : nd}</td><td>{T.nb ? E(T.sht / T.nb) : '—'}</td></tr></tbody></table>
    <h2 style={{ fontSize: 13, marginTop: 14 }}>Ventes facturées (compta)</h2>
    <table className="dt"><thead><tr><th className="l">Mois</th><th>Facturé HT</th><th>Facturé TTC</th><th className="c">Factures CRM</th></tr></thead><tbody>
      {rows.map(r => <tr key={r.label}><td className="l" style={{ textTransform: 'capitalize' }}>{r.label}</td><td className={'pos ' + cls(r.fht)}><b>{E(r.fht)}</b></td><td>{E(r.fttc)}</td><td className="c">{r.nbf}</td></tr>)}
      <tr className="tot"><td className="l">TOTAL {year}</td><td>{E(T.fht)}</td><td>{E(T.fttc)}</td><td className="c">{T.nbf}</td></tr></tbody></table>
    <h2 style={{ fontSize: 13, marginTop: 14 }}>Dépenses publicitaires et coût par lead</h2>
    <table className="dt"><thead><tr><th className="l">Mois</th><th>Google</th><th>Meta</th><th>Pub total</th><th className="c">Leads valides</th><th>Coût / lead</th><th>Coût / devis envoyé</th><th>Coût / facture</th></tr></thead><tbody>
      {rows.map(r => <tr key={r.label}><td className="l" style={{ textTransform: 'capitalize' }}>{r.label}</td><td>{r.g === null ? nd : E(r.g)}</td><td>{r.m === null ? nd : E(r.m)}</td><td><b>{r.sp === null ? nd : E(r.sp)}</b></td><td className="c">{r.ld === null ? nd : r.ld}</td><td>{r.sp === null || !r.ld ? nd : E(r.sp / r.ld)}</td><td>{r.sp === null || !r.nbq ? nd : E(r.sp / r.nbq)}</td><td>{r.sp === null || !r.nbf ? nd : E(r.sp / r.nbf)}</td></tr>)}
      <tr className="tot"><td className="l">TOTAL {year}</td><td>{spendOk ? E(T.g) : nd}</td><td>{spendOk ? E(T.m) : nd}</td><td>{spendOk ? E(T.sp) : nd}</td><td className="c">{leadsOk ? T.ld : nd}</td><td>{spendOk && leadsOk && T.ld ? E(T.sp / T.ld) : nd}</td><td>{spendOk && quotedOk && T.nbq ? E(T.sp / T.nbq) : nd}</td><td>{spendOk && T.nbf ? E(T.sp / T.nbf) : nd}</td></tr></tbody></table>
  </div>;
}

/* ---------- application ---------- */
function App() {
  const [user, setUser] = useState(() => { try { return localStorage.getItem('stats_session') ? JSON.parse(localStorage.getItem('stats_user') || 'null') : null; } catch (e) { return null; } });
  const [preset, setPreset] = useState('week'); const [custom, setCustom] = useState(null);
  const [tab, setTab] = useState('synthese');
  const [summary, setSummary] = useState(null); const [sumErr, setSumErr] = useState(''); const cache = useRef({});
  const per = custom || periodOf(preset);
  useEffect(() => { if (!user) return; api('me').then(j => { localStorage.setItem('stats_session', j.session); setUser(j.user); }).catch(() => {}); }, []);
  useEffect(() => {
    if (!user) return; const key = per.ds + '_' + per.de; setSumErr('');
    if (cache.current[key]) { setSummary(cache.current[key]); return; }
    setSummary(null);
    api('summary', { date_start: per.ds, date_end: per.de }).then(j => { cache.current[key] = j; setSummary(j); }).catch(e => setSumErr(e.message));
  }, [user, per.ds, per.de]);
  if (!user) return <Login onOk={setUser} />;
  const logout = () => { localStorage.removeItem('stats_session'); localStorage.removeItem('stats_user'); setUser(null); };
  const shift = (n) => { const len = Math.round((parseD(per.de) - parseD(per.ds)) / 86400000) + 1; const ds = addDays(per.ds, n * len), de = addDays(per.de, n * len); setCustom({ ds, de, label: 'du ' + frDate(ds) + ' au ' + frDate(de) }); };
  const year = per.de.slice(0, 4);
  const TABS = [['synthese', 'Synthèse'], ['signed', 'Devis signés'], ['quoted', 'Devis émis'], ['invoiced', 'Facturé compta'], ['costs', 'Coûts compta'], ['pub', 'Pub'], ['leads', 'Leads'], ['perf', 'Commerciaux'], ['mois', 'Mois par mois']];
  return <>
    <div className="hdr"><div className="brand">Stats Verlaine<small>ventes · compta · pub · leads</small></div><div className="sp" /><div className="user">{user.email}</div><button onClick={logout}>Déconnexion</button></div>
    <div className="wrap">
      <div className="card" style={{ paddingBottom: 10 }}>
        <div className="bar">
          {[['yesterday', 'Hier'], ['today', 'Aujourd’hui'], ['week', 'Semaine en cours'], ['lastweek', 'Semaine dernière'], ['month', 'Mois en cours'], ['lastmonth', 'Mois dernier'], ['year', 'Année']].map(([k, l]) => <button key={k} className={'chip' + (!custom && preset === k ? ' on' : '')} onClick={() => { setCustom(null); setPreset(k); }}>{l}</button>)}
          <span className="dates">du <input type="date" value={per.ds} max={per.de} onChange={e => e.target.value && setCustom({ ds: e.target.value, de: per.de, label: 'du ' + frDate(e.target.value) + ' au ' + frDate(per.de) })} /> au <input type="date" value={per.de} min={per.ds} onChange={e => e.target.value && setCustom({ ds: per.ds, de: e.target.value, label: 'du ' + frDate(per.ds) + ' au ' + frDate(e.target.value) })} /></span>
          <button className="arrow" title="Période précédente" onClick={() => shift(-1)}>◀</button><button className="arrow" title="Période suivante" onClick={() => shift(1)}>▶</button>
        </div>
        <div className="period">Période : <b>{per.label}</b> · {tab === 'mois' ? 'photographie ' + year : 'toutes les données de la synthèse portent sur cette période'}</div>
      </div>
      <div className="nav">{TABS.map(([k, l]) => <button key={k} className={tab === k ? 'on' : ''} onClick={() => setTab(k)}>{l}</button>)}</div>
      {sumErr && tab === 'synthese' && <div className="err">Synthèse indisponible : {sumErr}</div>}
      {tab === 'synthese' && !sumErr && <Synthese ds={per.ds} de={per.de} label={per.label} data={summary} />}
      {tab === 'signed' && <Devis ds={per.ds} de={per.de} kind="signed" />}
      {tab === 'quoted' && <Devis ds={per.ds} de={per.de} kind="quoted" />}
      {tab === 'invoiced' && <Compta ds={per.ds} de={per.de} kind="invoiced" />}
      {tab === 'costs' && <Compta ds={per.ds} de={per.de} kind="costs" />}
      {tab === 'pub' && <Pub ds={per.ds} de={per.de} />}
      {tab === 'leads' && <Leads ds={per.ds} de={per.de} />}
      {tab === 'perf' && <Commerciaux ds={per.ds} de={per.de} />}
      {tab === 'mois' && <Mois year={+year} />}
      <div className="note" style={{ marginTop: 20 }}>Sources : CRM (devis, leads, tableau de bord commercial), PennyLane (grand livre 70x / 6xx, mandant AMG Façades, journal de situation exclu), Jarvis (Google Ads + Meta). n/d = donnée non disponible, jamais remplacée par 0.</div>
    </div>
  </>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);

})();
