/* ──────────────────────────────────────────────────────────────────────────
   AppelsFondsModals.jsx — fenêtres partagées du flux Appels de fonds / EG

   Ces 4 modales ont été extraites de Notifications.jsx (dette D1, 2026-06-24).
   Elles sont désormais pilotées depuis l'onglet « Appels de fonds »
   (AppelsFonds.jsx). Définies en portée globale (convention projet : pas
   d'import/export), elles restent disponibles pour tout composant chargé.

   - AttestationContextModal   : étape 0 — lecture PDF Haiku (contexte attestation)
   - AttestationArchiveModal    : étape 1 — archivage OneDrive de l'attestation
   - ConvertirAppelFondsModal   : conversion d'une notif EG en appel de fonds
   - AppelClientEgModal         : génération de la facture/appel de fonds client
   ────────────────────────────────────────────────────────────────────────── */

/* ── Modale : Contexte attestation architecte (étape 0 — lecture PDF Haiku) ─── */
function AttestationContextModal({notif,projects,fiches,onConfirm,onCancel}){
  const inp={background:'#f8fafc',border:'1px solid #cbd5e1',borderRadius:6,padding:'7px 10px',fontSize:13,color:'#1e293b',width:'100%',boxSizing:'border-box'};
  const btn=(bg,sm)=>({background:bg||'#2563eb',color:bg==='#e2e8f0'?'#475569':'#fff',border:'none',borderRadius:6,padding:sm?'5px 12px':'8px 18px',cursor:'pointer',fontSize:sm?12:13,fontWeight:600,whiteSpace:'nowrap'});

  const metaRaw=notif.metadata||{};
  const [loading,  setLoading]  =React.useState(true);
  const [error,    setError]    =React.useState(null);
  const [projId,   setProjId]   =React.useState(String(metaRaw.programme_id||''));
  const [lotIdx,   setLotIdx]   =React.useState(metaRaw.lot_idx!=null?String(metaRaw.lot_idx):'');
  const [pctTotal, setPctTotal] =React.useState(metaRaw.avancement_pct!=null?String(metaRaw.avancement_pct):'');
  const [pctAppele,setPctAppele]=React.useState('');
  const [pctCumule,setPctCumule]=React.useState(0);

  // Lecture du PDF via Haiku au montage
  React.useEffect(()=>{
    (async()=>{
      try{
        const r=await window.apiFetch('/api/agent/notifications/'+notif.id+'/read-attestation',{method:'POST'});
        const d=await r.json();
        if(d.ok){
          if(d.programme_id!=null) setProjId(String(d.programme_id));
          if(d.lot_idx!=null)      setLotIdx(String(d.lot_idx));
          if(d.avancement_pct!=null) setPctTotal(String(d.avancement_pct));
          if(d.pct_cumule!=null)   setPctCumule(Number(d.pct_cumule));
        } else {
          setError(d.error||'Erreur inconnue');
        }
      }catch(e){ setError('Erreur réseau : '+e.message); }
      finally{ setLoading(false); }
    })();
  },[]);

  // Recalcul automatique du % appelé quand pctTotal ou pctCumule change
  React.useEffect(()=>{
    const t=parseInt(pctTotal,10);
    if(!isNaN(t)) setPctAppele(String(Math.max(0,t-pctCumule)));
  },[pctTotal,pctCumule]);

  const lots=projId?((fiches[projId]||{}).lots||[]):[];
  const pctTotalNum =parseInt(pctTotal,10);
  const pctAppeleNum=parseInt(pctAppele,10);
  const pctAttendu  =!isNaN(pctTotalNum)?pctTotalNum-pctCumule:null;
  const showWarn    =!isNaN(pctAppeleNum)&&pctAttendu!=null&&Math.abs(pctAppeleNum-pctAttendu)>0;
  const canConfirm  =projId&&lotIdx!==''&&!isNaN(pctTotalNum)&&pctTotalNum>0;

  const handleConfirm=()=>{
    const prog=(projects||[]).find(p=>String(p.id)===String(projId));
    const seg=s=>(s||'').replace(/[/\\:*?"<>|]/g,'_').trim()||'inconnu';
    const ville=seg(prog?.ville||'');
    const nom  =seg(prog?.nom  ||'');
    const progSeg=ville?`${ville} - ${nom}`:nom;
    const lotN =parseInt(lotIdx,10)+1;
    const pct  =pctTotalNum||'??';
    const suggestedFolder =`\\Blue\\Programmes\\Programmes validés\\${progSeg}\\03 - Vente\\02 - Lots\\Lot ${lotN}\\04 - Appels de fonds\\${pct}%\\`;
    const suggestedFilename=`Attestation_${pct}pct_Lot${lotN}.pdf`;

    const enrichedNotif={
      ...notif,
      metadata:{
        ...(notif.metadata||{}),
        programme_id:projId||null,
        lot_idx:lotIdx!==''?parseInt(lotIdx,10):null,
        avancement_pct:pctTotalNum||null,
      },
      proposed_action:{
        ...(notif.proposed_action||{}),
        params:{
          ...((notif.proposed_action||{}).params||{}),
          programmeId:projId||null,
          lotIdx:lotIdx!==''?parseInt(lotIdx,10):null,
          avancement_pct:pctTotalNum||null,
          pct_appele:!isNaN(pctAppeleNum)?pctAppeleNum:null,
          suggestedFolder,
          suggestedFilename,
        },
      },
    };
    onConfirm(enrichedNotif);
  };

  return(
    <div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.5)',zIndex:3000,display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
      <div style={{background:'#fff',borderRadius:12,padding:28,maxWidth:500,width:'100%',maxHeight:'85vh',overflowY:'auto',boxShadow:'0 20px 60px rgba(0,0,0,0.25)'}}>

        <div style={{fontWeight:800,fontSize:17,marginBottom:4}}>📋 Contexte de l'attestation</div>
        <div style={{fontSize:12,color:'#64748b',marginBottom:20}}>
          {loading?'Lecture du PDF en cours…':'Vérifiez et complétez les informations extraites du document.'}
        </div>

        {loading&&(
          <div style={{textAlign:'center',padding:'32px 0',color:'#64748b',fontSize:14}}>⏳ Haiku lit l'attestation…</div>
        )}

        {!loading&&(<>
          {error&&(
            <div style={{background:'#fee2e2',borderRadius:7,padding:'8px 12px',fontSize:12,color:'#dc2626',marginBottom:14}}>
              ⚠️ {error} — vous pouvez remplir le formulaire manuellement.
            </div>
          )}

          {/* Programme */}
          <div style={{marginBottom:14}}>
            <label style={{fontSize:12,fontWeight:600,color:'#475569',display:'block',marginBottom:4}}>Programme</label>
            <select value={projId} onChange={e=>{setProjId(e.target.value);setLotIdx('');}} style={inp}>
              <option value="">— sélectionner —</option>
              {(projects||[]).map(p=>(
                <option key={p.id} value={String(p.id)}>{p.ville?p.ville+' - ':''}{p.nom}</option>
              ))}
            </select>
          </div>

          {/* Lot */}
          <div style={{marginBottom:14}}>
            <label style={{fontSize:12,fontWeight:600,color:'#475569',display:'block',marginBottom:4}}>Lot</label>
            <select value={lotIdx} onChange={e=>setLotIdx(e.target.value)} style={inp} disabled={!projId}>
              <option value="">— sélectionner —</option>
              {lots.map((l,i)=>(
                <option key={i} value={String(i)}>Lot {i+1}{l.clientNom?' — '+l.clientNom:''}</option>
              ))}
            </select>
          </div>

          {/* % avancement total */}
          <div style={{marginBottom:14}}>
            <label style={{fontSize:12,fontWeight:600,color:'#475569',display:'block',marginBottom:4}}>% avancement total (cumulé après cet appel)</label>
            <div style={{display:'flex',alignItems:'center',gap:8}}>
              <input type="number" min="1" max="100" value={pctTotal} onChange={e=>setPctTotal(e.target.value)} style={{...inp,width:90}} placeholder="ex : 20"/>
              <span style={{fontSize:12,color:'#64748b'}}>%</span>
              {pctCumule>0&&<span style={{fontSize:11,color:'#94a3b8'}}>Cumul actuel : {pctCumule}%</span>}
            </div>
          </div>

          {/* % appelé (cet appel) */}
          <div style={{marginBottom:16}}>
            <label style={{fontSize:12,fontWeight:600,color:'#475569',display:'block',marginBottom:4}}>% travaux appelé (cet appel uniquement)</label>
            <div style={{display:'flex',alignItems:'center',gap:8}}>
              <input type="number" min="0" max="100" value={pctAppele} onChange={e=>setPctAppele(e.target.value)} style={{...inp,width:90}}/>
              <span style={{fontSize:12,color:'#64748b'}}>%</span>
            </div>
          </div>

          {/* Warning écart */}
          {showWarn&&(
            <div style={{background:'#fff7ed',border:'1px solid #fed7aa',borderRadius:7,padding:'8px 12px',fontSize:12,color:'#c2410c',marginBottom:14}}>
              ⚠️ Le % appelé ({pctAppele}%) ne correspond pas à l'avancement total ({pctTotal}%) moins le cumul déjà appelé ({pctCumule}%). Écart : {Math.abs(pctAppeleNum-pctAttendu)}%.
            </div>
          )}

          <div style={{display:'flex',gap:8,justifyContent:'flex-end',marginTop:4}}>
            <button onClick={onCancel} style={btn('#e2e8f0',false)}>Annuler</button>
            <button onClick={handleConfirm} disabled={!canConfirm}
              style={{...btn(canConfirm?'#2563eb':'#94a3b8',false),cursor:canConfirm?'pointer':'default'}}>
              Continuer →
            </button>
          </div>
        </>)}
      </div>
    </div>
  );
}

/* ── Modale : Archivage attestation architecte (étape 1) ────────────────────── */
function AttestationArchiveModal({notif,onArchived,onCancel}){
  const params=(notif.proposed_action&&notif.proposed_action.params)||{};
  const initFolder  =params.suggestedFolder  ||'\\Blue\\Programmes\\Programmes validés';
  const initFilename=params.suggestedFilename||'Attestation_avancement.pdf';

  const [currentPath,setCurrentPath]=React.useState('');
  const [folders,    setFolders]    =React.useState([]);
  const [pathExists, setPathExists] =React.useState(true);
  const [filename,   setFilename]   =React.useState(initFilename);
  const [newName,    setNewName]    =React.useState('');
  const [showCreate, setShowCreate] =React.useState(false);
  const [loading,    setLoading]    =React.useState(false);
  const [archiving,  setArchiving]  =React.useState(false);
  const [creating,   setCreating]   =React.useState(false);
  const [error,      setError]      =React.useState(null);

  const inp={background:'#f8fafc',border:'1px solid #cbd5e1',borderRadius:6,padding:'7px 10px',fontSize:13,color:'#1e293b',width:'100%',boxSizing:'border-box'};
  const btn=(bg,sm)=>({background:bg||'#2563eb',color:bg==='#e2e8f0'?'#475569':'#fff',border:'none',borderRadius:6,padding:sm?'5px 12px':'8px 18px',cursor:'pointer',fontSize:sm?12:13,fontWeight:600,whiteSpace:'nowrap'});

  const browseTo=React.useCallback(async(path)=>{
    setLoading(true);setError(null);
    try{
      const r=await fetch('/api/agent/onedrive/browse?path='+encodeURIComponent(path));
      const d=await r.json();
      setCurrentPath(path);setFolders(d.folders||[]);setPathExists(d.exists!==false);
    }catch(e){setError('Impossible de charger le dossier : '+e.message);}
    finally{setLoading(false);}
  },[]);

  // Au montage : essaie le chemin suggéré, puis remonte de niveau en niveau
  React.useEffect(()=>{
    const segs=(initFolder||'').replace(/\\+$/,'').split('\\').filter(Boolean);
    const chain=[
      initFolder,
      segs.length>1?'\\'+segs.slice(0,-1).join('\\'):null,
      '\\Blue\\Programmes\\Programmes validés',
    ].filter(Boolean);

    (async()=>{
      setLoading(true);
      for(const path of chain){
        try{
          const r=await fetch('/api/agent/onedrive/browse?path='+encodeURIComponent(path));
          const d=await r.json();
          if(d.exists!==false){
            setCurrentPath(path);setFolders(d.folders||[]);setPathExists(true);setLoading(false);
            return;
          }
        }catch(_){}
      }
      // dernier recours : on affiche quand même le dernier chemin
      browseTo('\\Blue\\Programmes\\Programmes validés');
    })();
  },[]);

  const goUp=()=>{
    const segs=currentPath.replace(/\\+$/,'').split('\\').filter(Boolean);
    if(segs.length<=1)return;
    browseTo('\\'+segs.slice(0,-1).join('\\'));
  };

  const createFolder=async()=>{
    if(!newName.trim())return;
    setCreating(true);setError(null);
    try{
      const full=currentPath.replace(/\\+$/,'')+'\\'+newName.trim();
      const r=await window.apiFetch('/api/agent/onedrive/mkdir',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path:full})});
      const d=await r.json();
      if(!d.ok)throw new Error(d.error||'Erreur création dossier');
      setNewName('');setShowCreate(false);
      await browseTo(full);
    }catch(e){setError('Impossible de créer le dossier : '+e.message);}
    finally{setCreating(false);}
  };

  const archive=async()=>{
    if(!filename.trim()||!currentPath)return;
    setArchiving(true);setError(null);
    try{
      const r=await window.apiFetch('/api/agent/notifications/'+notif.id+'/archive-attachment',{
        method:'POST',headers:{'Content-Type':'application/json'},
        body:JSON.stringify({folderPath:currentPath,filename:filename.trim()})
      });
      const d=await r.json();
      if(!d.ok)throw new Error(d.error||'Erreur archivage');
      onArchived(notif,d.shareUrl,currentPath);
    }catch(e){setError('Archivage échoué : '+e.message);}
    finally{setArchiving(false);}
  };

  // Breadcrumb : segments du chemin courant
  const crumbs=currentPath?currentPath.replace(/\\+$/,'').split('\\').filter(Boolean):[];

  return(
    <div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.5)',zIndex:3000,display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
      <div style={{background:'#fff',borderRadius:12,padding:28,maxWidth:600,width:'100%',maxHeight:'85vh',overflowY:'auto',boxShadow:'0 20px 60px rgba(0,0,0,0.25)'}}>

        <div style={{fontWeight:800,fontSize:17,marginBottom:4}}>📁 Archiver l'attestation architecte</div>
        <div style={{fontSize:12,color:'#64748b',marginBottom:20}}>Sélectionnez le dossier de destination, puis cliquez sur Archiver pour continuer vers la génération de l'appel client.</div>

        {/* Nom de fichier */}
        <div style={{marginBottom:16}}>
          <label style={{fontSize:12,fontWeight:600,color:'#475569',display:'block',marginBottom:4}}>Nom du fichier</label>
          <input value={filename} onChange={e=>setFilename(e.target.value)} style={inp} placeholder="Attestation_10pct_Lot1.pdf"/>
        </div>

        {/* Navigateur dossiers */}
        <div style={{marginBottom:12}}>
          <label style={{fontSize:12,fontWeight:600,color:'#475569',display:'block',marginBottom:6}}>Dossier de destination</label>

          {/* Breadcrumb */}
          <div style={{background:'#f1f5f9',borderRadius:6,padding:'6px 10px',fontSize:11,color:'#475569',marginBottom:8,display:'flex',alignItems:'center',gap:4,flexWrap:'wrap',minHeight:30}}>
            {crumbs.length>0
              ?crumbs.map((seg,i)=>(
                <React.Fragment key={i}>
                  {i>0&&<span style={{color:'#94a3b8',userSelect:'none'}}>\</span>}
                  <span
                    onClick={()=>{ if(i<crumbs.length-1) browseTo('\\'+crumbs.slice(0,i+1).join('\\')); }}
                    style={{cursor:i<crumbs.length-1?'pointer':'default',color:i<crumbs.length-1?'#2563eb':'#1e293b',fontWeight:i===crumbs.length-1?600:400}}
                  >{seg}</span>
                </React.Fragment>
              ))
              :<span style={{color:'#94a3b8'}}>Chargement…</span>
            }
          </div>

          {/* Bouton remonter */}
          {crumbs.length>1&&!loading&&(
            <button onClick={goUp} style={{...btn('#64748b',true),marginBottom:8}}>↑ Remonter</button>
          )}

          {/* Liste sous-dossiers */}
          <div style={{border:'1px solid #e2e8f0',borderRadius:8,overflow:'hidden',marginBottom:8,maxHeight:220,overflowY:'auto'}}>
            {loading
              ?<div style={{padding:'14px 12px',fontSize:12,color:'#94a3b8',textAlign:'center'}}>Chargement…</div>
              :folders.length===0
                ?<div style={{padding:'14px 12px',fontSize:12,color:'#94a3b8',textAlign:'center'}}>Aucun sous-dossier</div>
                :folders.map((f,i)=>(
                  <div key={i}
                    onClick={()=>browseTo(f.path)}
                    style={{padding:'9px 14px',fontSize:13,cursor:'pointer',borderBottom:i<folders.length-1?'1px solid #f1f5f9':'none',display:'flex',alignItems:'center',gap:8,background:'#fff'}}
                    onMouseEnter={e=>e.currentTarget.style.background='#f8fafc'}
                    onMouseLeave={e=>e.currentTarget.style.background='#fff'}
                  >
                    <span>📁</span><span>{f.name}</span>
                  </div>
                ))
            }
          </div>

          {/* Création dossier */}
          {!showCreate
            ?<button onClick={()=>setShowCreate(true)} style={btn('#e2e8f0',true)}>+ Créer un dossier ici</button>
            :<div style={{display:'flex',gap:6,alignItems:'center'}}>
              <input
                value={newName} onChange={e=>setNewName(e.target.value)}
                onKeyDown={e=>{if(e.key==='Enter')createFolder();if(e.key==='Escape'){setShowCreate(false);setNewName('');}}}
                placeholder="Nom du dossier (ex : 10%)"
                style={{...inp,flex:1}} autoFocus
              />
              <button onClick={createFolder} disabled={creating||!newName.trim()} style={btn(creating||!newName.trim()?'#94a3b8':'#2563eb',true)}>
                {creating?'⏳':'Créer'}
              </button>
              <button onClick={()=>{setShowCreate(false);setNewName('');}} style={btn('#94a3b8',true)}>✕</button>
            </div>
          }
        </div>

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

        {/* Résumé destination */}
        {currentPath&&filename&&(
          <div style={{background:'#f0fdf4',border:'1px solid #86efac',borderRadius:6,padding:'8px 12px',fontSize:11,color:'#15803d',marginBottom:16,wordBreak:'break-all'}}>
            📄 <strong>{currentPath.replace(/\\+$/,'')}\\{filename}</strong>
          </div>
        )}

        {/* Boutons */}
        <div style={{display:'flex',gap:8,justifyContent:'flex-end',marginTop:8}}>
          <button onClick={onCancel} disabled={archiving} style={btn('#64748b')}>Annuler</button>
          <button
            onClick={archive}
            disabled={archiving||!filename.trim()||!currentPath||loading}
            style={btn(archiving||!filename.trim()||!currentPath||loading?'#94a3b8':'#0369a1')}>
            {archiving?'⏳ Archivage…':'📁 Archiver & continuer →'}
          </button>
        </div>

      </div>
    </div>
  );
}

