// ─── AppelsFonds.jsx — Écran dédié appels de fonds (Phase 2, lecture seule) ───
// Pas de build, pas d'import/export — tout est global (React CDN).
// Données : GET /api/appels-eg (enrichi : montant_attendu, etat, programme_nom,
// travaux_reel). L'état "payé" (réconciliation) et la zone "À vérifier" arrivent
// dans un second temps. Les boutons d'action sont INERTES en Phase 2.

function AppelsFondsView({ C, projects, fiches, crm, rows, values, onLotStatutChange, lotsTravaux, lotsDevis }) {
  // lotsTravaux : { [programmeId]: { [lotId 1-based]: budget travaux } } calculé par App.jsx
  // (travaux réel = prixReel − foncierReel, repli sur travaux prévisionnels). Sert au
  // « reste à appeler » : la colonne lots.travaux_reel en base est vide.
  // fiches : état global { [programmeId]: fiche } — pour devisTravaux + surfaces (cohérence intake).
  const [appels, setAppels]   = React.useState([]);
  const [intake, setIntake]   = React.useState([]); // notifs entrantes à vérifier (EG + attestation)
  const [pdfModal, setPdfModal] = React.useState(null); // {id, label} → aperçu PDF agrandi en fenêtre
  const [loading, setLoading] = React.useState(true);
  const [error, setError]     = React.useState(null);
  const [prog, setProg]       = React.useState('');
  const [openLots, setOpenLots] = React.useState(new Set());
  const [openIntake, setOpenIntake] = React.useState(new Set()); // notifs intake dépliées (id)
  const [intakeBusy, setIntakeBusy] = React.useState(null);      // id de la notif en cours d'action
  const [editId, setEditId] = React.useState(null);              // id de la notif en correction manuelle
  const [editForm, setEditForm] = React.useState({});            // {programme_id, lot_idx, eg_global, eg_pct, eg_montant}
  const [validateNotif, setValidateNotif] = React.useState(null);// notif EG par lot en cours de validation (ouvre ConvertirAppelFondsModal)
  const [validateGlobalNotif, setValidateGlobalNotif] = React.useState(null);// notif EG global (ouvre ValiderEgGlobalModal)
  // ── Validation d'une attestation (C1) : réutilise les 3 modales de Notifications ──
  const [attestContextModal, setAttestContextModal] = React.useState(null); // {notif}
  const [attestArchiveModal, setAttestArchiveModal] = React.useState(null); // {notif}
  const [appelClientModal, setAppelClientModal] = React.useState(null);     // {notif, attestFolderPath}
  // ── Étape E : suivi paiement + relance client ──
  const [relanceModal, setRelanceModal] = React.useState(null);             // {appel}
  const [payBusy, setPayBusy] = React.useState(null);                       // id de l'appel en cours de "marquer payé"
  // Accordéon "Appels en cours" : on stocke les programmes REPLIÉS. Au 1er chargement
  // des données, on les replie TOUS (cf. seedCollapsedRef). Le toggle retire/ajoute l'id.
  const [collapsedProgs, setCollapsedProgs] = React.useState(new Set());
  const seedCollapsedRef = React.useRef(false); // garde-fou : ne replier d'office qu'une fois

  // ── États du workflow ──────────────────────────────────────────────────────
  const ETATS = {
    archi_relancer:   { label: 'Archi à relancer',          color: '#0C447C', bg: '#E6F1FB', dot: '#185FA5', action: 'Relancer' },
    attente_attest:   { label: 'Attente attestation',       color: '#633806', bg: '#FAEEDA', dot: '#BA7517', action: 'Relancer' },
    facture:          { label: 'Facture à envoyer',         color: '#26215C', bg: '#EEEDFE', dot: '#534AB7', action: 'Générer' },
    attente_paiement: { label: 'Envoyé — attente paiement', color: '#712B13', bg: '#FAECE7', dot: '#D85A30', action: 'Voir' },
    partiel:          { label: 'Partiellement payé',        color: '#633806', bg: '#FAEEDA', dot: '#BA7517', action: 'Voir' },
    paye:             { label: 'Payé',                       color: '#173404', bg: '#EAF3DE', dot: '#639922', action: 'Voir' },
    reste_a_appeler:  { label: 'Reste à appeler',            color: '#475569', bg: '#F1F5F9', dot: '#94A3B8', action: '—' },
  };
  const PRIO = { archi_relancer: 0, attente_attest: 1, facture: 2, attente_paiement: 3, partiel: 4, paye: 9 };
  // reste_a_appeler en dernier : c'est le budget travaux pas encore appelé (bucket synthétique du cumul)
  const BUCKET_ORDER = ['paye', 'attente_paiement', 'facture', 'attente_attest', 'archi_relancer', 'partiel', 'reste_a_appeler'];

  function em(e) { return ETATS[e] || { label: e || '?', color: C.muted, bg: C.bg, dot: C.muted, action: 'Voir' }; }

  // ── Chargement ─────────────────────────────────────────────────────────────
  async function load() {
    setLoading(true); setError(null);
    try {
      const [ra, rn] = await Promise.all([
        window.apiFetch('/api/appels-eg'),
        window.apiFetch('/api/agent/notifications?status=new&limit=200'),
      ]);
      if (!ra.ok) throw new Error('Erreur ' + ra.status);
      setAppels(await ra.json());
      // Zone "À vérifier" : notifs EG + attestation, status=new, non encore triées.
      if (rn.ok) {
        const notifs = await rn.json();
        setIntake(notifs.filter(n =>
          ['eg_appel_fonds', 'architecte_attestation'].includes(n.category) &&
          !(parseMeta(n).intake_status)
        ));
      } else { setIntake([]); }
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  }
  React.useEffect(() => { load(); }, []);

  // ── Helpers ────────────────────────────────────────────────────────────────
  function eur(n) {
    if (n == null || n === '') return '—';
    return Number(n).toLocaleString('fr-FR', { maximumFractionDigits: 0 }) + ' €';
  }
  function pct(n) {
    if (n == null || n === '') return '—';
    return (Math.round(Number(n) * 100) / 100) + ' %';
  }
  // Montant d'un appel : montant_client réel (stocké) en priorité, sinon estimé.
  function mont(a) {
    if (a.montant_client != null && a.montant_client !== '') return a.montant_client;
    return a.montant_attendu;
  }
  function fmtDate(iso) {
    if (!iso) return null;
    const d = new Date(iso);
    if (isNaN(d)) return null;
    return d.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: '2-digit' });
  }
  function progName(id) {
    const a = appels.find(x => x.programme_id === id && x.programme_nom);
    if (a) return a.programme_nom;
    const p = (projects || []).find(x => x.id === id);
    return (p && (p.nom || p.ville)) || id;
  }

  // ── Suivi paiement / relance (étape E) ──
  const RELANCE_JOURS = 15; // seuil de mise en avant « à relancer » (modifiable)
  function joursDepuis(iso) {
    if (!iso) return null;
    const d = new Date(iso);
    if (isNaN(d)) return null;
    return Math.floor((Date.now() - d.getTime()) / 86400000);
  }
  function isUnpaid(a) { return a.etat === 'attente_paiement' || a.etat === 'partiel'; }
  function isOverdue(a) {
    if (!isUnpaid(a)) return false;
    const j = joursDepuis(a.appel_client_date);
    return j != null && j >= RELANCE_JOURS;
  }
  async function markPaye(a) {
    if (payBusy) return;
    if (!window.confirm(`Confirmer que l'appel du Lot ${a.lot_id} (${eur(mont(a))}) est payé ?`)) return;
    setPayBusy(a.id);
    try {
      const r = await window.apiFetch('/api/appels-eg/' + a.id, {
        method: 'PATCH', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ paye: true }),
      });
      if (!r.ok) throw new Error('Erreur ' + r.status);
      await load();
    } catch (e) { alert(e.message || 'Impossible de marquer payé'); }
    finally { setPayBusy(null); }
  }

  // ── Intake (zone « À vérifier ») ─────────────────────────────────────────────
  // metadata des notifs : pg renvoie le JSONB en objet ; on parse défensivement.
  function parseMeta(n) {
    const m = n && n.metadata;
    if (!m) return {};
    if (typeof m === 'string') { try { return JSON.parse(m); } catch (e) { return {}; } }
    return m;
  }
  // programme résolu par le classifieur (null si non résolu → « à classer »).
  function notifProg(n) { return parseMeta(n).programme_id || null; }
  // lot 0-based (metadata partout en 0-based ; lot_id appel = lot_idx + 1).
  // null si ambigu (0 ou plusieurs lots) → rattaché au programme sans lot précis.
  function notifLotIdx0(n) {
    const m = parseMeta(n);
    if (m.lot_idx != null && m.lot_idx !== '') return Number(m.lot_idx);
    if (Array.isArray(m.lot_idxs) && m.lot_idxs.length === 1) return Number(m.lot_idxs[0]);
    return null;
  }
  function isEgNotif(n) { return n.category === 'eg_appel_fonds'; }
  // Marqueur d'avancement : on pose le % EXACT de l'attestation (statut AV<%>, ex. AV35),
  // plus d'arrondi au palier le plus proche (chantier « AVxx libres », 2026-06-18).

  const FLAG_MSG = {
    progression_regressive: 'Avancement ≤ cumul déjà appelé',
    depassement_devis: 'Cumul EG dépasse le devis travaux',
    ecart_prorata: 'Écart fort vs estimation au prorata des surfaces',
  };

  // Contrôle de cohérence d'une notif intake (null si programme/lot non résolus ;
  // {missing:true} si le % entrant manque). S'appuie sur les données déjà côté front.
  function coherence(n) {
    const m = parseMeta(n);
    const p = notifProg(n);
    const idx0 = notifLotIdx0(n);
    if (p == null || idx0 == null) return null;
    const lotId = String(idx0 + 1);
    const eg = isEgNotif(n);
    const egPct = eg ? Number(m.eg_pct ?? m.avancement_pct) : Number(m.avancement_pct);
    if (!isFinite(egPct)) return { missing: true };
    const travMap = (lotsTravaux && lotsTravaux[p]) || {};
    const travauxReel = travMap[lotId];
    const lotAppels = appels.filter(a => a.programme_id === p && String(a.lot_id) === lotId);
    const cumul = lotAppels.reduce((mx, a) => Math.max(mx, Number(a.pct_cumule) || 0), 0);
    const fiche = (fiches && fiches[p]) || {};
    const lots = fiche.lots || [];
    const surfaceLot = lots[idx0] ? toN(lots[idx0].surface) : null;
    const surfaceTotale = lots.reduce((s, l) => s + toN(l.surface), 0);
    const egMontant = eg ? toN(m.eg_montant) : null;
    const egMontantCumule = eg
      ? appels.filter(a => a.programme_id === p).reduce((s, a) => s + (toN(a.eg_montant) || 0), 0) + (toN(m.eg_montant) || 0)
      : null;
    const r = controleCoherenceEG({
      cumul, egPct, travauxReel,
      devisTravaux: fiche.devisTravaux,
      egMontantCumule, egMontant,
      surfaceLot, surfaceTotale,
    });
    return { ...r, cumul, egPct, lotId };
  }

  // « Ce qu'on attend » pour une notif EG : devis travaux (lot ou programme si
  // facture globale), prorata = %EG × devis, et montant déjà appelé par l'EG.
  // Base = DEVIS prévisionnel (côté EG), distincte du réel utilisé côté client.
  // Renvoie null si programme non résolu.
  function attendus(n) {
    const m = parseMeta(n);
    const eg = isEgNotif(n);
    const p = notifProg(n);
    const idx0 = notifLotIdx0(n);
    if (p == null) return null;
    const egPct = eg ? Number(m.eg_pct ?? m.avancement_pct) : Number(m.avancement_pct);
    // Global si marqué explicitement, ou si aucun lot précis n'est résolu.
    const isGlobal = m.eg_global === true || idx0 == null;
    const fiche = (fiches && fiches[p]) || {};
    let devis, dejaAppele, cumulPct;
    if (isGlobal) {
      devis = toN(fiche.devisTravaux);
      const progAppels = appels.filter(a => a.programme_id === p);
      dejaAppele = progAppels.reduce((s, a) => s + (toN(a.eg_montant) || 0), 0);
      cumulPct = progAppels.reduce((mx, a) => Math.max(mx, Number(a.pct_cumule) || 0), 0);
    } else {
      const lotId = String(idx0 + 1);
      devis = toN(((lotsDevis && lotsDevis[p]) || {})[lotId]);
      const lotAppels = appels.filter(a => a.programme_id === p && String(a.lot_id) === lotId);
      dejaAppele = lotAppels.reduce((s, a) => s + (toN(a.eg_montant) || 0), 0);
      cumulPct = lotAppels.reduce((mx, a) => Math.max(mx, Number(a.pct_cumule) || 0), 0);
    }
    // Prorata sur le DELTA (incrément de cet appel), pas sur le % cumulé.
    const deltaPct = isFinite(egPct) ? Math.max(0, egPct - cumulPct) : null;
    const prorata = (deltaPct != null && devis) ? Math.round((deltaPct / 100) * devis) : null;
    return { isGlobal, devis, prorata, dejaAppele, egPct, cumulPct, deltaPct };
  }

  function toggleIntake(id) {
    setOpenIntake(prev => { const s = new Set(prev); s.has(id) ? s.delete(id) : s.add(id); return s; });
  }

  // Triage d'une notif intake. Valider/Anomalie → marque metadata.intake_status
  // (status inchangé → reste actionnable dans Notifications). Rejeter → dismiss.
  // Corriger → relance l'extraction Haiku existante. Recharge à la fin.
  async function intakeAction(n, kind) {
    if (intakeBusy) return;
    // Valider un EG → ouvre la modale de conversion (crée l'appel + la facture à
    // régler + propose l'email archi). Rien n'est créé tant que la modale n'a pas
    // abouti. Global (eg_global ou pas de lot unique résolu) → modale multi-lots
    // (B2) ; sinon par-lot → ConvertirAppelFondsModal (B1). Les attestations
    // gardent le triage simple (marquage 'validé').
    if (kind === 'valider' && isEgNotif(n)) {
      const mm = parseMeta(n);
      const isGlobal = mm.eg_global === true || notifLotIdx0(n) == null;
      if (isGlobal) setValidateGlobalNotif(n); else setValidateNotif(n);
      return;
    }
    // Valider une ATTESTATION (C1) → enchaîne lecture PDF → archivage OneDrive →
    // appel client (modales réutilisées de Notifications), puis confirmation AVxx.
    if (kind === 'valider' && !isEgNotif(n)) { setAttestContextModal({ notif: n }); return; }
    if (kind === 'rejeter' && !window.confirm('Rejeter cette alerte ? Elle sera écartée (notifications comprises).')) return;
    // Requalifier en facture : l'élément a été classé « appel de fonds » à tort
    // (ex. facture de plus-value MOE). On force la catégorie 'facture' → il quitte
    // cet onglet et réapparaît dans « factures à régler » (onglet Notifications).
    if (kind === 'requalifier' && !window.confirm('Requalifier en facture ? Cet élément quittera l\'onglet « Appels de fonds » et apparaîtra dans vos factures à régler (onglet Notifications).')) return;
    setIntakeBusy(n.id);
    try {
      if (kind === 'valider') {
        await patchNotif(n.id, { action: 'intake_status', value: 'validé' });
      } else if (kind === 'rejeter') {
        await patchNotif(n.id, { action: 'dismiss' });
      } else if (kind === 'corriger') {
        const ep = isEgNotif(n) ? 'read-invoice' : 'read-attestation';
        const r = await window.apiFetch('/api/agent/notifications/' + n.id + '/' + ep, { method: 'POST' });
        if (!r.ok) throw new Error('Extraction : erreur ' + r.status);
      } else if (kind === 'requalifier') {
        await patchNotif(n.id, { action: 'force_category', category: 'facture' });
        setEditId(null);
      }
      await load();
    } catch (e) {
      alert(e.message || 'Action impossible');
    } finally {
      setIntakeBusy(null);
    }
  }
  async function patchNotif(id, body) {
    const r = await window.apiFetch('/api/agent/notifications/' + id, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error('Erreur ' + r.status);
    return r.json();
  }

  // Correction manuelle (étape A) : ouvre le formulaire pré-rempli depuis la notif.
  function openEdit(n) {
    const m = parseMeta(n);
    setEditForm({
      programme_id: m.programme_id || '',
      eg_global: m.eg_global === true || notifLotIdx0(n) == null,
      lot_idx: (m.lot_idx != null && m.lot_idx !== '') ? String(m.lot_idx) : '',
      eg_pct: isEgNotif(n) ? (m.eg_pct ?? '') : (m.avancement_pct ?? ''),
      eg_montant: m.eg_montant ?? '',
    });
    setEditId(n.id);
  }
  async function saveEdit(n) {
    if (intakeBusy) return;
    setIntakeBusy(n.id);
    try {
      await patchNotif(n.id, {
        action: 'correct_eg',
        programme_id: editForm.programme_id || null,
        eg_global: !!editForm.eg_global,
        lot_idx: editForm.eg_global ? null : (editForm.lot_idx === '' ? null : Number(editForm.lot_idx)),
        eg_pct: editForm.eg_pct === '' ? null : Number(editForm.eg_pct),
        eg_montant: editForm.eg_montant === '' ? null : Number(editForm.eg_montant),
      });
      setEditId(null);
      await load();
    } catch (e) { alert(e.message || 'Correction impossible'); }
    finally { setIntakeBusy(null); }
  }
  // Programmes proposés dans le dropdown de correction (hors global), triés alpha.
  const progOptions = (projects || []).filter(p => p && !p.isGlobal)
    .map(p => ({ id: p.id, nom: progName(p.id) }))
    .sort((a, b) => String(a.nom).localeCompare(String(b.nom), 'fr'));

  // ── Dérivés ────────────────────────────────────────────────────────────────
  const enCours = appels
    .filter(a => a.etat !== 'paye')
    .slice()
    .sort((a, b) => (PRIO[a.etat] ?? 8) - (PRIO[b.etat] ?? 8));

  const mAppelsEnCours = enCours.length;
  const mATraiter      = appels.filter(a => ['archi_relancer', 'attente_attest', 'facture'].includes(a.etat)).length;
  const mAttenteAttest = appels.filter(a => ['archi_relancer', 'attente_attest'].includes(a.etat)).length;
  const mEnAttPaie     = appels.filter(a => a.etat === 'attente_paiement').reduce((s, a) => s + (Number(mont(a)) || 0), 0);

  // Appels en cours regroupés par programme (accordéon). Lots triés 1, 2, 3…,
  // puis par ordre de création de l'appel à l'intérieur d'un lot. Programmes triés alpha.
  const enCoursByProg = React.useMemo(() => {
    const by = new Map();
    appels.filter(a => a.etat !== 'paye').forEach(a => {
      if (!by.has(a.programme_id)) by.set(a.programme_id, []);
      by.get(a.programme_id).push(a);
    });
    return [...by.entries()].map(([id, arr]) => ({
      id,
      nom: progName(id),
      total: arr.reduce((s, a) => s + (Number(mont(a)) || 0), 0),
      appels: arr.slice().sort((p, q) => {
        const lp = Number(p.lot_id) || 0, lq = Number(q.lot_id) || 0;
        if (lp !== lq) return lp - lq;
        return new Date(p.created_at || 0) - new Date(q.created_at || 0);
      }),
    })).sort((x, y) => String(x.nom).localeCompare(String(y.nom), 'fr'));
  }, [appels]);

  function toggleProg(id) {
    setCollapsedProgs(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
  }

  // Intake groupé : { [programmeId]: [notif] } pour les notifs avec programme résolu,
  // + repli "à classer" pour les notifs sans programme.
  const intakeByProg = React.useMemo(() => {
    const by = {};
    intake.forEach(n => { const p = notifProg(n); if (p) (by[p] = by[p] || []).push(n); });
    return by;
  }, [intake]);
  const intakeUnresolved = React.useMemo(() => intake.filter(n => !notifProg(n)), [intake]);
  const mAVerifier = intake.length;

  // Groupes de la section "Appels en cours" = union des programmes avec appels en
  // cours ET des programmes avec une notif intake résolue (un nouvel EG peut arriver
  // sur un programme sans appel en cours).
  const enCoursGroups = React.useMemo(() => {
    const byId = {};
    enCoursByProg.forEach(g => { byId[g.id] = g; });
    const ids = new Set([...enCoursByProg.map(g => g.id), ...Object.keys(intakeByProg)]);
    return [...ids].map(id => ({
      id,
      nom: byId[id] ? byId[id].nom : progName(id),
      appels: byId[id] ? byId[id].appels : [],
      total: byId[id] ? byId[id].total : 0,
      intake: intakeByProg[id] || [],
    })).sort((x, y) => String(x.nom).localeCompare(String(y.nom), 'fr'));
  }, [enCoursByProg, intakeByProg]);

  // Au tout premier rendu où des groupes existent : replier tous les accordéons.
  // Le ref garantit qu'on ne le fait qu'une fois → les rechargements (triage, Corriger,
  // SSE) ne re-replient jamais un programme que l'utilisateur a ouvert entre-temps.
  React.useEffect(() => {
    if (seedCollapsedRef.current || enCoursGroups.length === 0) return;
    seedCollapsedRef.current = true;
    setCollapsedProgs(new Set(enCoursGroups.map(g => g.id)));
  }, [enCoursGroups]);

  const programmes = React.useMemo(() => {
    const m = new Map();
    appels.forEach(a => { if (!m.has(a.programme_id)) m.set(a.programme_id, progName(a.programme_id)); });
    return [...m.entries()].sort((x, y) => String(x[1]).localeCompare(String(y[1]), 'fr'));
  }, [appels]);

  React.useEffect(() => {
    if (!prog && programmes.length) setProg(programmes[0][0]);
  }, [programmes, prog]);

  const progAppels = appels.filter(a => a.programme_id === prog);

  // Cumul par statut (programme sélectionné) + "reste à appeler".
  // Total affiché = budget travaux de TOUS les lots du programme (somme travaux_reel),
  // décomposé en : ce qui a été appelé (par statut) + le reste pas encore appelé.
  const cumul = React.useMemo(() => {
    const tot = {}, cnt = {}; let appele = 0;
    progAppels.forEach(a => {
      const m = Number(mont(a)) || 0;
      tot[a.etat] = (tot[a.etat] || 0) + m; cnt[a.etat] = (cnt[a.etat] || 0) + 1; appele += m;
    });
    // Budget travaux total = somme des travaux (réel, repli prévisionnel) de TOUS les
    // lots du programme — calculé par App.jsx (prop lotsTravaux), comme l'onglet programme.
    const travMap = (lotsTravaux && lotsTravaux[prog]) || {};
    const lotIds = Object.keys(travMap);
    const totalTravaux = lotIds.reduce((s, k) => s + (Number(travMap[k]) || 0), 0);
    const reste = Math.max(0, Math.round((totalTravaux - appele) * 100) / 100);
    if (reste > 0) { tot.reste_a_appeler = reste; cnt.reste_a_appeler = lotIds.length; }
    const grand = appele + reste; // = budget travaux total si appelé ≤ budget
    return { tot, cnt, grand };
  }, [prog, appels, lotsTravaux]);

  // Groupement par lot
  const lots = React.useMemo(() => {
    const by = {};
    progAppels.forEach(a => { const k = String(a.lot_id); (by[k] = by[k] || []).push(a); });
    return Object.keys(by)
      .sort((x, y) => (Number(x) || 0) - (Number(y) || 0))
      .map(k => ({
        lot: k,
        appels: by[k].slice().sort((p, q) => new Date(p.created_at || 0) - new Date(q.created_at || 0)),
        travaux: by[k][0] && by[k][0].travaux_reel,
        cumul: by[k][0] ? Number(by[k][0].pct_cumule) || 0 : 0,
      }));
  }, [prog, appels]);

  function toggleLot(k) {
    setOpenLots(prev => { const n = new Set(prev); n.has(k) ? n.delete(k) : n.add(k); return n; });
  }

  // ── Styles ─────────────────────────────────────────────────────────────────
  const card = { background: C.card, border: '1px solid ' + C.border, borderRadius: 12 };
  const th = { padding: '8px 12px', textAlign: 'left', fontSize: 11, fontWeight: 700, color: C.muted, borderBottom: '1px solid ' + C.border, whiteSpace: 'nowrap' };
  const td = { padding: '10px 12px', fontSize: 13, borderBottom: '1px solid ' + C.border, verticalAlign: 'middle' };
  const badge = (e) => ({ display: 'inline-block', background: em(e).bg, color: em(e).color, borderRadius: 6, padding: '3px 8px', fontSize: 11, fontWeight: 700 });
  const btnInert = { fontSize: 12, padding: '5px 10px', borderRadius: 6, border: '1px solid ' + C.border, background: C.card, color: C.muted, cursor: 'not-allowed', whiteSpace: 'nowrap' };
  const btnAct = { fontSize: 12, padding: '5px 10px', borderRadius: 6, border: '1px solid ' + C.border, background: C.card, color: C.text, cursor: 'pointer', whiteSpace: 'nowrap' };
  const metric = { background: C.bg, borderRadius: 8, padding: '14px 16px' };

  // ── Carte intake (« À vérifier ») ────────────────────────────────────────────
  function intakeLotLabel(n) {
    const idx0 = notifLotIdx0(n);
    if (idx0 != null) return 'Lot ' + (idx0 + 1);
    const m = parseMeta(n);
    if (Array.isArray(m.lot_idxs) && m.lot_idxs.length) return 'Lots ' + m.lot_idxs.map(i => Number(i) + 1).join(', ');
    return 'Lot ?';
  }


  function renderIntakeCard(n) {
    const open = openIntake.has(n.id);
    const eg = isEgNotif(n);
    const coh = coherence(n);
    const m = parseMeta(n);
    let voyant;
    if (!coh)              voyant = { bg: '#F1F5F9', color: '#475569', dot: '#94A3B8', label: 'à classer' };
    else if (coh.missing)  voyant = { bg: '#F1F5F9', color: '#475569', dot: '#94A3B8', label: 'donnée manquante' };
    else if (coh.flags.length) voyant = { bg: '#FAECE7', color: '#712B13', dot: '#D85A30', label: 'incohérent' };
    else                   voyant = { bg: '#EAF3DE', color: '#173404', dot: '#639922', label: 'cohérent' };
    const typeLabel = eg ? 'Nouvel appel — facture EG' : 'Attestation reçue';
    const idx0 = notifLotIdx0(n), p = notifProg(n);
    const lotId = idx0 != null ? String(idx0 + 1) : null;
    const hist = (p && lotId)
      ? appels.filter(a => a.programme_id === p && String(a.lot_id) === lotId).slice()
          .sort((x, y) => new Date(x.created_at || 0) - new Date(y.created_at || 0))
      : [];
    return (
      <div key={n.id} style={{ border: '1px solid ' + C.border, borderRadius: 8, marginBottom: 8, overflow: 'hidden', background: C.card }}>
        <div onClick={() => toggleIntake(n.id)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', cursor: 'pointer', background: '#FFFBEB' }}>
          <span style={{ fontSize: 14 }}>⚠️</span>
          <div style={{ fontWeight: 600, minWidth: 64 }}>{intakeLotLabel(n)}</div>
          <div style={{ fontSize: 12, color: C.muted, flex: 1 }}>{typeLabel}</div>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: voyant.bg, color: voyant.color, borderRadius: 6, padding: '3px 8px', fontSize: 11, fontWeight: 700 }}>
            <span style={{ width: 8, height: 8, borderRadius: '50%', background: voyant.dot }} />{voyant.label}
          </span>
          <span style={{ color: C.muted, fontSize: 13, width: 14, textAlign: 'right' }}>{open ? '▾' : '▸'}</span>
        </div>
        {open && (
          <div style={{ display: 'flex', gap: 12, padding: 12, borderTop: '1px solid ' + C.border, flexWrap: 'wrap' }}>
            {n.attachment_ref ? (
              <div style={{ flex: '1 1 320px', minWidth: 280, position: 'relative' }}>
                <button onClick={() => setPdfModal({ id: n.id, label: intakeLotLabel(n) })}
                  title="Agrandir le PDF en plein écran"
                  style={{ position: 'absolute', top: 6, right: 6, zIndex: 2, fontSize: 11, fontWeight: 600, padding: '4px 9px', borderRadius: 6, border: '1px solid ' + C.border, background: C.card, color: C.text, cursor: 'pointer', boxShadow: '0 1px 4px rgba(0,0,0,0.18)' }}>⛶ Agrandir</button>
                <iframe title="PDF" src={'/api/agent/notifications/' + n.id + '/attachment'}
                  style={{ width: '100%', height: 360, border: '1px solid ' + C.border, borderRadius: 6, background: '#fff', display: 'block' }} />
              </div>
            ) : (
              <div style={{ flex: '1 1 320px', minWidth: 280, height: 360, display: 'flex', alignItems: 'center', justifyContent: 'center', color: C.muted, border: '1px dashed ' + C.border, borderRadius: 6 }}>Pas de pièce jointe</div>
            )}
            <div style={{ flex: '1 1 280px', minWidth: 260, fontSize: 13 }}>
              <div style={{ fontSize: 11, color: C.muted, fontWeight: 700, marginBottom: 6 }}>EMAIL</div>
              <div style={{ marginBottom: 2 }}>{n.subject || '(sans objet)'}</div>
              <div style={{ color: C.muted, fontSize: 12, marginBottom: 12 }}>{n.from_name || n.from_email}</div>
              <div style={{ fontSize: 11, color: C.muted, fontWeight: 700, marginBottom: 6 }}>DONNÉES EXTRAITES</div>
              <div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '4px 12px', marginBottom: 12 }}>
                <span style={{ color: C.muted }}>{eg ? '% EG annoncé' : '% attestation'}</span>
                <span>{eg ? pct(m.eg_pct ?? m.avancement_pct) : pct(m.avancement_pct)}</span>
                {coh && !coh.missing && (<React.Fragment><span style={{ color: C.muted }}>Cumul déjà appelé</span><span>{pct(coh.cumul)}</span></React.Fragment>)}
                {coh && !coh.missing && (<React.Fragment><span style={{ color: C.muted }}>Delta de l'appel</span><span style={{ fontWeight: 600 }}>{pct(coh.delta)}</span></React.Fragment>)}
                {!eg && coh && !coh.missing && coh.montantClient != null && (<React.Fragment><span style={{ color: C.muted }}>Montant client dérivé</span><span style={{ fontWeight: 600 }}>{eur(coh.montantClient)}</span></React.Fragment>)}
                {eg && m.eg_montant != null && (<React.Fragment><span style={{ color: C.muted }}>Montant EG facturé</span><span>{eur(toN(m.eg_montant))}</span></React.Fragment>)}
              </div>
              {eg && (() => {
                const at = attendus(n);
                if (!at) return null;
                return (
                  <React.Fragment>
                    <div style={{ fontSize: 11, color: C.muted, fontWeight: 700, marginBottom: 6 }}>
                      CE QU'ON ATTEND {at.isGlobal ? '· facture globale (niveau programme)' : '· ce lot'}
                    </div>
                    <div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '4px 12px', marginBottom: 12 }}>
                      <span style={{ color: C.muted }}>Devis travaux</span><span>{eur(at.devis)}</span>
                      <span style={{ color: C.muted }}>Montant attendu (delta {at.deltaPct != null ? pct(at.deltaPct) : '—'} × devis)</span><span style={{ fontWeight: 600 }}>{at.prorata != null ? eur(at.prorata) : '—'}</span>
                      <span style={{ color: C.muted }}>Déjà appelé par l'EG</span><span>{eur(at.dejaAppele)}</span>
                    </div>
                  </React.Fragment>
                );
              })()}
              {coh && !coh.missing && coh.flags.length > 0 && (
                <div style={{ background: '#FAECE7', border: '1px solid #f3c5b5', borderRadius: 6, padding: '8px 10px', marginBottom: 12 }}>
                  {coh.flags.map(f => (<div key={f} style={{ fontSize: 12, color: '#712B13' }}>⚠ {FLAG_MSG[f] || f}</div>))}
                </div>
              )}
              {coh && !coh.missing && coh.flags.length === 0 && (
                <div style={{ fontSize: 12, color: '#173404', marginBottom: 12 }}>✓ Cohérent avec l'historique du lot.</div>
              )}
              {(!coh || coh.missing) && (
                <div style={{ fontSize: 12, color: C.muted, marginBottom: 12 }}>{!coh ? 'Programme/lot non résolus — à classer.' : 'Pourcentage non extrait — à corriger.'}</div>
              )}
              {(p && lotId) && (
                <div style={{ marginBottom: 12 }}>
                  <div style={{ fontSize: 11, color: C.muted, fontWeight: 700, marginBottom: 6 }}>HISTORIQUE DU LOT</div>
                  {hist.length === 0 ? (
                    <div style={{ fontSize: 12, color: C.muted }}>Aucun appel existant sur ce lot.</div>
                  ) : hist.map(a => (
                    <div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, padding: '2px 0' }}>
                      <span style={{ width: 8, height: 8, borderRadius: '50%', background: em(a.etat).dot, flex: 'none' }} />
                      <span style={{ width: 54 }}>+ {pct(a.pct_delta)}</span>
                      <span style={{ color: C.muted }}>{em(a.etat).label}</span>
                    </div>
                  ))}
                </div>
              )}
              <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                <button disabled={!!intakeBusy} onClick={() => intakeAction(n, 'valider')}
                  style={{ fontSize: 12, padding: '5px 10px', borderRadius: 6, border: '1px solid #bfe0a0', background: '#EAF3DE', color: '#173404', fontWeight: 600, cursor: intakeBusy ? 'wait' : 'pointer', opacity: intakeBusy && intakeBusy !== n.id ? 0.5 : 1 }}>Valider</button>
                <button disabled={!!intakeBusy} onClick={() => (editId === n.id ? setEditId(null) : openEdit(n))}
                  style={{ fontSize: 12, padding: '5px 10px', borderRadius: 6, border: '1px solid ' + C.border, background: editId === n.id ? C.bg : C.card, color: C.text, cursor: 'pointer', opacity: intakeBusy && intakeBusy !== n.id ? 0.5 : 1 }}>✎ Modifier</button>
                <button disabled={!!intakeBusy} onClick={() => intakeAction(n, 'rejeter')}
                  style={{ fontSize: 12, padding: '5px 10px', borderRadius: 6, border: '1px solid ' + C.border, background: C.card, color: C.muted, cursor: intakeBusy ? 'wait' : 'pointer', opacity: intakeBusy && intakeBusy !== n.id ? 0.5 : 1 }}>Rejeter</button>
              </div>
              {editId === n.id && (() => {
                const editInp = { background: C.card, border: '1px solid ' + C.border, borderRadius: 6, padding: '4px 6px', fontSize: 12, color: C.text, width: '100%' };
                const roInp = { ...editInp, background: C.bg, color: C.muted, fontStyle: 'italic' };
                const selLots = ((fiches && fiches[editForm.programme_id] && fiches[editForm.programme_id].lots) || []);
                // Delta = avancement total saisi − cumul déjà appelé sur le lot (ou le
                // programme entier si facture globale). Lecture seule : valeur dérivée,
                // recalculée quand le total / le lot changent.
                const selLotId = editForm.eg_global ? null : (editForm.lot_idx === '' ? null : String(Number(editForm.lot_idx) + 1));
                const cumulSel = appels
                  .filter(a => a.programme_id === editForm.programme_id && (editForm.eg_global ? true : String(a.lot_id) === selLotId))
                  .reduce((mx, a) => Math.max(mx, Number(a.pct_cumule) || 0), 0);
                const totalPctNum = editForm.eg_pct === '' ? null : Number(editForm.eg_pct);
                const deltaPctVal = (totalPctNum == null || !isFinite(totalPctNum)) ? null : Math.max(0, totalPctNum - cumulSel);
                return (
                  <div style={{ marginTop: 10, padding: 12, border: '1px solid ' + C.border, borderRadius: 8, background: C.bg }}>
                    <div style={{ fontSize: 11, color: C.muted, fontWeight: 700, marginBottom: 8 }}>MODIFIER</div>
                    <div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '8px 10px', alignItems: 'center', fontSize: 12 }}>
                      <span style={{ color: C.muted }}>Programme</span>
                      <select value={editForm.programme_id} onChange={e => setEditForm(f => ({ ...f, programme_id: e.target.value }))} style={editInp}>
                        <option value="">— choisir —</option>
                        {progOptions.map(o => <option key={o.id} value={o.id}>{o.nom}</option>)}
                      </select>
                      <span style={{ color: C.muted }}>Type</span>
                      <label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                        <input type="checkbox" checked={!!editForm.eg_global} onChange={e => setEditForm(f => ({ ...f, eg_global: e.target.checked }))} />
                        Facture globale (tout le programme)
                      </label>
                      {!editForm.eg_global && (
                        <React.Fragment>
                          <span style={{ color: C.muted }}>Lot</span>
                          <select value={editForm.lot_idx} onChange={e => setEditForm(f => ({ ...f, lot_idx: e.target.value }))} style={editInp}>
                            <option value="">— choisir —</option>
                            {selLots.map((l, i) => <option key={i} value={i}>Lot {i + 1}</option>)}
                          </select>
                        </React.Fragment>
                      )}
                      <span style={{ color: C.muted }}>% d'avancement total</span>
                      <input type="number" step="0.01" value={editForm.eg_pct} onChange={e => setEditForm(f => ({ ...f, eg_pct: e.target.value }))} style={editInp} />
                      <span style={{ color: C.muted }}>Delta % d'avancement <span style={{ fontSize: 10 }}>(calculé)</span></span>
                      <input type="text" readOnly value={deltaPctVal == null ? '—' : pct(deltaPctVal)} title="Calculé : avancement total − ce qui a déjà été appelé" style={roInp} />
                      <span style={{ color: C.muted }}>Montant de la facture (€)</span>
                      <input type="number" step="0.01" value={editForm.eg_montant} onChange={e => setEditForm(f => ({ ...f, eg_montant: e.target.value }))} style={editInp} />
                    </div>
                    <div style={{ display: 'flex', gap: 6, marginTop: 12 }}>
                      <button disabled={!!intakeBusy} onClick={() => saveEdit(n)}
                        style={{ fontSize: 12, padding: '5px 12px', borderRadius: 6, border: '1px solid #bfe0a0', background: '#EAF3DE', color: '#173404', fontWeight: 600, cursor: intakeBusy ? 'wait' : 'pointer' }}>{intakeBusy === n.id ? '…' : 'Enregistrer'}</button>
                      <button disabled={!!intakeBusy} onClick={() => setEditId(null)}
                        style={{ fontSize: 12, padding: '5px 12px', borderRadius: 6, border: '1px solid ' + C.border, background: C.card, color: C.muted, cursor: 'pointer' }}>Annuler</button>
                    </div>
                    <div style={{ display: 'flex', gap: 6, marginTop: 12, paddingTop: 10, borderTop: '1px dashed ' + C.border, flexWrap: 'wrap', alignItems: 'center' }}>
                      <span style={{ fontSize: 10, color: C.muted, fontWeight: 700, whiteSpace: 'nowrap' }}>AUTRES&nbsp;:</span>
                      <button disabled={!!intakeBusy} onClick={() => intakeAction(n, 'corriger')}
                        title="Relancer la lecture automatique du PDF (recalcule % et montant)"
                        style={{ fontSize: 12, padding: '5px 10px', borderRadius: 6, border: '1px solid ' + C.border, background: C.card, color: C.text, cursor: intakeBusy ? 'wait' : 'pointer', opacity: intakeBusy && intakeBusy !== n.id ? 0.5 : 1 }}>{intakeBusy === n.id ? '…' : '🔄 Relancer la lecture automatique'}</button>
                      <button disabled={!!intakeBusy} onClick={() => intakeAction(n, 'requalifier')}
                        title="Cet élément n'est pas un appel de fonds (ex. facture de plus-value) → le déplacer vers les factures à régler"
                        style={{ fontSize: 12, padding: '5px 10px', borderRadius: 6, border: '1px solid #c9b3e8', background: '#F0EAF8', color: '#5b2a91', cursor: intakeBusy ? 'wait' : 'pointer', opacity: intakeBusy && intakeBusy !== n.id ? 0.5 : 1 }}>↦ Requalifier en facture</button>
                    </div>
                  </div>
                );
              })()}
            </div>
          </div>
        )}
      </div>
    );
  }

  // ── Render ──────────────────────────────────────────────────────────────────
  return (
    <div style={{ padding: '20px 24px', maxWidth: 1100, margin: '0 auto' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
        <h2 style={{ margin: 0, fontSize: 18, fontWeight: 700 }}>💶 Appels de fonds</h2>
        <button onClick={load} style={{ background: C.accent, color: '#fff', border: 'none', borderRadius: 7, padding: '7px 16px', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
          ↻ Actualiser
        </button>
      </div>

      {loading && <div style={{ textAlign: 'center', padding: 48, color: C.muted }}>Chargement…</div>}
      {error && <div style={{ background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 8, padding: '12px 16px', color: '#dc2626', fontSize: 13 }}>Erreur : {error}</div>}

      {!loading && !error && (
        <React.Fragment>
          {/* ── Indicateurs ── */}
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12, marginBottom: 24 }}>
            <div style={{ ...metric, background: mAVerifier ? '#FAECE7' : C.bg }}><div style={{ fontSize: 12, color: mAVerifier ? '#712B13' : C.muted, marginBottom: 4 }}>À vérifier</div><div style={{ fontSize: 22, fontWeight: 700, color: mAVerifier ? '#D85A30' : C.text }}>{mAVerifier}</div></div>
            <div style={metric}><div style={{ fontSize: 12, color: C.muted, marginBottom: 4 }}>Appels en cours</div><div style={{ fontSize: 22, fontWeight: 700 }}>{mAppelsEnCours}</div></div>
            <div style={metric}><div style={{ fontSize: 12, color: C.muted, marginBottom: 4 }}>À traiter maintenant</div><div style={{ fontSize: 22, fontWeight: 700 }}>{mATraiter}</div></div>
            <div style={metric}><div style={{ fontSize: 12, color: C.muted, marginBottom: 4 }}>Attente attestation</div><div style={{ fontSize: 22, fontWeight: 700 }}>{mAttenteAttest}</div></div>
            <div style={metric}><div style={{ fontSize: 12, color: C.muted, marginBottom: 4 }}>En attente de paiement</div><div style={{ fontSize: 22, fontWeight: 700 }}>{eur(mEnAttPaie)}</div></div>
          </div>

          {/* ── À classer (intake sans programme résolu) ── */}
          {intakeUnresolved.length > 0 && (
            <div style={{ marginBottom: 18 }}>
              <div style={{ fontSize: 16, fontWeight: 700, margin: '0 0 10px' }}>À classer <span style={{ fontSize: 12, fontWeight: 400, color: C.muted }}>· programme/lot non résolu</span></div>
              {intakeUnresolved.map(n => renderIntakeCard(n))}
            </div>
          )}

          {/* ── Dashboard appels en cours (accordéon par programme) + intake rattaché ── */}
          <div style={{ fontSize: 16, fontWeight: 700, margin: '0 0 10px' }}>Appels en cours</div>
          {enCoursGroups.length === 0 ? (
            <div style={{ ...card, padding: 32, textAlign: 'center', color: C.muted, fontSize: 14, marginBottom: 28 }}>Aucun appel en cours.</div>
          ) : (
            <div style={{ marginBottom: 28 }}>
              {enCoursGroups.map(G => {
                const open = !collapsedProgs.has(G.id);
                const nbIntake = G.intake.length;
                return (
                  <div key={G.id} style={{ ...card, overflow: 'hidden', marginBottom: 10 }}>
                    <div onClick={() => toggleProg(G.id)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', cursor: 'pointer', background: C.bg }}>
                      <span style={{ color: C.muted, fontSize: 13, width: 14 }}>{open ? '▾' : '▸'}</span>
                      <div style={{ fontWeight: 700, flex: 1 }}>{G.nom}</div>
                      {nbIntake > 0 && (
                        <span style={{ background: '#FAECE7', color: '#D85A30', borderRadius: 6, padding: '2px 8px', fontSize: 11, fontWeight: 700 }}>⚠️ {nbIntake} à vérifier</span>
                      )}
                      <div style={{ fontSize: 12, color: C.muted }}>{G.appels.length} appel{G.appels.length > 1 ? 's' : ''} · {eur(G.total)}</div>
                    </div>
                    {open && (
                      <React.Fragment>
                        {nbIntake > 0 && (
                          <div style={{ padding: '10px 12px', borderTop: '1px solid ' + C.border, background: '#FFFEF8' }}>
                            <div style={{ fontSize: 11, color: '#B45309', fontWeight: 700, marginBottom: 8 }}>À VÉRIFIER</div>
                            {G.intake.map(n => renderIntakeCard(n))}
                          </div>
                        )}
                        {G.appels.length > 0 && (
                          <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                            <thead><tr style={{ background: C.card }}>
                              <th style={th}>Lot</th>
                              <th style={th}>Appel</th>
                              <th style={{ ...th, textAlign: 'right' }}>Montant attendu</th>
                              <th style={th}>État</th>
                              <th style={th}></th>
                            </tr></thead>
                            <tbody>
                              {G.appels.map(a => (
                                <tr key={a.id}>
                                  <td style={td}><span style={{ fontWeight: 600 }}>Lot {a.lot_id}</span></td>
                                  <td style={td}>
                                    <div style={{ fontWeight: 600 }}>+ {pct(a.pct_delta)}</div>
                                    <div style={{ fontSize: 11, color: C.muted }}>cumulé {pct(a.pct_cumule)}</div>
                                  </td>
                                  <td style={{ ...td, textAlign: 'right' }}>{eur(mont(a))}</td>
                                  <td style={td}>
                                    <span style={badge(a.etat)}>{em(a.etat).label}</span>
                                    {isOverdue(a) && (
                                      <div style={{ fontSize: 10, color: '#b91c1c', fontWeight: 700, marginTop: 3 }}>⚠ à relancer ({joursDepuis(a.appel_client_date)} j)</div>
                                    )}
                                    {a.relance_client_date && (
                                      <div style={{ fontSize: 10, color: C.muted, marginTop: 2 }}>relancé le {fmtDate(a.relance_client_date)}</div>
                                    )}
                                  </td>
                                  <td style={td}>
                                    {isUnpaid(a) ? (
                                      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                                        <button onClick={() => setRelanceModal({ appel: a })}
                                          style={{ ...btnAct, ...(isOverdue(a) ? { borderColor: '#f87171', background: '#fef2f2', color: '#b91c1c', fontWeight: 700 } : {}) }}>
                                          {isOverdue(a) ? '⚠ Relancer' : 'Relancer'}
                                        </button>
                                        <button disabled={payBusy === a.id} onClick={() => markPaye(a)}
                                          style={{ ...btnAct, opacity: payBusy === a.id ? 0.5 : 1 }}>
                                          {payBusy === a.id ? '…' : 'Marquer payé'}
                                        </button>
                                      </div>
                                    ) : (
                                      <button style={btnInert} title="—">{em(a.etat).action}</button>
                                    )}
                                  </td>
                                </tr>
                              ))}
                            </tbody>
                          </table>
                        )}
                      </React.Fragment>
                    )}
                  </div>
                );
              })}
            </div>
          )}

          {/* ── Détail par programme ── */}
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', margin: '0 0 12px' }}>
            <div style={{ fontSize: 16, fontWeight: 700 }}>Détail par programme</div>
            <select value={prog} onChange={e => setProg(e.target.value)}
              style={{ padding: '6px 10px', borderRadius: 6, border: '1px solid ' + C.border, background: C.card, color: C.text, fontSize: 13, minWidth: 200 }}>
              {programmes.map(([id, nom]) => <option key={id} value={id}>{nom}</option>)}
            </select>
          </div>

          {progAppels.length === 0 ? (
            <div style={{ ...card, padding: 28, textAlign: 'center', color: C.muted, fontSize: 14 }}>Aucun appel pour ce programme.</div>
          ) : (
            <React.Fragment>
              {/* Cumul par statut */}
              <div style={{ ...card, padding: '14px 18px', marginBottom: 14 }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 10 }}>
                  <span style={{ fontSize: 13, color: C.muted }}>Cumul des appels par statut</span>
                  <span style={{ fontSize: 13 }}>Total <b>{eur(cumul.grand)}</b></span>
                </div>
                <div style={{ height: 14, borderRadius: 7, overflow: 'hidden', display: 'flex', background: C.bg }}>
                  {BUCKET_ORDER.filter(k => cumul.tot[k]).map(k => (
                    <span key={k} title={em(k).label + ' : ' + eur(cumul.tot[k])}
                      style={{ width: (cumul.grand ? (cumul.tot[k] / cumul.grand * 100) : 0) + '%', background: em(k).dot, height: '100%' }} />
                  ))}
                </div>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: 8, marginTop: 12 }}>
                  {BUCKET_ORDER.filter(k => cumul.cnt[k]).map(k => (
                    <div key={k} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', background: C.bg, borderRadius: 6 }}>
                      <span style={{ width: 9, height: 9, borderRadius: '50%', background: em(k).dot, flex: 'none' }} />
                      <div style={{ flex: 1 }}>
                        <div style={{ fontSize: 12, color: C.muted }}>{em(k).label}</div>
                        <div style={{ fontWeight: 600 }}>{eur(cumul.tot[k])} <span style={{ fontWeight: 400, color: C.muted, fontSize: 12 }}>· {cumul.cnt[k]} {k === 'reste_a_appeler' ? ('lot' + (cumul.cnt[k] > 1 ? 's' : '')) : ('appel' + (cumul.cnt[k] > 1 ? 's' : ''))}</span></div>
                      </div>
                    </div>
                  ))}
                </div>
              </div>

              {/* Drill-down lots */}
              {lots.map(L => {
                const open = openLots.has(L.lot);
                return (
                  <div key={L.lot} style={{ ...card, marginBottom: 10, overflow: 'hidden' }}>
                    <div onClick={() => toggleLot(L.lot)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', cursor: 'pointer' }}>
                      <span style={{ color: C.muted, fontSize: 13, width: 14 }}>{open ? '▾' : '▸'}</span>
                      <div style={{ fontWeight: 600, minWidth: 54 }}>Lot {L.lot}</div>
                      <div style={{ fontSize: 12, color: C.muted }}>travaux {eur(L.travaux)}</div>
                      <div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'flex-end' }}>
                        <div style={{ width: 120, height: 6, borderRadius: 6, background: C.bg, overflow: 'hidden' }}>
                          <span style={{ display: 'block', height: '100%', width: Math.min(100, L.cumul) + '%', background: '#639922' }} />
                        </div>
                        <span style={{ fontSize: 12, color: C.muted, minWidth: 40 }}>cumulé {L.cumul} %</span>
                      </div>
                    </div>
                    {open && (
                      <div>
                        {L.appels.map(a => {
                          const d = fmtDate(a.appel_client_date) || fmtDate(a.attestation_date) || fmtDate(a.eg_date);
                          return (
                            <div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 14px', borderTop: '1px solid ' + C.border }}>
                              <span style={{ width: 9, height: 9, borderRadius: '50%', background: em(a.etat).dot, flex: 'none' }} />
                              <div style={{ width: 78, fontWeight: 600 }}>+ {pct(a.pct_delta)}</div>
                              <div style={{ width: 100, textAlign: 'right', fontWeight: 600 }}>{eur(mont(a))}</div>
                              <div style={{ flex: 1 }}><span style={badge(a.etat)}>{em(a.etat).label}</span></div>
                              <div style={{ fontSize: 12, color: C.muted, minWidth: 70, textAlign: 'right' }}>{d || ''}</div>
                            </div>
                          );
                        })}
                      </div>
                    )}
                  </div>
                );
              })}
            </React.Fragment>
          )}

          <p style={{ fontSize: 11, color: C.muted, marginTop: 18 }}>
            L'état « payé » est dérivé de la réconciliation. Valider un EG « à vérifier » crée l'appel + la facture à régler et propose l'email à l'architecte.
          </p>
        </React.Fragment>
      )}

      {/* ── Validation d'un EG : réutilise la modale de Notifications ──────────────
          Étape 1 : crée l'appel + la facture à régler (→ Notifications).
          Étape 2 : email archi pré-rempli, envoyé à la main.
          À l'aboutissement (onDone) on marque la notif 'validé' → la carte quitte
          « à vérifier ». onClose (annulation) ne marque rien ; si l'appel a déjà été
          créé puis la modale fermée par le fond, la carte reste (rappel à finir). */}
      {validateNotif && (
        <ConvertirAppelFondsModal
          notif={validateNotif}
          projects={projects}
          fiches={fiches}
          crm={crm}
          onClose={() => { setValidateNotif(null); load(); }}
          onDone={async () => {
            const id = validateNotif.id;
            setValidateNotif(null);
            try { await patchNotif(id, { action: 'intake_status', value: 'validé' }); } catch (e) { /* non bloquant */ }
            await load();
          }}
        />
      )}

      {validateGlobalNotif && (
        <ValiderEgGlobalModal
          notif={validateGlobalNotif}
          projects={projects}
          fiches={fiches}
          crm={crm}
          appels={appels}
          lotsDevis={lotsDevis}
          lotsTravaux={lotsTravaux}
          C={C}
          onClose={() => { setValidateGlobalNotif(null); load(); }}
          onDone={async () => {
            const id = validateGlobalNotif.id;
            setValidateGlobalNotif(null);
            try { await patchNotif(id, { action: 'intake_status', value: 'validé' }); } catch (e) { /* non bloquant */ }
            await load();
          }}
        />
      )}

      {/* ── Validation d'une attestation (C1) : 3 modales réutilisées de Notifications,
          puis confirmation du marqueur AVxx. ──────────────────────────────────── */}
      {attestContextModal && (
        <AttestationContextModal
          notif={attestContextModal.notif}
          projects={projects}
          fiches={fiches}
          onConfirm={(enrichedNotif) => { setAttestContextModal(null); setAttestArchiveModal({ notif: enrichedNotif }); }}
          onCancel={() => setAttestContextModal(null)}
        />
      )}
      {attestArchiveModal && (
        <AttestationArchiveModal
          notif={attestArchiveModal.notif}
          onArchived={(notif, shareUrl, folderPath) => { setAttestArchiveModal(null); setAppelClientModal({ notif, attestFolderPath: folderPath || null }); }}
          onCancel={() => setAttestArchiveModal(null)}
        />
      )}
      {appelClientModal && (
        <AppelClientEgModal
          notif={appelClientModal.notif}
          attestFolderPath={appelClientModal.attestFolderPath || null}
          projects={projects}
          fiches={fiches}
          crm={crm}
          rows={rows}
          values={values}
          onClose={() => setAppelClientModal(null)}
          onDone={async () => {
            const nid = appelClientModal.notif.id;
            setAppelClientModal(null);
            try { await patchNotif(nid, { action: 'intake_status', value: 'validé' }); } catch (e) { /* non bloquant */ }
            // L'avancement (% des travaux) se reflète tout seul via le cumul des appels —
            // on ne marque plus le lot en AVxx (l'avancement n'est plus un statut commercial).
            await load();
          }}
        />
      )}

      {relanceModal && (
        <RelanceClientModal
          appel={relanceModal.appel}
          projects={projects}
          fiches={fiches}
          crm={crm}
          C={C}
          onClose={() => setRelanceModal(null)}
          onSent={async () => { setRelanceModal(null); await load(); }}
        />
      )}

      {/* Aperçu PDF agrandi (plein écran) */}
      {pdfModal && (
        <div onClick={() => setPdfModal(null)}
          style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(15,23,42,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
          <div onClick={e => e.stopPropagation()}
            style={{ background: C.card, borderRadius: 12, width: 'min(1000px, 96vw)', height: '92vh', display: 'flex', flexDirection: 'column', overflow: 'hidden', boxShadow: '0 24px 60px rgba(0,0,0,0.4)' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px', borderBottom: '1px solid ' + C.border }}>
              <div style={{ fontWeight: 700, fontSize: 14 }}>Aperçu PDF — {pdfModal.label}</div>
              <a href={'/api/agent/notifications/' + pdfModal.id + '/attachment'} target="_blank" rel="noopener"
                style={{ fontSize: 12, color: C.accent }}>↗ Ouvrir dans un onglet</a>
              <button onClick={() => setPdfModal(null)}
                style={{ marginLeft: 'auto', fontSize: 13, padding: '5px 12px', borderRadius: 6, border: '1px solid ' + C.border, background: C.bg, color: C.text, cursor: 'pointer' }}>✕ Fermer</button>
            </div>
            <iframe title="PDF agrandi" src={'/api/agent/notifications/' + pdfModal.id + '/attachment'}
              style={{ flex: 1, width: '100%', border: 'none', background: '#fff' }} />
          </div>
        </div>
      )}
    </div>
  );
}

/* ── Modale : valider un EG GLOBAL (1 facture → N appels) ──────────────────────
   Cas où l'EG couvre tout le programme. On liste les lots vendus (pré-cochés,
   décochables), on crée 1 appel par lot coché (même %, delta par lot) et UNE
   SEULE facture à régler (eg_notif_id transmis au seul 1er appel). Puis 1 email
   archi unique listant les lots. Aucun changement backend : réutilise POST
   /api/appels-eg appelé N fois. Cf. PROCESSUS_METIER.md étape B (B2). */
function ValiderEgGlobalModal({ notif, projects, fiches, crm, appels, lotsDevis, lotsTravaux, C, onClose, onDone }) {
  const parseM = (n) => {
    const m = n && n.metadata;
    if (!m) return {};
    if (typeof m === 'string') { try { return JSON.parse(m); } catch (e) { return {}; } }
    return m;
  };
  const meta = parseM(notif);
  const action = notif.proposed_action || {};

  // Lots concernés par un EG : vendus ou en cours d'avancement (AVxx, palier libre inclus).
  const eur = (n) => (n == null || n === '') ? '—' : Number(n).toLocaleString('fr-FR', { maximumFractionDigits: 0 }) + ' €';
  const pctFmt = (n) => (n == null || n === '' || !isFinite(Number(n))) ? '—' : (Math.round(Number(n) * 100) / 100) + ' %';

  const [step, setStep] = React.useState(1);
  const [projId, setProjId] = React.useState(meta.programme_id || action?.params?.programmeId || '');
  const [egPct, setEgPct] = React.useState(String(meta.eg_pct || meta.avancement_pct || meta.pct || ''));
  const [egMontant] = React.useState(meta.eg_montant || meta.montant || meta.montant_ttc || '');
  const [selected, setSelected] = React.useState(() => new Set());
  const [saving, setSaving] = React.useState(false);
  const [err, setErr] = React.useState('');
  const [createdAppels, setCreatedAppels] = React.useState([]);
  const [factureNotifId, setFactureNotifId] = React.useState(null);

  const [archSending, setArchSending] = React.useState(false);
  const [archSent, setArchSent] = React.useState(false);
  const [archErr, setArchErr] = React.useState('');
  const [egAttachment, setEgAttachment] = React.useState(null);
  const [loadingAttach, setLoadingAttach] = React.useState(false);

  const fiche = (fiches && fiches[projId]) || {};
  const soldLots = React.useMemo(() => (
    (fiche.lots || []).map((l, i) => ({ idx: i, lot: l }))
      .filter(({ lot }) => isLotVenduOuAvance(lot.statutCommercial || ''))
  ), [fiche]);

  const lotCumul = (idx) => {
    const lotId = String(idx + 1);
    return (appels || []).filter(a => a.programme_id === projId && String(a.lot_id) === lotId)
      .reduce((mx, a) => Math.max(mx, Number(a.pct_cumule) || 0), 0);
  };
  const lotDevisOf = (idx) => toN(((lotsDevis && lotsDevis[projId]) || {})[String(idx + 1)]);
  const lotTravauxOf = (idx) => toN(((lotsTravaux && lotsTravaux[projId]) || {})[String(idx + 1)]);

  // Pré-cochage : lots vendus avec delta > 0, une fois par programme choisi.
  const seededRef = React.useRef(null);
  React.useEffect(() => {
    if (!projId || seededRef.current === projId) return;
    seededRef.current = projId;
    const pct = parseFloat(egPct);
    const s = new Set();
    soldLots.forEach(({ idx }) => {
      const delta = isFinite(pct) ? pct - lotCumul(idx) : 0;
      if (delta > 0) s.add(idx);
    });
    setSelected(s);
  }, [projId, soldLots]);

  const toggleLot = (idx) => setSelected(prev => {
    const s = new Set(prev); s.has(idx) ? s.delete(idx) : s.add(idx); return s;
  });

  // Architecte depuis CRM (même résolution que ConvertirAppelFondsModal).
  const architecte = React.useMemo(() => {
    try {
      if (!crm) return null;
      for (const e of (crm.entreprises || [])) {
        if ((e.activite || '').toLowerCase() === 'architecte' || (e.nom || '').toLowerCase().includes('karoubi')) {
          const email = String(e.emails || e.email || '').split(/[,;\s]/)[0].trim();
          return { nom: e.nom, email };
        }
        for (const c of (e.contacts || [])) {
          if ((c.nom || '').toLowerCase().includes('karoubi') || (c.prenom || '').toLowerCase().includes('yael')) {
            return { nom: `${c.prenom || ''} ${c.nom || ''}`.trim(), email: c.email || '' };
          }
        }
      }
      return null;
    } catch (e) { return null; }
  }, [crm]);

  const proj = (projects || []).find(p => p.id === projId) || {};
  const progNom = [proj.ville, proj.nom].filter(Boolean).join(' ') || fiche.adresse || '';

  const emailArchBody = React.useMemo(() => {
    if (step !== 2 || createdAppels.length === 0) return '';
    const archNom = architecte?.nom || 'Madame Karoubi';
    const pct = parseFloat(egPct) || 0;
    const lignes = createdAppels.map(({ lotIdx }) => {
      const lot = (fiche.lots || [])[lotIdx] || {};
      return `  • Lot ${lotIdx + 1}${lot.type ? ` (${lot.type})` : ''} — ${lot.clientNom || '[Client]'}`;
    }).join('\n');
    const ville = String(proj.ville || proj.nom || 'PROG').replace(/\s+/g, '-').toUpperCase().slice(0, 15);
    const ref = `${ville}-AV${pct}-GLOBAL`;
    return `Objet : Demande attestation avancement ${pct}% — ${progNom} [Réf: ${ref}]

${archNom},

Dans le cadre du programme ${progNom}, nous avons reçu la facture de l'entreprise générale correspondant à un avancement de ${pct}%. Vous trouverez cette facture en pièce jointe.

Nous vous prions de bien vouloir nous faire parvenir votre attestation d'avancement des travaux correspondante afin de procéder aux appels de fonds clients.

LOTS CONCERNÉS :
─────────────────────────────────────────
${lignes}
─────────────────────────────────────────
  Avancement  : ${pct}%

Nous vous remercions de votre diligence.

Cordialement,`;
  }, [step, createdAppels, projId, egPct, architecte, fiche, proj, progNom]);

  const loadEgAttachment = React.useCallback(async () => {
    setLoadingAttach(true);
    try {
      const resp = await window.apiFetch(`/api/agent/notifications/${notif.id}/attachment`);
      if (!resp.ok) { setLoadingAttach(false); return; }
      const blob = await resp.blob();
      const base64 = await new Promise(resolve => {
        const reader = new FileReader();
        reader.onloadend = () => resolve(reader.result.split(',')[1]);
        reader.readAsDataURL(blob);
      });
      let filename = 'facture_eg.pdf';
      try { const ref = JSON.parse(notif.attachment_ref || '{}'); filename = ref.filename || filename; } catch (_) {}
      setEgAttachment({ filename, contentType: 'application/pdf', content: base64 });
    } catch (e) { /* PJ optionnelle */ }
    setLoadingAttach(false);
  }, [notif.id, notif.attachment_ref]);

  // ── Étape 1 : créer N appels + 1 facture ──────────────────────────────────
  const handleCreate = async () => {
    if (!projId) { setErr('Sélectionne un programme.'); return; }
    const pct = parseFloat(egPct);
    if (!isFinite(pct) || pct <= 0) { setErr("Le % d'avancement doit être renseigné."); return; }
    const chosen = soldLots.filter(({ idx }) => selected.has(idx));
    if (chosen.length === 0) { setErr('Coche au moins un lot.'); return; }
    setSaving(true); setErr('');
    try {
      const created = [];
      for (let i = 0; i < chosen.length; i++) {
        const { idx } = chosen[i];
        const delta = Math.max(0, pct - lotCumul(idx));
        const devis = lotDevisOf(idx);
        const body = {
          lot_id:       String(idx + 1),
          programme_id: projId,
          eg_notif_id:  i === 0 ? notif.id : null,   // une seule facture à régler
          eg_date:      new Date().toISOString().slice(0, 10),
          eg_pct:       pct,
          eg_montant:   devis ? Math.round((delta / 100) * devis) : null,
          eg_niveau:    'lot',
          pct_delta:    delta,
        };
        const r = await window.apiFetch('/api/appels-eg', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
        const d = await r.json();
        if (!r.ok) throw new Error(d.error || 'Erreur serveur');
        if (i === 0) setFactureNotifId(d.facture_notif_id || null);
        created.push({ appel: d, lotIdx: idx });
      }
      setCreatedAppels(created);
      setStep(2);
      loadEgAttachment();
    } catch (e) { setErr(e.message); }
    setSaving(false);
  };

  // ── Étape 2 : un seul email archi ─────────────────────────────────────────
  const handleSendArch = async () => {
    const archEmail = architecte?.email || '';
    if (!archEmail) { setArchErr("Email de l'architecte introuvable dans le CRM."); return; }
    setArchSending(true); setArchErr('');
    try {
      const pct = parseFloat(egPct) || 0;
      const payload = { to: archEmail, subject: `Demande attestation ${pct}% — ${progNom}`, body: emailArchBody };
      if (egAttachment) payload.attachment = { filename: egAttachment.filename, contentType: egAttachment.contentType, content: egAttachment.content };
      const r = await window.apiFetch('/api/email/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
      const d = await r.json();
      if (!r.ok || d.error) throw new Error(d.error || 'Erreur envoi');
      await Promise.all(createdAppels.map(({ appel }) => window.apiFetch(`/api/appels-eg/${appel.id}`, {
        method: 'PATCH', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ arch_email_sent: true, arch_email_date: new Date().toISOString() }),
      })));
      setArchSent(true);
    } catch (e) { setArchErr(e.message); }
    setArchSending(false);
  };

  const projs = (projects || []).filter(p => !p.isGlobal && (!p.statut || p.statut === 'En cours'));
  const inp = { background: C.card, border: '1px solid ' + C.border, borderRadius: 6, color: C.text, padding: '6px 8px', fontSize: 13, width: '100%' };
  const pctNum = parseFloat(egPct);

  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
      onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={{ background: '#fff', borderRadius: 16, padding: 28, width: 620, maxWidth: '96vw', maxHeight: '90vh', overflowY: 'auto', boxShadow: '0 8px 40px #0003', border: '1px solid ' + C.border }}
        onClick={e => e.stopPropagation()}>

        {step === 1 && <React.Fragment>
          <div style={{ fontWeight: 800, fontSize: 17, marginBottom: 4 }}>💶 Valider un EG global → appels par lot</div>
          <div style={{ fontSize: 12, color: C.muted, marginBottom: 16 }}>Notif : <b>{notif.subject || '(sans objet)'}</b></div>

          <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 14 }}>
            <div>
              <div style={{ fontSize: 11, fontWeight: 700, color: C.muted, marginBottom: 3 }}>Programme *</div>
              <select value={projId} onChange={e => { setProjId(e.target.value); seededRef.current = null; }} style={inp}>
                <option value="">— Sélectionner un programme —</option>
                {projs.map(p => <option key={p.id} value={p.id}>{p.ville ? p.ville + ' – ' : ''}{p.nom}</option>)}
              </select>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
              <div>
                <div style={{ fontSize: 11, fontWeight: 700, color: C.muted, marginBottom: 3 }}>% d'avancement (EG) *</div>
                <input type="number" min="0" max="100" step="0.5" value={egPct} onChange={e => setEgPct(e.target.value)} placeholder="Ex : 40" style={inp} />
              </div>
              <div>
                <div style={{ fontSize: 11, fontWeight: 700, color: C.muted, marginBottom: 3 }}>Montant EG global (€)</div>
                <input type="number" value={egMontant} readOnly style={{ ...inp, background: C.bg, color: C.muted }} />
              </div>
            </div>
          </div>

          <div style={{ fontSize: 11, fontWeight: 700, color: C.muted, marginBottom: 6 }}>LOTS VENDUS DU PROGRAMME</div>
          {!projId ? (
            <div style={{ fontSize: 12, color: C.muted, marginBottom: 12 }}>Sélectionne d'abord un programme.</div>
          ) : soldLots.length === 0 ? (
            <div style={{ fontSize: 12, color: C.muted, marginBottom: 12 }}>Aucun lot vendu dans ce programme.</div>
          ) : (
            <div style={{ border: '1px solid ' + C.border, borderRadius: 8, overflow: 'hidden', marginBottom: 12 }}>
              {soldLots.map(({ idx, lot }) => {
                const cumul = lotCumul(idx);
                const delta = isFinite(pctNum) ? pctNum - cumul : null;
                const travaux = lotTravauxOf(idx);
                const montantClient = (delta != null && delta > 0 && travaux) ? Math.round((delta / 100) * travaux) : 0;
                const checked = selected.has(idx);
                const dejaAtteint = delta != null && delta <= 0;
                return (
                  <label key={idx} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px', borderTop: idx === soldLots[0].idx ? 'none' : '1px solid ' + C.border, background: checked ? '#F6FAF0' : C.card, cursor: 'pointer' }}>
                    <input type="checkbox" checked={checked} onChange={() => toggleLot(idx)} />
                    <div style={{ fontWeight: 600, minWidth: 56 }}>Lot {idx + 1}</div>
                    <div style={{ flex: 1, fontSize: 12, color: C.muted }}>{lot.clientNom || '—'} · {lot.statutCommercial}</div>
                    <div style={{ fontSize: 12, textAlign: 'right' }}>
                      {dejaAtteint
                        ? <span style={{ color: '#B45309' }}>déjà à {pctFmt(cumul)}</span>
                        : <React.Fragment>delta <b>{pctFmt(delta)}</b> · client ~ <b>{eur(montantClient)}</b></React.Fragment>}
                    </div>
                  </label>
                );
              })}
            </div>
          )}

          <div style={{ fontSize: 11, color: C.muted, marginBottom: 12 }}>
            {selected.size} lot{selected.size > 1 ? 's' : ''} sélectionné{selected.size > 1 ? 's' : ''} → {selected.size} appel{selected.size > 1 ? 's' : ''} client + 1 facture à régler.
          </div>

          {err && <div style={{ background: '#fee2e2', border: '1px solid #fca5a5', borderRadius: 8, padding: '8px 12px', marginBottom: 12, fontSize: 12, color: '#dc2626' }}>❌ {err}</div>}

          <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
            <button onClick={onClose} style={{ background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, padding: '8px 18px', fontSize: 13, cursor: 'pointer' }}>Annuler</button>
            <button onClick={handleCreate} disabled={saving}
              style={{ background: saving ? '#94a3b8' : '#b45309', color: '#fff', border: 'none', borderRadius: 8, padding: '8px 22px', fontSize: 13, fontWeight: 700, cursor: saving ? 'default' : 'pointer' }}>
              {saving ? '⏳ Création…' : `✓ Créer les appels (${selected.size})`}
            </button>
          </div>
        </React.Fragment>}

        {step === 2 && <React.Fragment>
          <div style={{ fontWeight: 800, fontSize: 17, marginBottom: 4 }}>✅ Appels créés — Email architecte</div>
          <div style={{ background: '#dcfce7', border: '1px solid #86efac', borderRadius: 8, padding: '8px 12px', marginBottom: 10, fontSize: 12, color: '#15803d' }}>
            ✓ {createdAppels.length} appel{createdAppels.length > 1 ? 's' : ''} créé{createdAppels.length > 1 ? 's' : ''} à {pctFmt(parseFloat(egPct))}.
          </div>
          {factureNotifId ? (
            <div style={{ background: '#eff6ff', border: '1px solid #bfdbfe', borderRadius: 8, padding: '8px 12px', marginBottom: 14, fontSize: 12, color: '#1d4ed8' }}>
              🧾 Une seule facture à régler créée (n°{factureNotifId}) → onglet Notifications → Factures.
            </div>
          ) : (
            <div style={{ background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, padding: '7px 12px', marginBottom: 14, fontSize: 11, color: C.muted }}>
              ℹ️ Aucune facture générée (pas de pièce jointe ou source introuvable).
            </div>
          )}

          {architecte?.email ? (
            <div style={{ background: '#f0fdf4', border: '1px solid #86efac', borderRadius: 8, padding: '8px 12px', marginBottom: 12, fontSize: 12 }}>
              <span style={{ color: '#15803d', fontWeight: 700 }}>Architecte : </span>
              <span style={{ fontFamily: 'monospace' }}>{architecte.email}</span>
              {architecte.nom && <span style={{ color: C.muted, marginLeft: 8 }}>({architecte.nom})</span>}
            </div>
          ) : (
            <div style={{ background: '#fef9c3', border: '1px solid #fde68a', borderRadius: 8, padding: '8px 12px', marginBottom: 12, fontSize: 12, color: '#92400e' }}>
              ⚠️ Email de l'architecte non trouvé dans le CRM (cherchez "Karoubi" → Entreprises).
            </div>
          )}

          {loadingAttach && <div style={{ fontSize: 11, color: C.muted, marginBottom: 8 }}>⏳ Chargement de la facture EG…</div>}
          {egAttachment && !loadingAttach && (
            <div style={{ background: '#eff6ff', border: '1px solid #bfdbfe', borderRadius: 8, padding: '6px 12px', marginBottom: 10, fontSize: 12 }}>📎 PJ : <b>{egAttachment.filename}</b></div>
          )}

          <div style={{ marginBottom: 14 }}>
            <div style={{ fontSize: 11, color: C.muted, marginBottom: 4 }}>Corps de l'email (un seul, pour tout le programme)</div>
            <textarea value={emailArchBody} readOnly
              style={{ background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, width: '100%', height: 240, fontFamily: "'Courier New',monospace", fontSize: 11, padding: '8px', resize: 'vertical', lineHeight: 1.5 }} />
          </div>

          {archSent && (
            <div style={{ background: '#dcfce7', border: '1px solid #86efac', borderRadius: 8, padding: '10px 12px', marginBottom: 12, fontSize: 12, color: '#15803d' }}>
              ✅ Email envoyé. L'agent surveillera la réponse de l'architecte (attestation).
            </div>
          )}
          {archErr && <div style={{ background: '#fee2e2', border: '1px solid #fca5a5', borderRadius: 8, padding: '8px 12px', marginBottom: 12, fontSize: 12, color: '#dc2626' }}>❌ {archErr}</div>}

          <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', flexWrap: 'wrap' }}>
            {!archSent && <React.Fragment>
              <button onClick={onDone} style={{ background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, padding: '8px 18px', fontSize: 13, cursor: 'pointer' }}>Ignorer l'email</button>
              <button onClick={handleSendArch} disabled={archSending || !architecte?.email}
                style={{ background: archSending || !architecte?.email ? '#94a3b8' : '#2563eb', color: '#fff', border: 'none', borderRadius: 8, padding: '8px 22px', fontSize: 13, fontWeight: 700, cursor: archSending || !architecte?.email ? 'default' : 'pointer' }}>
                {archSending ? '⏳ Envoi…' : '📧 Envoyer à l\'architecte'}
              </button>
            </React.Fragment>}
            {archSent && (
              <button onClick={onDone} style={{ background: '#15803d', color: '#fff', border: 'none', borderRadius: 8, padding: '8px 22px', fontSize: 13, fontWeight: 700, cursor: 'pointer' }}>Fermer</button>
            )}
          </div>
        </React.Fragment>}

      </div>
    </div>
  );
}

/* ── Modale : relance client (étape E) ────────────────────────────────────────
   Email de relance pré-rempli pour un appel envoyé non payé. Envoi manuel via
   /api/email/send (BCC archivage ajouté côté backend), puis PATCH
   relance_client_date. Indivision : envoi aux deux acquéreurs. */
function RelanceClientModal({ appel, projects, fiches, crm, C, onClose, onSent }) {
  const programmeId = appel.programme_id;
  const lotIdx = Math.max(0, (parseInt(String(appel.lot_id || '').replace(/[^0-9]/g, ''), 10) || 1) - 1);
  const fiche = (fiches && fiches[programmeId]) || {};
  const lot = ((fiche.lots) || [])[lotIdx] || {};
  const proj = (projects || []).find(p => p.id === programmeId) || {};
  const progNom = [proj.ville, proj.nom].filter(Boolean).join(' ') || fiche.adresse || '';

  const clientsCrm = (crm && crm.clients) || [];
  const cli = lot.clientId ? clientsCrm.find(c => c.id === lot.clientId) : null;
  const clientEmail = (cli && cli.email) || '';
  const isIndiv = (lot.typeAcquisition === 'indivision') && lot.coAcheteurClientId;
  const co = isIndiv ? clientsCrm.find(c => c.id === lot.coAcheteurClientId) : null;
  const coEmail = (co && co.email) || '';
  const recipients = [clientEmail, isIndiv ? coEmail : null].filter(Boolean);
  const clientNom = lot.clientNom || (cli ? `${cli.prenom || ''} ${cli.nom || ''}`.trim() : '') || '[Client]';

  const montant = (appel.montant_client != null && appel.montant_client !== '') ? Number(appel.montant_client)
    : (appel.montant_attendu != null ? Number(appel.montant_attendu) : null);
  const montantStr = montant != null ? montant.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '';
  const pct = appel.pct_delta || appel.eg_pct || 0;
  const lotN = lotIdx + 1;
  const iban = fiche.ibanProgramme || '[IBAN À COMPLÉTER]';
  const bic = fiche.bic || '';
  const dEnvoi = appel.appel_client_date ? new Date(appel.appel_client_date) : null;
  const dEnvoiStr = (dEnvoi && !isNaN(dEnvoi)) ? dEnvoi.toLocaleDateString('fr-FR') : '';
  const jours = dEnvoi && !isNaN(dEnvoi) ? Math.floor((Date.now() - dEnvoi.getTime()) / 86400000) : null;

  const body = `Madame, Monsieur ${clientNom},

Sauf erreur de notre part, nous n'avons pas encore reçu le règlement de l'appel de fonds suivant, que nous vous avons adressé${dEnvoiStr ? ` le ${dEnvoiStr}` : ''} :

  Programme   : ${progNom}
  Lot N°      : ${lotN}${lot.type ? ` (${lot.type})` : ''}
  Avancement  : ${pct}%${montantStr ? `\n  Montant dû  : ${montantStr} €` : ''}

Nous vous remercions de bien vouloir procéder au règlement dans les meilleurs délais par virement :
  IBAN : ${iban}${bic ? `\n  BIC  : ${bic}` : ''}

Si le règlement a été effectué entre-temps, merci de ne pas tenir compte de ce message.

Cordialement,`;

  const [sending, setSending] = React.useState(false);
  const [sent, setSent] = React.useState(false);
  const [err, setErr] = React.useState('');

  const handleSend = async () => {
    if (recipients.length === 0) { setErr("Aucun email client trouvé dans le CRM pour ce lot."); return; }
    setSending(true); setErr('');
    try {
      const r = await window.apiFetch('/api/email/send', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ to: recipients.join(', '), subject: `Relance — appel de fonds Lot ${lotN} — ${progNom}`, body }),
      });
      const d = await r.json();
      if (!r.ok || d.error) throw new Error(d.error || 'Erreur envoi');
      await window.apiFetch('/api/appels-eg/' + appel.id, {
        method: 'PATCH', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ relance_client_date: new Date().toISOString() }),
      }).catch(() => {});
      setSent(true);
    } catch (e) { setErr(e.message); }
    setSending(false);
  };

  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
      onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={{ background: '#fff', borderRadius: 16, padding: 26, width: 560, maxWidth: '96vw', maxHeight: '90vh', overflowY: 'auto', boxShadow: '0 8px 40px #0003', border: '1px solid ' + C.border }}
        onClick={e => e.stopPropagation()}>
        <div style={{ fontWeight: 800, fontSize: 17, marginBottom: 4 }}>📧 Relancer le client</div>
        <div style={{ fontSize: 12, color: C.muted, marginBottom: 14 }}>
          {progNom} — Lot {lotN} · appel de {pct}%{montantStr ? ` (${montantStr} €)` : ''}{jours != null ? ` · impayé depuis ${jours} j` : ''}
        </div>

        {recipients.length > 0 ? (
          <div style={{ background: '#f0fdf4', border: '1px solid #86efac', borderRadius: 8, padding: '8px 12px', marginBottom: 12, fontSize: 12 }}>
            <span style={{ color: '#15803d', fontWeight: 700 }}>Destinataire{recipients.length > 1 ? 's' : ''} : </span>
            <span style={{ fontFamily: 'monospace' }}>{recipients.join(', ')}</span>
          </div>
        ) : (
          <div style={{ background: '#fef9c3', border: '1px solid #fde68a', borderRadius: 8, padding: '8px 12px', marginBottom: 12, fontSize: 12, color: '#92400e' }}>
            ⚠️ Aucun email client trouvé dans le CRM pour ce lot — impossible d'envoyer.
          </div>
        )}

        <div style={{ marginBottom: 14 }}>
          <div style={{ fontSize: 11, color: C.muted, marginBottom: 4 }}>Corps de l'email</div>
          <textarea value={body} readOnly
            style={{ background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, width: '100%', height: 240, fontFamily: "'Courier New',monospace", fontSize: 11, padding: '8px', resize: 'vertical', lineHeight: 1.5 }} />
        </div>

        {sent && (
          <div style={{ background: '#dcfce7', border: '1px solid #86efac', borderRadius: 8, padding: '10px 12px', marginBottom: 12, fontSize: 12, color: '#15803d' }}>
            ✅ Relance envoyée.
          </div>
        )}
        {err && <div style={{ background: '#fee2e2', border: '1px solid #fca5a5', borderRadius: 8, padding: '8px 12px', marginBottom: 12, fontSize: 12, color: '#dc2626' }}>❌ {err}</div>}

        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          {!sent && <React.Fragment>
            <button onClick={onClose} style={{ background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, padding: '8px 18px', fontSize: 13, cursor: 'pointer' }}>Annuler</button>
            <button onClick={handleSend} disabled={sending || recipients.length === 0}
              style={{ background: (sending || recipients.length === 0) ? '#94a3b8' : '#2563eb', color: '#fff', border: 'none', borderRadius: 8, padding: '8px 22px', fontSize: 13, fontWeight: 700, cursor: (sending || recipients.length === 0) ? 'default' : 'pointer' }}>
              {sending ? '⏳ Envoi…' : '📧 Envoyer la relance'}
            </button>
          </React.Fragment>}
          {sent && (
            <button onClick={onSent} style={{ background: '#15803d', color: '#fff', border: 'none', borderRadius: 8, padding: '8px 22px', fontSize: 13, fontWeight: 700, cursor: 'pointer' }}>Fermer</button>
          )}
        </div>
      </div>
    </div>
  );
}