/* ── Modale : Convertir notif EG en appel de fonds (2 étapes) ───────────────── */
function ConvertirAppelFondsModal({notif,projects,fiches,crm,onClose,onDone}) {
  const meta=(notif.metadata)||{};
  const action=(notif.proposed_action)||{};

  // ── Parsing du nom de PJ pour auto-remplissage ───────────────────────────
  // Ex : "Situation Arles 389 - Arles 20% lot4.pdf" → pct=20, lotNum=4, projId=…
  let _filePct=null, _fileLotNum=null, _fileProjId="";
  try{
    const _ref=JSON.parse(notif.attachment_ref||'{}');
    const _fn=(_ref.filename||notif.subject||'');
    const _pm=_fn.match(/(\d+(?:\.\d+)?)\s*%/);
    const _lm=_fn.match(/lot\s*(\d+)/i);
    if(_pm) _filePct=_pm[1];
    if(_lm) _fileLotNum=parseInt(_lm[1],10);
    // Fuzzy match programme : on cherche un programme dont la ville/nom est dans le nom de fichier
    const _fnLow=_fn.toLowerCase();
    const _activeProjs=(projects||[]).filter(p=>!p.isGlobal&&(!p.statut||p.statut==='En cours'));
    for(const _p of _activeProjs){
      const _ville=(_p.ville||'').toLowerCase();
      const _nom=(_p.nom||'').toLowerCase();
      if(_ville.length>2&&_fnLow.includes(_ville)){_fileProjId=_p.id;break;}
      if(_nom.length>2&&_fnLow.includes(_nom)){_fileProjId=_p.id;break;}
    }
  }catch(_){}

  // ── Étape et appel créé ──────────────────────────────────────────────────
  const [step,setStep]=React.useState(1);
  const [createdAppel,setCreatedAppel]=React.useState(null);

  // ── Step 1 : saisie ──────────────────────────────────────────────────────
  const preProjId=meta.programme_id||action?.params?.programmeId||_fileProjId||"";
  const [projId,setProjId]=React.useState(preProjId);
  const lots=React.useMemo(()=>{
    const fiche=fiches[projId];
    if(!fiche||!fiche.lots) return [];
    return fiche.lots.map((l,i)=>({idx:i,lot:l})).filter(({lot})=>
      isLotVenduOuAvance(lot.statutCommercial||"")
    );
  },[projId,fiches]);
  const [lotIdx,setLotIdx]=React.useState("");
  const [egPct,setEgPct]=React.useState(String(meta.eg_pct||meta.pct||meta.avancement_pct||_filePct||""));
  const [egMontant,setEgMontant]=React.useState(String(meta.eg_montant||meta.montant||meta.montant_ttc||""));
  const [saving,setSaving]=React.useState(false);
  const [err,setErr]=React.useState("");


  // Helper : calcule travauxReel depuis les champs bruts de la fiche (même formule que FicheDetail)
  const calcTravauxReel=React.useCallback((pId,lIdx)=>{
    try{
      const f=(fiches||{})[pId]||{};
      const lot=((f.lots)||[])[parseInt(lIdx,10)]||{};
      const toN=v=>parseFloat(String(v||'').replace(/\s/g,'').replace(',','.'))||0;
      const prixAchat=toN(f.prixAchat), devisTravaux=toN(f.devisTravaux);
      const euribor=toN(f.euribor), montantCredit=toN(f.montantCredit);
      const creditMensuel=montantCredit*(euribor/100+0.025)/12;
      const montantCommerce=toN(f.montantCommerceRdc);
      const prixDenormandie=prixAchat-montantCommerce;
      const creditTotal=creditMensuel*8;
      const achatFNI=prixDenormandie*1.025+toN(f.dossierBanque)+creditTotal;
      const moe=devisTravaux*0.08;
      const montantTravaux=moe+toN(f.dp)+toN(f.pc)+toN(f.plaquette)+toN(f.deplacements)
        +toN(f.avocat)+toN(f.hommeArt)+toN(f.geometre)+toN(f.diagnostics)
        +toN(f.loyerMeuble)+toN(f.doTrc)+toN(f.gfa)+devisTravaux;
      const prixTotalHorsMarge=achatFNI+montantTravaux;
      const prixReelVal=toN(lot.prixReel);
      if(prixReelVal<=0||prixTotalHorsMarge<=0) return 0;
      const foncierReelCalc=prixReelVal*achatFNI/prixTotalHorsMarge;
      const foncierReelOv=lot.foncierReelO!==""&&lot.foncierReelO!=null?toN(lot.foncierReelO):null;
      const foncierReel=foncierReelOv!==null?foncierReelOv:foncierReelCalc;
      return prixReelVal-foncierReel;
    }catch(_){return 0;}
  },[fiches]);

  // ── Step 2 : email architecte ─────────────────────────────────────────────
  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 [factureNotifId,setFactureNotifId]=React.useState(null);

  // ── Auto-sélection du lot si numéro parsé depuis la PJ ──────────────────
  React.useEffect(()=>{
    if(!projId||_fileLotNum==null||lotIdx!=="") return;
    const targetIdx=_fileLotNum-1; // le nom de fichier est 1-based (lot4 → idx 3)
    if(lots.find(l=>l.idx===targetIdx)) setLotIdx(String(targetIdx));
  },[projId,lots]);

  // Architecte depuis CRM (entreprise activite=Architecte ou nom Karoubi)
  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){
      console.error('[ConvertirModal] architecte useMemo error:',e);
      return null;
    }
  },[crm]);

  // Corps email architecte (recalculé quand step=2)
  const emailArchBody=React.useMemo(()=>{
    try{
      if(step!==2||!createdAppel) return '';
      const lotInfo=lots.find(l=>l.idx===parseInt(lotIdx,10));
      const lot=lotInfo?.lot||{};
      const fiche=(fiches||{})[projId]||{};
      const proj=((projects)||[]).find(p=>p.id===projId)||{};
      const progNom=[proj.ville,proj.nom].filter(Boolean).join(' ')||fiche.adresse||'';
      const clientNom=lot.clientNom||'[Client]';
      const archNom=architecte?.nom||'Madame Karoubi';
      const pct=Number(createdAppel.eg_pct||0);
      const montant=createdAppel.eg_montant?Number(createdAppel.eg_montant).toLocaleString('fr-FR',{minimumFractionDigits:2,maximumFractionDigits:2}):'';
      const lotN=parseInt(lotIdx,10)+1;
      const villeRef=String(proj.ville||proj.nom||'PROG').replace(/\s+/g,'-').toUpperCase().slice(0,15);
      const ref=`LOT${lotN}-${villeRef}-AV${pct}`;
      return `Objet : Demande attestation avancement ${pct}% — ${progNom} — Lot ${lotN} [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}%${montant?` (${montant} €)`:''}. 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 à l'appel de fonds client.

INFORMATIONS :
─────────────────────────────────────────
  Programme   : ${progNom}
  Lot N°      : ${lotN}${lot.type?` (${lot.type})`:''}
  Acquéreur   : ${clientNom}
  Avancement  : ${pct}%
─────────────────────────────────────────

Nous vous remercions de votre diligence.

Cordialement,`;
    }catch(e){
      console.error('[ConvertirModal] emailArchBody error:',e);
      return '(Erreur lors de la génération du corps — voir console navigateur)';
    }
  },[step,createdAppel,projId,lotIdx,fiches,projects,architecte,lots]);

  // Charge la PJ EG (PDF) depuis la notification source
  const loadEgAttachment=React.useCallback(async()=>{
    setLoadingAttach(true);
    try{
      const resp=await fetch(`/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){console.warn('Chargement PJ EG échoué:',e.message);}
    setLoadingAttach(false);
  },[notif.id,notif.attachment_ref]);

  const inp={background:"#f8fafc",border:"1px solid #cbd5e1",borderRadius:6,color:"#1e293b",padding:"6px 8px",fontSize:13,width:"100%"};

  // ── Handler Step 1 : créer l'appel ──────────────────────────────────────
  const handleSubmit=async()=>{
    if(!projId){setErr("Veuillez sélectionner un programme.");return;}
    if(lotIdx===""){setErr("Veuillez sélectionner un lot.");return;}
    if(!lots.find(l=>String(l.idx)===lotIdx)){setErr("Lot invalide ou non vendu pour ce programme.");return;}
    const pct=parseFloat(egPct);
    if(isNaN(pct)||pct<=0){setErr("Le % d'avancement doit être renseigné.");return;}
    setSaving(true); setErr("");
    try{
      const lotIdStr=String(Number(lotIdx)+1);
      // Le delta de CET appel = % total annoncé par l'EG − ce qui a déjà été
      // appelé au client sur ce lot (cumul). Sans cette soustraction, un lot déjà
      // avancé repartirait au % total au lieu de l'incrément (fix 2026-06-17).
      let cumul=0;
      try{
        const cr=await fetch(`/api/appels-eg?programme_id=${encodeURIComponent(projId)}&lot_id=${encodeURIComponent(lotIdStr)}`);
        if(cr.ok){ const arr=await cr.json(); cumul=(Array.isArray(arr)?arr:[]).reduce((mx,a)=>Math.max(mx,Number(a.pct_cumule)||0),0); }
      }catch(_){}
      const delta=Math.max(0, pct-cumul);
      const body={
        lot_id:       lotIdStr,
        programme_id: projId,
        eg_notif_id:  notif.id,
        eg_date:      new Date().toISOString().slice(0,10),
        eg_pct:       pct,
        eg_montant:   parseFloat(egMontant)||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");
      setCreatedAppel(d);
      setFactureNotifId(d.facture_notif_id||null);
      setStep(2);
      loadEgAttachment();
    }catch(e){setErr(e.message);}
    setSaving(false);
  };

  // ── Handler Step 2 : envoyer email architecte ───────────────────────────
  const handleSendArch=async()=>{
    const archEmail=architecte?.email||'';
    if(!archEmail){setArchErr("Email de l'architecte introuvable dans le CRM.");return;}
    setArchSending(true); setArchErr("");
    try{
      const lotInfo=lots.find(l=>l.idx===parseInt(lotIdx));
      const lot=lotInfo?.lot||{};
      const fiche=fiches[projId]||{};
      const proj=(projects||[]).find(p=>p.id===projId)||{};
      const progNom=[proj.ville,proj.nom].filter(Boolean).join(' ')||fiche.adresse||'';
      const pct=createdAppel.eg_pct||0;
      const lotN=parseInt(lotIdx)+1;
      const payload={
        to:archEmail,
        subject:`Demande attestation ${pct}% — ${progNom} — Lot ${lotN}`,
        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");
      // Marquer arch_email_sent
      await window.apiFetch(`/api/appels-eg/${createdAppel.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'));

  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:540,maxWidth:"96vw",maxHeight:"90vh",overflowY:"auto",boxShadow:"0 8px 40px #0003",border:"1px solid #e2e8f0"}}
        onClick={e=>e.stopPropagation()}>

        {/* ════ ÉTAPE 1 : Créer l'appel ════ */}
        {step===1&&<>
          <div style={{fontWeight:800,fontSize:17,marginBottom:4}}>💶 Convertir en appel de fonds</div>
          <div style={{fontSize:12,color:"#64748b",marginBottom:16}}>Notif : <b>{notif.subject}</b></div>

          {(meta.eg_pct||meta.eg_montant||meta.montant||meta.avancement_pct||_filePct||_fileLotNum!=null)&&(
            <div style={{background:"#fef3c7",border:"1px solid #fde68a",borderRadius:8,padding:"8px 12px",marginBottom:14,fontSize:12,color:"#92400e"}}>
              <b>Infos détectées :</b>
              {(meta.eg_pct||meta.avancement_pct||_filePct)&&<span style={{marginLeft:8}}>Avancement : <b>{meta.eg_pct||meta.avancement_pct||_filePct}%</b></span>}
              {_fileLotNum!=null&&<span style={{marginLeft:8}}>Lot : <b>Lot {_fileLotNum}</b></span>}
              {(meta.eg_montant||meta.montant||meta.montant_ttc)&&<span style={{marginLeft:8}}>Montant EG : <b>{Number(meta.eg_montant||meta.montant||meta.montant_ttc).toLocaleString("fr-FR")} €</b></span>}
            </div>
          )}

          <div style={{display:"flex",flexDirection:"column",gap:10,marginBottom:16}}>
            <div>
              <div style={{fontSize:11,fontWeight:700,color:"#64748b",marginBottom:3}}>Programme *</div>
              <select value={projId} onChange={e=>{setProjId(e.target.value);setLotIdx("");}} 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>
              <div style={{fontSize:11,fontWeight:700,color:"#64748b",marginBottom:3}}>Lot cible (statut VENDU) *</div>
              <select value={lotIdx} onChange={e=>setLotIdx(e.target.value)} style={inp} disabled={!projId||lots.length===0}>
                <option value="">— Sélectionner un lot —</option>
                {lots.map(({idx,lot})=>(
                  <option key={idx} value={String(idx)}>
                    Lot {idx+1}{lot.type?" ("+lot.type+")":""}{lot.clientNom?" — "+lot.clientNom:""} [{lot.statutCommercial}]
                  </option>
                ))}
              </select>
              {projId&&lots.length===0&&<div style={{fontSize:11,color:"#94a3b8",marginTop:3}}>Aucun lot VENDU dans ce programme.</div>}
            </div>
            <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:10}}>
              <div>
                <div style={{fontSize:11,fontWeight:700,color:"#64748b",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 : 10" style={inp}/>
              </div>
              <div>
                <div style={{fontSize:11,fontWeight:700,color:"#64748b",marginBottom:3}}>Montant EG (€)</div>
                <input type="number" min="0" step="0.01" value={egMontant} onChange={e=>setEgMontant(e.target.value)} placeholder="Ex : 45000" style={inp}/>
              </div>
            </div>
          </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:"#f1f5f9",border:"1px solid #e2e8f0",borderRadius:8,padding:"8px 18px",fontSize:13,cursor:"pointer"}}>Annuler</button>
            <button onClick={handleSubmit} 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?"⏳ Enregistrement…":"✓ Créer l'appel de fonds"}
            </button>
          </div>
        </>}

        {/* ════ ÉTAPE 2 : Email architecte ════ */}
        {step===2&&<>
          <div style={{fontWeight:800,fontSize:17,marginBottom:4}}>✅ Appel créé — Email architecte</div>
          <div style={{background:"#dcfce7",border:"1px solid #86efac",borderRadius:8,padding:"8px 12px",marginBottom:10,fontSize:12,color:"#15803d"}}>
            ✓ Appel de fonds <b>{createdAppel?.eg_pct}%</b> enregistré. Envoyez la facture EG à l'architecte pour demander son attestation.
          </div>
          {factureNotifId?(
            <div style={{background:"#eff6ff",border:"1px solid #bfdbfe",borderRadius:8,padding:"8px 12px",marginBottom:14,fontSize:12,color:"#1d4ed8"}}>
              🧾 Notification <b>facture</b> créée pour le paiement vers l'EG (n°{factureNotifId}). Elle apparaît dans l'onglet Notifications → Factures, prête à être réglée.
            </div>
          ):(
            <div style={{background:"#f1f5f9",border:"1px solid #e2e8f0",borderRadius:8,padding:"7px 12px",marginBottom:14,fontSize:11,color:"#64748b"}}>
              ℹ️ Aucune notification 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:"#64748b",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:"#64748b",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>
          )}
          {!egAttachment&&!loadingAttach&&(
            <div style={{background:"#f1f5f9",borderRadius:8,padding:"6px 12px",marginBottom:10,fontSize:11,color:"#64748b"}}>
              ℹ️ Aucune PJ trouvée sur la notification. L'email sera envoyé sans pièce jointe.
            </div>
          )}

          <div style={{marginBottom:14}}>
            <div style={{fontSize:11,color:"#64748b",marginBottom:4}}>Corps de l'email</div>
            <textarea value={emailArchBody} readOnly
              style={{background:"#f8fafc",border:"1px solid #e2e8f0",borderRadius:8,width:"100%",height:220,
                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&&<>
              <button onClick={onDone} style={{background:"#f1f5f9",border:"1px solid #e2e8f0",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>
            </>}
            {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>
        </>}

      </div>
    </div>
  );
}

/* ── Modale : Générer appel de fonds client (depuis attestation architecte) ──── */
function AppelClientEgModal({notif,attestFolderPath,projects,fiches,crm,rows,values,onClose,onDone}) {
  const meta=(notif.metadata)||{};
  const params=(notif.proposed_action&&notif.proposed_action.params)||{};

  // Sélection programme / lot (pré-rempli si metadata, fallback heuristique via params)
  // Sprint F-a : meta.programme_id/lot_idx vient du classifier (whitelist CRM, fiable).
  // params.programmeId/lotIdx vient du résultat enrichi de buildProposedAction()
  // qui ajoute la résolution heuristique (sujet + preview + noms de PJ). On lit
  // metadata d'abord (source de vérité), puis params en fallback (cas où le
  // classifier n'a pas su trouver mais l'heuristique oui).
  const preProjId=meta.programme_id||params.programmeId||"";
  const preLotIdx=meta.lot_idx!=null?String(meta.lot_idx)
                  :params.lotIdx!=null?String(params.lotIdx)
                  :"";
  const [projId,setProjId]=React.useState(preProjId);
  const [lotIdx,setLotIdx]=React.useState(preLotIdx);

  // Appel EG en attente pour ce lot
  const [appel,setAppel]=React.useState(null);
  const [appelLoading,setAppelLoading]=React.useState(false);
  const [appelErr,setAppelErr]=React.useState("");

  // PDF facture client
  const [generating,setGenerating]=React.useState(false);
  // Sprint Indivision-4 : tableau de 1 ou 2 entrées {pdf, filename, recipientEmail?, recipientName?, quotePartPct?, montant?, isPrincipal?}
  // - seul/couple : 1 entrée, sans champs recipientEmail (utilise clientEmail du lot)
  // - indivision : 2 entrées, chacune avec recipientEmail/recipientName/quotePartPct
  const [facturePdfs,setFacturePdfs]=React.useState([]);

  // PJ attestation
  const [attestPdf,setAttestPdf]=React.useState(null);
  const [loadingAttest,setLoadingAttest]=React.useState(false);

  // Email
  const [sending,setSending]=React.useState(false);
  const [sent,setSent]=React.useState(false);
  const [sendErr,setSendErr]=React.useState("");

  // ── Sprint C : modale de replanification (étape 5) ────────────────────────
  const [replanOpen,setReplanOpen]=React.useState(false);

  // ── Cohérence attestation ↔ appel (sprint 2026-05-21) ─────────────────────
  // Si l'avancement certifié dans le PDF (params.avancement_pct) ne correspond
  // pas au pct_delta de l'appel en DB, on affiche un récap et un bouton qui
  // PATCH l'appel (pct_delta + montant_client recalculé) pour réaligner avant
  // génération de la facture.
  const [updatingAppel,setUpdatingAppel]=React.useState(false);
  const [updateAppelErr,setUpdateAppelErr]=React.useState("");

  // ── Création d'un appel ad-hoc (cas historique : appel créé hors système) ─
  // Quand aucun appel en attente n'est trouvé, on propose à l'utilisateur de
  // saisir manuellement les infos pour créer un appel à la volée.
  const [noAppelPending,setNoAppelPending]=React.useState(false);
  const [adhocPct,setAdhocPct]=React.useState(params.pct_appele!=null?String(params.pct_appele):"");
  const [adhocMontant,setAdhocMontant]=React.useState("");
  const [adhocMontantManual,setAdhocMontantManual]=React.useState(false);
  const [adhocAttestPct,setAdhocAttestPct]=React.useState(params.avancement_pct!=null?String(params.avancement_pct):"");
  const [adhocCreating,setAdhocCreating]=React.useState(false);

  // Helper : calcule travauxReel (même formule que FicheDetail)
  const calcTravauxReelClient=React.useCallback((pId,lIdx)=>{
    try{
      const f=(fiches||{})[pId]||{};
      const lot=((f.lots)||[])[parseInt(lIdx,10)]||{};
      const toNl=v=>parseFloat(String(v||'').replace(/\s/g,'').replace(',','.'))||0;
      const prixAchat=toNl(f.prixAchat), devisTravaux=toNl(f.devisTravaux);
      const euribor=toNl(f.euribor), montantCredit=toNl(f.montantCredit);
      const creditMensuel=montantCredit*(euribor/100+0.025)/12;
      const montantCommerce=toNl(f.montantCommerceRdc);
      const prixDenormandie=prixAchat-montantCommerce;
      const creditTotal=creditMensuel*8;
      const achatFNI=prixDenormandie*1.025+toNl(f.dossierBanque)+creditTotal;
      const moe=devisTravaux*0.08;
      const montantTravaux=moe+toNl(f.dp)+toNl(f.pc)+toNl(f.plaquette)+toNl(f.deplacements)
        +toNl(f.avocat)+toNl(f.hommeArt)+toNl(f.geometre)+toNl(f.diagnostics)
        +toNl(f.loyerMeuble)+toNl(f.doTrc)+toNl(f.gfa)+devisTravaux;
      const prixTotalHorsMarge=achatFNI+montantTravaux;
      const prixReelVal=toNl(lot.prixReel);
      if(prixReelVal<=0||prixTotalHorsMarge<=0) return 0;
      const foncierReelCalc=prixReelVal*achatFNI/prixTotalHorsMarge;
      const foncierReelOv=lot.foncierReelO!==""&&lot.foncierReelO!=null?toNl(lot.foncierReelO):null;
      const foncierReel=foncierReelOv!==null?foncierReelOv:foncierReelCalc;
      return prixReelVal-foncierReel;
    }catch(_){return 0;}
  },[fiches]);


  const inp={background:"#f8fafc",border:"1px solid #cbd5e1",borderRadius:6,color:"#1e293b",padding:"6px 8px",fontSize:13,width:"100%"};

  // Lots VENDU
  const lots=React.useMemo(()=>{
    const fiche=fiches[projId];
    if(!fiche||!fiche.lots) return [];
    return fiche.lots.map((l,i)=>({idx:i,lot:l})).filter(({lot})=>
      isLotVenduOuAvance(lot.statutCommercial||"")
    );
  },[projId,fiches]);

  const fiche=fiches[projId]||{};
  const proj=(projects||[]).find(p=>p.id===projId)||{};
  const lotInfo=lots.find(l=>l.idx===parseInt(lotIdx));
  const lot=lotInfo?.lot||{};
  const programmeNom=[proj.ville,proj.nom].filter(Boolean).join(' ')||fiche.adresse||'';

  // Infos contact client + CGP
  // Email client : lookup CRM par clientId (le lot.clientEmail redondant a été
  // supprimé au Sprint A3 — source de vérité = crm.clients).
  const clientFromCrm=lot.clientId?(crm?.clients||[]).find(c=>c.id===lot.clientId):null;
  const clientEmail=clientFromCrm?.email||'';
  const cgpId=lot.cgpId||'';
  const cgp=(crm?.cgps||[]).find(c=>c.id===cgpId)||null;
  const cgpEmail=cgp?(cgp.emails||'').split(/[,;\s]/)[0].trim():'';

  // ── Sprint C : données pour la modale de replanification ─────────────────
  const lotN=lotIdx!==""?parseInt(lotIdx,10)+1:null;
  const lotRow=React.useMemo(()=>{
    if(!projId||lotN==null) return null;
    return (rows||[]).find(r=>r.projetId===projId&&r.label===`Lot ${lotN}`)||null;
  },[rows,projId,lotN]);
  const lotRowValues=React.useMemo(()=>{
    if(!lotRow) return [];
    return (values||[]).filter(v=>v.rowId===lotRow.id);
  },[values,lotRow]);
  const prixReelLot=Number(lot.prixReel||0);

  // Charger appel EG dès que projId+lotIdx sont connus
  React.useEffect(()=>{
    if(!projId||lotIdx===""){setAppel(null);setAppelErr("");setNoAppelPending(false);return;}
    const lotId=String(parseInt(lotIdx)+1);
    setAppelLoading(true); setAppelErr(""); setAppel(null); setNoAppelPending(false);
    fetch(`/api/appels-eg?programme_id=${encodeURIComponent(projId)}&lot_id=${encodeURIComponent(lotId)}`)
      .then(r=>r.json())
      .then(rows=>{
        const pending=(rows||[]).filter(r=>r.arch_email_sent&&!r.appel_client_sent);
        // Pas d'appel en attente : on n'arrête pas le flux, on propose une
        // création ad-hoc (cas historique d'appels créés hors système).
        if(pending.length===0) setNoAppelPending(true);
        else setAppel(pending[0]);
      })
      .catch(e=>setAppelErr(e.message))
      .finally(()=>setAppelLoading(false));
  },[projId,lotIdx]);

  // Auto-calcul montant_client ad-hoc : pct_delta × travauxReel / 100
  // Ne recalcule que si l'utilisateur n'a pas saisi le montant manuellement.
  React.useEffect(()=>{
    if(adhocMontantManual) return;
    const pct=parseFloat(adhocPct);
    if(isNaN(pct)||pct<=0||!projId||lotIdx==="") return;
    const travauxReel=calcTravauxReelClient(projId,lotIdx);
    if(travauxReel>0) setAdhocMontant((Math.round(travauxReel*pct/100*100)/100).toFixed(2));
  },[adhocPct,projId,lotIdx,adhocMontantManual,calcTravauxReelClient]);

  // Crée un appel ad-hoc via POST /api/appels-eg, marqué directement arch_email_sent=TRUE
  // pour qu'il prenne la place de l'appel en attente.
  const handleCreateAdhocAppel=async()=>{
    if(!projId||lotIdx===""){setAppelErr("Sélectionnez programme + lot d'abord.");return;}
    const pct=parseFloat(adhocPct);
    const montant=parseFloat(adhocMontant);
    const attestPct=adhocAttestPct?parseFloat(adhocAttestPct):pct;
    if(isNaN(pct)||pct<=0){setAppelErr("Pct delta invalide.");return;}
    if(isNaN(montant)||montant<=0){setAppelErr("Montant client invalide.");return;}
    setAdhocCreating(true); setAppelErr("");
    try{
      const lotId=String(parseInt(lotIdx,10)+1);
      const body={
        programme_id:projId,
        lot_id:lotId,
        pct_delta:pct,
        montant_client:montant,
        attestation_pct:attestPct,
        attestation_date:new Date().toISOString().slice(0,10),
        // Sprint F-f : lier l'appel ad-hoc à la notif d'attestation source.
        // On ne pose l'ID que si on a effectivement chargé une attestation
        // depuis cette notif (pattern identique à ligne 2332 / handleReplanConfirm).
        attest_notif_id:attestPdf?notif.id:null,
      };
      const r=await window.apiFetch("/api/appels-eg",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});
      const newAppel=await r.json();
      if(!r.ok||newAppel.error) throw new Error(newAppel.error||"Erreur création appel");
      // Marquer arch_email_sent=TRUE (l'appel ad-hoc saute l'étape 3 du workflow)
      await window.apiFetch(`/api/appels-eg/${newAppel.id}`,{
        method:"PATCH",headers:{"Content-Type":"application/json"},
        body:JSON.stringify({arch_email_sent:true,arch_email_date:new Date().toISOString()}),
      });
      setAppel({...newAppel,arch_email_sent:true});
      setNoAppelPending(false);
    }catch(e){setAppelErr(e.message);}
    setAdhocCreating(false);
  };

  // ── PATCH appel pour réaligner pct_delta / montant_client sur l'attestation
  // Appelé depuis le récap cohérence quand l'utilisateur clique « Mettre à jour ».
  // Recalcule montant_client = travauxReel × newPctDelta / 100.
  // Efface la facture déjà générée pour forcer une regénération sur les bonnes
  // valeurs.
  const handleUpdateAppel=async()=>{
    if(!appel||!projId||lotIdx==="") return;
    const avancAttest=parseFloat(params.avancement_pct!=null?params.avancement_pct:meta.avancement_pct);
    if(isNaN(avancAttest)){setUpdateAppelErr("Avancement attestation manquant — impossible de réaligner.");return;}
    const pctCumuleAppel=Number(appel.pct_cumule||0);
    // fix R6(b) : si l'opérateur a changé de lot, le pct_appele figé (calculé pour le lot
    // deviné) n'est plus valable → on recalcule depuis l'attestation et le cumul du lot choisi.
    const lotChanged=(projId!==preProjId)||(lotIdx!==preLotIdx);
    const newPctDelta=(!lotChanged&&params.pct_appele!=null)?Number(params.pct_appele):(avancAttest-pctCumuleAppel);
    if(!(newPctDelta>0)){setUpdateAppelErr("Le nouveau pct_delta serait ≤ 0 — vérifiez l'attestation.");return;}
    const travauxReel=calcTravauxReelClient(projId,lotIdx);
    if(!(travauxReel>0)){setUpdateAppelErr("Impossible de recalculer le montant (prixReel/foncier manquants).");return;}
    const newMontant=Math.round(travauxReel*newPctDelta/100*100)/100;
    setUpdatingAppel(true); setUpdateAppelErr("");
    try{
      const r=await window.apiFetch(`/api/appels-eg/${appel.id}`,{
        method:"PATCH",
        headers:{"Content-Type":"application/json"},
        body:JSON.stringify({
          pct_delta:newPctDelta,
          montant_client:newMontant,
          attestation_pct:avancAttest,
        }),
      });
      const d=await r.json();
      if(!r.ok||d.error) throw new Error(d.error||"Erreur PATCH appel");
      // PATCH ne retourne pas pct_cumule (champ calculé) — on le préserve
      setAppel({...d,pct_cumule:appel.pct_cumule});
      setFacturePdfs([]); // force la regénération avec les bons chiffres
    }catch(e){setUpdateAppelErr("Mise à jour échouée : "+e.message);}
    setUpdatingAppel(false);
  };

  // Charger attestation PDF depuis la notification
  const loadAttestation=React.useCallback(async()=>{
    setLoadingAttest(true);
    try{
      const resp=await fetch(`/api/agent/notifications/${notif.id}/attachment`);
      if(!resp.ok){setLoadingAttest(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='attestation.pdf';
      try{const ref=JSON.parse(notif.attachment_ref||'{}');filename=ref.filename||filename;}catch(_){}
      setAttestPdf({filename,contentType:'application/pdf',content:base64});
    }catch(e){console.warn('Chargement attestation échoué:',e.message);}
    setLoadingAttest(false);
  },[notif.id,notif.attachment_ref]);

  React.useEffect(()=>{loadAttestation();},[loadAttestation]);

  const handleGeneratePdf=async()=>{
    if(!appel) return;
    setGenerating(true); setSendErr("");
    try{
      const r=await window.apiFetch(`/api/appels-eg/${appel.id}/generate-facture-client`,{
        method:"POST",
        headers:{"Content-Type":"application/json"},
        body:JSON.stringify({folderPath:attestFolderPath||null}),
      });
      const d=await r.json();
      if(!r.ok||d.error) throw new Error(d.error||"Erreur génération");
      // Sprint Indivision-4 : la route peut renvoyer {pdf,filename} (legacy seul/couple)
      // ou {pdfs:[{pdf,filename,recipientEmail,recipientName,quotePartPct,...}]} (indivision).
      if(Array.isArray(d.pdfs)&&d.pdfs.length>0){
        setFacturePdfs(d.pdfs);
      }else if(d.pdf&&d.filename){
        setFacturePdfs([{pdf:d.pdf,filename:d.filename}]);
      }else{
        throw new Error("Réponse inattendue du serveur (pdf|pdfs manquant)");
      }
    }catch(e){setSendErr(e.message);}
    setGenerating(false);
  };

  const handlePreviewPdf=React.useCallback((idx)=>{
    const target=facturePdfs[idx??0];
    if(!target) return;
    const chars=atob(target.pdf);
    const bytes=new Uint8Array(chars.length);
    for(let i=0;i<chars.length;i++) bytes[i]=chars.charCodeAt(i);
    const blob=new Blob([bytes],{type:'application/pdf'});
    window.open(URL.createObjectURL(blob),'_blank');
  },[facturePdfs]);

  const emailBody=React.useMemo(()=>{
    if(!appel) return '';
    const isIndivision=facturePdfs.length===2;
    // Nom à utiliser dans la salutation
    let clientNom;
    if(isIndivision){
      clientNom=`${facturePdfs[0].recipientName||'[Acquéreur 1]'} et ${facturePdfs[1].recipientName||'[Acquéreur 2]'}`;
    }else{
      clientNom=[lot.clientNom,lot.conjointNomPrenom||lot.clientNomConjoint].filter(Boolean).join(' & ')||'[Client]';
    }
    const pct=appel.pct_delta||appel.eg_pct||0;
    const montant=appel.montant_client?Number(appel.montant_client).toLocaleString('fr-FR',{minimumFractionDigits:2,maximumFractionDigits:2}):'';
    const iban=fiche.ibanProgramme||'[IBAN À COMPLÉTER]';
    const bic=fiche.bic||'';
    const lotN=parseInt(lotIdx)+1;
    const fmtEur=n=>Number(n||0).toLocaleString('fr-FR',{minimumFractionDigits:2,maximumFractionDigits:2});

    // Bloc spécifique indivision : détail par investisseur (quote-part + montant)
    const indivisionBlock=isIndivision?`

Acquisition en indivision — détail par investisseur :
${facturePdfs.map(p=>`  • ${p.recipientName} — Quote-part ${p.quotePartPct}% — Montant dû ${fmtEur(p.montant)} €`).join('\n')}

Chaque investisseur trouvera sa facture nominative en pièce jointe.`:'';

    const facturesIntro=isIndivision
      ?`- Les factures correspondant à l'appel de fonds de ${pct}% des travaux${montant?`, d'un montant total de ${montant} €`:''}`
      :`- La facture correspondant à l'appel de fonds de ${pct}% des travaux${montant?`, d'un montant de ${montant} €`:''}`;

    return `Madame, Monsieur ${clientNom},

Suite à l'avancement des travaux du programme ${programmeNom}, nous avons le plaisir de vous adresser :
${facturesIntro}
- L'attestation d'avancement délivrée par l'architecte
${indivisionBlock}

INFORMATIONS :
─────────────────────────────────────────
  Programme   : ${programmeNom}
  Lot N°      : ${lotN}${lot.type?` (${lot.type})`:''}
  Avancement  : ${pct}%${montant?`\n  Montant     : ${montant} €`:''}
─────────────────────────────────────────

Conformément à nos conditions générales, nous vous demandons de bien vouloir procéder au règlement à réception par virement :
  IBAN : ${iban}${bic?`\n  BIC  : ${bic}`:''}

Nous restons à votre disposition pour tout renseignement.

Cordialement,`;
  },[appel,lot,lotIdx,programmeNom,fiche,facturePdfs]);

  // ── Étape 5 (Sprint C) ────────────────────────────────────────────────────
  // Le clic sur « Envoyer » ouvre la modale de replanification du plan. Elle
  // appelle ensuite handleReplanConfirm qui exécute /replan (atomique : maj plan
  // + appel_client_sent=TRUE) puis envoie l'email.
  // Sprint Indivision-4 : en indivision, on utilise les recipientEmail renvoyés par
  // le backend ; sinon (seul/couple) on utilise clientEmail du lot.
  const isIndivisionFactures=facturePdfs.length===2;
  const recipientsTo=React.useMemo(()=>{
    if(isIndivisionFactures){
      return facturePdfs.map(p=>p.recipientEmail).filter(Boolean);
    }
    return clientEmail?[clientEmail]:[];
  },[isIndivisionFactures,facturePdfs,clientEmail]);

  const handleSend=()=>{
    if(facturePdfs.length===0){setSendErr("Générez d'abord la facture PDF.");return;}
    if(isIndivisionFactures){
      if(recipientsTo.length<2){
        const missing=facturePdfs.filter(p=>!p.recipientEmail).map(p=>p.recipientName||'?').join(', ');
        setSendErr(`Email manquant pour l'un des investisseurs (indivision) : ${missing}. Renseignez l'email dans la fiche CRM.`);
        return;
      }
    }else{
      if(!clientEmail){setSendErr("Email client introuvable sur ce lot.");return;}
    }
    if(!lotRow){setSendErr(`Ligne « Lot ${lotN} » introuvable dans le plan de trésorerie — impossible de replanifier.`);return;}
    if(!(prixReelLot>0)){setSendErr("prixReel du lot non renseigné — impossible de vérifier l'invariant.");return;}
    setSendErr("");
    setReplanOpen(true);
  };

  // Appelé par AppelReplanModal après validation utilisateur.
  // 1. POST /api/appels-eg/:id/replan (transaction : update plan_values + set appel_client_sent)
  // 2. Si OK : ferme la modale, envoie l'email, marque sent, appelle onDone.
  // Si l'email échoue après /replan : l'appel est marqué sent en DB ; on remonte
  // l'erreur au parent pour permettre une action manuelle (sans rejouer /replan).
  const handleReplanConfirm=async(changes,attestNotifIdArg)=>{
    // 1. /replan (atomique côté serveur)
    const rr=await window.apiFetch(`/api/appels-eg/${appel.id}/replan`,{
      method:"POST",headers:{"Content-Type":"application/json"},
      body:JSON.stringify({changes,attestNotifId:attestNotifIdArg||null}),
    });
    const rd=await rr.json();
    if(!rr.ok||rd.error) throw new Error(rd.error||"Erreur lors de la replanification");

    // Sprint F-d : l'appel vient de passer appel_client_sent=TRUE, donc pct_cumule
    // change côté DB. On signale aux autres composants (FichesProgrammes notamment)
    // qu'ils doivent rafraîchir le badge % avancement.
    window.dispatchEvent(new CustomEvent('appels-eg-updated',{detail:{programme_id:projId,lot_id:String(parseInt(lotIdx,10)+1)}}));

    // 2. Email — fermeture de la modale avant l'envoi pour rendre la main visuellement
    setReplanOpen(false);
    setSending(true);
    try{
      // Sprint Indivision-4 : 1 ou 2 factures + attestation. `to` = liste des destinataires
      // séparés par virgule (Graph API et Nodemailer acceptent ce format nativement).
      const attachments=facturePdfs.map(p=>({filename:p.filename,contentType:'application/pdf',content:p.pdf}));
      if(attestPdf) attachments.push({filename:attestPdf.filename,contentType:attestPdf.contentType,content:attestPdf.content});
      const pct=appel.pct_delta||appel.eg_pct||0;
      const toField=recipientsTo.join(', ');
      const payload={
        to:toField,
        subject:`Appel de fonds ${pct}% travaux — ${programmeNom} — Lot ${lotN}`,
        body:emailBody,
        attachments,
      };
      if(cgpEmail) payload.cc=cgpEmail;
      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");
      setSent(true);
      await onDone();
    }catch(e){
      // L'appel est déjà marqué envoyé côté DB (par /replan). L'email lui n'est
      // pas parti. On le signale clairement à l'utilisateur.
      setSendErr(`Plan mis à jour avec succès, mais l'envoi email a échoué : ${e.message}. L'appel est marqué comme envoyé en base. Renvoyez le mail manuellement si nécessaire.`);
      setSending(false);
    }
  };

  const projs=(projects||[]).filter(p=>!p.isGlobal&&(!p.statut||p.statut==='En cours'));

  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:560,maxWidth:"96vw",maxHeight:"90vh",overflowY:"auto",boxShadow:"0 8px 40px #0003",border:"1px solid #e2e8f0"}}
        onClick={e=>e.stopPropagation()}>

        <div style={{fontWeight:800,fontSize:17,marginBottom:4}}>💶 Générer appel de fonds client</div>
        <div style={{fontSize:12,color:"#64748b",marginBottom:14}}>Attestation reçue : <b>{notif.subject}</b></div>

        {/* Sélection programme + lot */}
        <div style={{display:"flex",flexDirection:"column",gap:10,marginBottom:14}}>
          <div>
            <div style={{fontSize:11,fontWeight:700,color:"#64748b",marginBottom:3}}>Programme *</div>
            <select value={projId} onChange={e=>{setProjId(e.target.value);setLotIdx("");setAppel(null);setAppelErr("");setFacturePdfs([]);}} style={inp}>
              <option value="">— Sélectionner —</option>
              {projs.map(p=><option key={p.id} value={p.id}>{p.ville?p.ville+" – ":""}{p.nom}</option>)}
            </select>
          </div>
          <div>
            <div style={{fontSize:11,fontWeight:700,color:"#64748b",marginBottom:3}}>Lot *</div>
            <select value={lotIdx} onChange={e=>{setLotIdx(e.target.value);setAppel(null);setAppelErr("");setFacturePdfs([]);}} style={inp} disabled={!projId}>
              <option value="">— Sélectionner —</option>
              {lots.map(({idx,lot})=>(
                <option key={idx} value={String(idx)}>
                  Lot {idx+1}{lot.type?" ("+lot.type+")":""}{lot.clientNom?" — "+lot.clientNom:""} [{lot.statutCommercial}]
                </option>
              ))}
            </select>
          </div>
        </div>

        {appelLoading&&<div style={{fontSize:12,color:"#64748b",marginBottom:10}}>⏳ Recherche de l'appel de fonds…</div>}
        {appelErr&&<div style={{background:"#fee2e2",border:"1px solid #fca5a5",borderRadius:8,padding:"8px 12px",marginBottom:12,fontSize:12,color:"#dc2626"}}>❌ {appelErr}</div>}

        {/* ── Cas historique : pas d'appel en attente → création ad-hoc ── */}
        {noAppelPending&&!appel&&(
          <div style={{background:"#fef9c3",border:"1px solid #fde68a",borderRadius:8,padding:"10px 12px",marginBottom:12,fontSize:12}}>
            <div style={{fontWeight:700,color:"#92400e",marginBottom:6}}>⚠️ Aucun appel de fonds en attente pour ce lot.</div>
            <div style={{color:"#78350f",marginBottom:10,lineHeight:1.5}}>
              Si cet email correspond à un appel créé manuellement (cas historique, antérieur à l'automatisation), saisissez les informations ci-dessous pour le rattacher au système :
            </div>
            <div style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:8,marginBottom:8}}>
              <div>
                <div style={{fontSize:10,fontWeight:700,color:"#78350f",marginBottom:2}}>Pct delta (%) *</div>
                <input type="number" step="0.01" min="0" value={adhocPct} onChange={e=>setAdhocPct(e.target.value)}
                  placeholder="25" style={{...inp,padding:"5px 8px",fontSize:12}}/>
              </div>
              <div>
                <div style={{fontSize:10,fontWeight:700,color:"#78350f",marginBottom:2}}>Montant client (€) *{!adhocMontantManual&&adhocMontant?<span style={{fontWeight:400,color:"#92400e",marginLeft:4}}>(calculé)</span>:null}</div>
                <input type="number" step="0.01" min="0" value={adhocMontant} onChange={e=>{setAdhocMontantManual(true);setAdhocMontant(e.target.value);}}
                  placeholder="25000" style={{...inp,padding:"5px 8px",fontSize:12}}/>
              </div>
              <div>
                <div style={{fontSize:10,fontWeight:700,color:"#78350f",marginBottom:2}}>Attestation pct (%)</div>
                <input type="number" step="0.01" min="0" value={adhocAttestPct} onChange={e=>setAdhocAttestPct(e.target.value)}
                  placeholder="(par défaut = pct delta)" style={{...inp,padding:"5px 8px",fontSize:12}}/>
              </div>
            </div>
            <button onClick={handleCreateAdhocAppel} disabled={adhocCreating||!adhocPct||!adhocMontant}
              style={{background:adhocCreating||!adhocPct||!adhocMontant?"#94a3b8":"#0369a1",color:"#fff",border:"none",borderRadius:6,padding:"6px 14px",cursor:adhocCreating||!adhocPct||!adhocMontant?"default":"pointer",fontSize:12,fontWeight:700}}>
              {adhocCreating?"⏳ Création…":"+ Créer un appel ad-hoc"}
            </button>
          </div>
        )}

        {appel&&!appelErr&&<>
          {/* Récapitulatif appel */}
          <div style={{background:"#fef3c7",border:"1px solid #fde68a",borderRadius:8,padding:"10px 12px",marginBottom:12,fontSize:12}}>
            <b>Appel :</b> Lot {parseInt(lotIdx)+1} · <b>{appel.pct_delta||appel.eg_pct}%</b>
            {appel.montant_client&&<span style={{marginLeft:8}}>· {Number(appel.montant_client).toLocaleString("fr-FR",{minimumFractionDigits:2,maximumFractionDigits:2})} €</span>}
          </div>

          {/* ── Cohérence attestation ↔ appel ── */}
          {(()=>{
            const avancAttestRaw=params.avancement_pct!=null?params.avancement_pct:meta.avancement_pct;
            const avancAttest=parseFloat(avancAttestRaw);
            if(isNaN(avancAttest)) return null;
            const pctCumuleAppel=Number(appel.pct_cumule||0);
            const pctDeltaAppel=Number(appel.pct_delta||appel.eg_pct||0);
            const deltaTheorique=Math.round((avancAttest-pctCumuleAppel)*100)/100;
            // fix R6(b) : pct_appele figé invalide si le lot a changé → on retombe sur le calcul live.
            const lotChanged=(projId!==preProjId)||(lotIdx!==preLotIdx);
            const pctAppeleSaisi=(!lotChanged&&params.pct_appele!=null)?Number(params.pct_appele):deltaTheorique;
            const ecart=Math.round((pctAppeleSaisi-pctDeltaAppel)*100)/100;
            const isCoherent=Math.abs(ecart)<0.01;
            const travauxReel=calcTravauxReelClient(projId,lotIdx);
            const newMontant=travauxReel>0?Math.round(travauxReel*pctAppeleSaisi/100*100)/100:null;
            const bg=isCoherent?"#f0fdf4":"#fef2f2";
            const bd=isCoherent?"#86efac":"#f87171";
            const titleC=isCoherent?"#15803d":"#b91c1c";
            return(
              <div style={{background:bg,border:`${isCoherent?1:2}px solid ${bd}`,borderRadius:8,padding:"10px 12px",marginBottom:12,fontSize:12}}>
                <div style={{fontWeight:800,marginBottom:6,color:titleC,fontSize:isCoherent?12:14}}>
                  {isCoherent?"✅ Cohérence attestation ↔ appel":"⚠️ ANOMALIE — l'EG et l'architecte ne sont pas d'accord"}
                </div>
                {!isCoherent&&(
                  <div style={{color:"#7f1d1d",marginBottom:8,lineHeight:1.45}}>
                    Le % certifié par l'architecte ne correspond pas à celui de l'appel issu de l'entreprise générale.
                    En principe cela ne devrait pas arriver — vérifie l'attestation et la facture EG avant de continuer.
                    Si l'architecte fait foi, clique « Mettre à jour » pour aligner l'appel sur son %.
                  </div>
                )}
                <div style={{display:"grid",gridTemplateColumns:"auto auto",columnGap:14,rowGap:3,fontSize:11,lineHeight:1.5}}>
                  <span style={{color:"#475569"}}>Avancement attestation :</span><b>{avancAttest}%</b>
                  <span style={{color:"#475569"}}>Cumul déjà appelé :</span><b>{pctCumuleAppel}%</b>
                  <span style={{color:"#475569"}}>Delta théorique :</span><b>{deltaTheorique}%</b>
                  <span style={{color:"#475569"}}>Delta de cet appel (DB) :</span>
                  <b style={{color:isCoherent?"inherit":"#dc2626"}}>
                    {pctDeltaAppel}%
                    {!isCoherent&&pctAppeleSaisi!==pctDeltaAppel&&
                      <span style={{fontWeight:400,marginLeft:6,color:"#7c4700"}}>(voulu : {pctAppeleSaisi}%)</span>}
                  </b>
                  {!isCoherent&&<>
                    <span style={{color:"#475569"}}>Écart :</span>
                    <b style={{color:"#dc2626"}}>{ecart>0?"+":""}{ecart}%</b>
                  </>}
                </div>
                {!isCoherent&&newMontant!==null&&pctAppeleSaisi>0&&(
                  <div style={{marginTop:10}}>
                    <button onClick={handleUpdateAppel} disabled={updatingAppel}
                      style={{background:updatingAppel?"#94a3b8":"#0369a1",color:"#fff",border:"none",borderRadius:6,padding:"6px 14px",fontSize:12,fontWeight:700,cursor:updatingAppel?"default":"pointer"}}>
                      {updatingAppel?"⏳ Mise à jour…":`↻ Mettre à jour l'appel à ${pctAppeleSaisi}% (${fmtEur(newMontant)} €)`}
                    </button>
                    <div style={{fontSize:10,color:"#7c4700",marginTop:4,lineHeight:1.4}}>
                      pct_delta et montant_client seront recalculés ; la facture devra être regénérée.
                    </div>
                    {updateAppelErr&&<div style={{background:"#fee2e2",border:"1px solid #fca5a5",borderRadius:6,padding:"6px 10px",fontSize:11,color:"#dc2626",marginTop:6}}>❌ {updateAppelErr}</div>}
                  </div>
                )}
              </div>
            );
          })()}

          {/* PJ attestation */}
          <div style={{marginBottom:10}}>
            {loadingAttest&&<div style={{fontSize:11,color:"#64748b"}}>⏳ Chargement attestation…</div>}
            {attestPdf&&!loadingAttest&&(
              <div style={{background:"#ecfdf5",border:"1px solid #6ee7b7",borderRadius:6,padding:"6px 10px",fontSize:11}}>
                📎 Attestation : <b>{attestPdf.filename}</b>
              </div>
            )}
            {!attestPdf&&!loadingAttest&&(
              <div style={{background:"#fef9c3",border:"1px solid #fde68a",borderRadius:6,padding:"6px 10px",fontSize:11,color:"#92400e"}}>
                ⚠️ Aucune PJ attestation sur cette notification.
              </div>
            )}
          </div>

          {/* Infos client — en indivision, on affiche les 2 destinataires */}
          <div style={{background:"#f0f9ff",border:"1px solid #bae6fd",borderRadius:8,padding:"8px 12px",marginBottom:12,fontSize:12}}>
            {isIndivisionFactures?(<>
              <b>Indivision — destinataires :</b>
              {facturePdfs.map((p,i)=>(
                <div key={i} style={{fontFamily:"monospace",marginTop:2}}>
                  • {p.recipientName} — {p.recipientEmail||"⚠️ Email manquant"} ({p.quotePartPct}%)
                </div>
              ))}
            </>):(<>
              <b>Client :</b> <span style={{fontFamily:"monospace"}}>{clientEmail||"⚠️ Email manquant"}</span>
            </>)}
            {cgpEmail&&<><br/><b>CGP (CC) :</b> <span style={{fontFamily:"monospace"}}>{cgpEmail}</span></>}
          </div>

          {/* Générer PDF(s) */}
          {facturePdfs.length===0?(
            <div style={{marginBottom:14}}>
              <button onClick={handleGeneratePdf} disabled={generating}
                style={{background:generating?"#94a3b8":"#7c3aed",color:"#fff",border:"none",borderRadius:8,padding:"8px 18px",fontSize:13,fontWeight:700,cursor:generating?"default":"pointer"}}>
                {generating?"⏳ Génération…":"📄 Générer la facture client PDF"}
              </button>
            </div>
          ):(
            <div style={{background:"#eff6ff",border:"1px solid #bfdbfe",borderRadius:8,padding:"6px 12px",marginBottom:12,fontSize:12,display:"flex",flexDirection:"column",gap:6}}>
              {facturePdfs.map((p,i)=>(
                <div key={i} style={{display:"flex",alignItems:"center",justifyContent:"space-between",gap:8}}>
                  <span>✅ Facture {facturePdfs.length===2?`(${p.quotePartPct}%) `:''}générée : <b>{p.filename}</b></span>
                  <button onClick={()=>handlePreviewPdf(i)}
                    style={{background:"#2563eb",color:"#fff",border:"none",borderRadius:6,padding:"4px 12px",fontSize:12,fontWeight:600,cursor:"pointer",whiteSpace:"nowrap"}}>
                    👁 Prévisualiser
                  </button>
                </div>
              ))}
            </div>
          )}

          {/* Corps email */}
          <div style={{marginBottom:14}}>
            <div style={{fontSize:11,color:"#64748b",marginBottom:4}}>Corps de l'email client</div>
            <textarea value={emailBody} readOnly
              style={{background:"#f8fafc",border:"1px solid #e2e8f0",borderRadius:8,width:"100%",height:200,
                fontFamily:"'Courier New',monospace",fontSize:11,padding:"8px",resize:"vertical",lineHeight:1.5}}/>
          </div>

          {sendErr&&<div style={{background:"#fee2e2",border:"1px solid #fca5a5",borderRadius:8,padding:"8px 12px",marginBottom:10,fontSize:12,color:"#dc2626"}}>❌ {sendErr}</div>}
          {sent&&<div style={{background:"#dcfce7",border:"1px solid #86efac",borderRadius:8,padding:"10px 12px",marginBottom:10,fontSize:12,color:"#15803d"}}>✅ Email envoyé au client.</div>}

          <div style={{display:"flex",gap:8,justifyContent:"flex-end"}}>
            <button onClick={onClose} style={{background:"#f1f5f9",border:"1px solid #e2e8f0",borderRadius:8,padding:"8px 18px",fontSize:13,cursor:"pointer"}}>
              {sent?"Fermer":"Annuler"}
            </button>
            {!sent&&(()=>{
              // Sprint Indivision-4 : conditions de désactivation
              // - seul/couple : !clientEmail (1 destinataire requis)
              // - indivision  : recipientsTo.length < 2 (les 2 emails requis)
              const missingEmail=isIndivisionFactures?(recipientsTo.length<2):(!clientEmail);
              const disabled=sending||facturePdfs.length===0||missingEmail||!lotRow||!(prixReelLot>0);
              return(
                <button onClick={handleSend} disabled={disabled}
                  style={{background:disabled?"#94a3b8":"#0369a1",color:"#fff",border:"none",borderRadius:8,padding:"8px 22px",fontSize:13,fontWeight:700,cursor:disabled?"default":"pointer"}}>
                  {sending?"⏳ Envoi email…":"📅 Replanifier & envoyer"}
                </button>
              );
            })()}
          </div>
        </>}

        {!appel&&!appelLoading&&!appelErr&&(projId===""||lotIdx==="")&&(
          <div style={{textAlign:"right",marginTop:16}}>
            <button onClick={onClose} style={{background:"#f1f5f9",border:"1px solid #e2e8f0",borderRadius:8,padding:"8px 18px",fontSize:13,cursor:"pointer"}}>Fermer</button>
          </div>
        )}

      </div>
    </div>

    {/* Sprint C — modale de replanification (étape 5) */}
    {replanOpen&&appel&&lotRow&&prixReelLot>0&&(
      <AppelReplanModal
        appel={appel}
        prixReel={prixReelLot}
        currentRowValues={lotRowValues}
        lotN={lotN}
        programmeNom={programmeNom}
        attestNotifId={attestPdf?notif.id:null}
        onConfirm={handleReplanConfirm}
        onCancel={()=>setReplanOpen(false)}
      />
    )}
  </>);
}
