function MainApp({data,soldeInitial,savedFiches,savedCrm,savedJournal,savedArchRequests,savedReconciliations,savedReceptions,onReset,loggedUser,onLogout,initialVersion,initialDomainVersions,onVersionChange,onDomainVersionChange,dirtyRef}) {
  const [projects,setProjects]=useState(data.projects);
  const [rows,setRows]=useState(data.rows);
  const [values,setValues]=useState(data.values);
  const [fiches,setFiches]=useState(()=>{
    if(savedFiches) return savedFiches;
    const f={};data.projects.forEach(p=>{f[p.id]=EMPTY_FICHE();});return f;
  });
  const [crm,setCrm]=useState(()=>{
    const c=savedCrm||{};
    return {entreprises:[],clients:[],cgps:[],banques:[],comptes:[],...c};
  });
  const [journal,setJournal]=useState(()=>savedJournal||[]);
  const [reconciliations,setReconciliations]=useState(()=>savedReconciliations||{});
  const [receptions,setReceptions]=useState(()=>savedReceptions||[]);
  const [deleteProjId,setDeleteProjId]=useState(null);

  // ── Sprint A4 (étape 4) : versions par domaine + save granulaire ──────────
  // initialDomainVersions provient du GET /api/state enrichi (cf. App
  // component). _versionRef garde la version globale (app_state.updated_at)
  // pour le PUT global rétro-compat utilisé par receptions (résiduel A4).
  const _planVerRef       = useRef((initialDomainVersions && initialDomainVersions.plan)   || null);
  const _fichesVerRef     = useRef((initialDomainVersions && initialDomainVersions.fiches) || null);
  const _crmVerRef        = useRef((initialDomainVersions && initialDomainVersions.crm)    || null);
  const _miscVerRef       = useRef((initialDomainVersions && initialDomainVersions.misc)   || null);
  const _versionRef       = useRef(initialVersion || null);
  const _receptionsTimer  = useRef(null);
  const _receptionsMounted= useRef(false);
  const _receptionsConflict = useRef(false);

  // ── 3 hooks granulaires (le 4e, 'misc', est déclaré APRÈS archRequests) ──
  // payload mémoïsé pour éviter de retrigger le useEffect interne à chaque
  // render (React compare par identité de référence sur le deps array).
  const planPayload = React.useMemo(
    () => ({ projects, rows, values }),
    [projects, rows, values]
  );
  useDomainSave({
    domain:'plan', payloadKey:'plan',
    payload: planPayload,
    versionRef:_planVerRef, dirtyRef,
    onVersionChange: onDomainVersionChange,
  });
  useDomainSave({
    domain:'fiches', payloadKey:'fiches',
    payload: fiches,
    versionRef:_fichesVerRef, dirtyRef,
    onVersionChange: onDomainVersionChange,
  });
  useDomainSave({
    domain:'crm', payloadKey:'crm',
    payload: crm,
    versionRef:_crmVerRef, dirtyRef,
    onVersionChange: onDomainVersionChange,
  });

  /* ── Attestations : suivi des demandes envoyées à l'architecte ── */
  const [archRequests,setArchRequests]=useState(()=>Array.isArray(savedArchRequests)?savedArchRequests:[]);

  // ── Sprint A4 (étape 4) : useDomainSave('misc') ──────────────────────────
  // Placé APRÈS la déclaration de archRequests (TDZ avec const, contournée
  // historiquement par hoisting Babel-standalone, mais on garde l'ordre
  // logique pour ne pas perpétuer le piège).
  const miscPayload = React.useMemo(
    () => ({ soldeInitial, journal, archRequests, reconciliations }),
    [soldeInitial, journal, archRequests, reconciliations]
  );
  useDomainSave({
    domain:'misc', payloadKey:'misc',
    payload: miscPayload,
    versionRef:_miscVerRef, dirtyRef,
    onVersionChange: onDomainVersionChange,
  });

  // ── receptions : résiduel A4, transite par le PUT global /api/state ──────
  // (sprint Réception futur le migrera en granulaire). Le PUT global envoie
  // le state complet car syncTables fait des DELETE inconditionnels sur
  // projects (cf. routes/state.js §syncTables) — passer un payload partiel
  // wiperait toute la base.
  useEffect(() => {
    if (!_receptionsMounted.current) { _receptionsMounted.current = true; return; }
    if (dirtyRef) dirtyRef.current = true;
    clearTimeout(_receptionsTimer.current);
    _receptionsTimer.current = setTimeout(async () => {
      try {
        const resp = await fetch('/api/state', {
          method:'PUT',
          headers:{'Content-Type':'application/json'},
          body: JSON.stringify({
            data:{projects,rows,values,fiches,soldeInitial,crm,journal,archRequests,reconciliations,receptions},
            version:_versionRef.current,
          }),
        });
        if (resp.status === 409) {
          if (_receptionsConflict.current) return;
          _receptionsConflict.current = true;
          alert("Un autre utilisateur a modifié les données en même temps que vous.\n\nPour éviter tout écrasement, la page va se recharger avec la dernière version.\n\nVos modifications les plus récentes (non encore sauvegardées) pourraient être perdues.");
          window.location.reload();
          return;
        }
        if (resp.status === 401) {
          alert("Votre session a expiré. La page va se recharger pour vous reconnecter.");
          window.location.reload();
          return;
        }
        if (resp.ok) {
          const body = await resp.json();
          if (body && body.version) {
            _versionRef.current = body.version;
            if (onVersionChange) onVersionChange(body.version);
          }
          if (dirtyRef) dirtyRef.current = false;
        } else {
          console.error('[receptions PUT global] HTTP', resp.status);
        }
      } catch (e) {
        console.error('[receptions PUT global] Erreur réseau :', e);
      }
    }, 1200);
  }, [receptions]);
  const [pendingAttestations,setPendingAttestations]=useState([]);
  const [showAttestationList,setShowAttestationList]=useState(false);
  const [attestationAppel,setAttestationAppel]=useState(null);

  useEffect(()=>{
    const checkAttestations=async()=>{
      try{
        const r=await fetch('/api/agent/attestations');
        if(r.ok){const data=await r.json();if(Array.isArray(data))setPendingAttestations(data);}
      }catch(e){}
    };
    checkAttestations();
    const t=setInterval(checkAttestations,86400000);
    return ()=>clearInterval(t);
  },[]);

  const handleArchitecteEmailSent=(info)=>{
    setArchRequests(reqs=>[...reqs.filter(r=>!(r.projId===info.projId&&r.lotIdx===info.lotIdx)),info]);
  };

  const [user,setUser]=useState("Direction");
  const [page,setPage]=useState(()=>{try{return sessionStorage.getItem('tresoimmo_page')||'plan';}catch{return 'plan';}});
  useEffect(()=>{try{sessionStorage.setItem('tresoimmo_page',page);}catch{}},[page]);
  const [fichePage,setFichePage]=useState(null);

  /* ── Notifications agent email ── */
  const [notifications,setNotifications]=useState([]);
  const [newNotifCount,setNewNotifCount]=useState(0);
  const [notifLoading,setNotifLoading]=useState(false);
  const [notifError,setNotifError]=useState(null);
  const [notifFilterBal,setNotifFilterBal]=useState('all');
  const [notifFilterCat,setNotifFilterCat]=useState('all');
  const [notifShowAll,setNotifShowAll]=useState(false);
  const [notifPdfModal,setNotifPdfModal]=useState(null);
  const [analyzingIds,setAnalyzingIds]=useState({});
  const [editingFacture,setEditingFacture]=useState(null);
  const [reglerModal,setReglerModal]=useState(null);
  const [agentRunning,setAgentRunning]=useState(false);

  const loadNotifs=useCallback(async(all)=>{
    setNotifLoading(true);
    setNotifError(null);
    try{
      const qs=all?'':'?status=new';
      const r=await fetch('/api/agent/notifications'+qs);
      if(r.ok){
        const d=await r.json();
        if(Array.isArray(d)){
          const normalized=d.map(n=>({
            ...n,
            proposed_action: typeof n.proposed_action==='string'
              ? (()=>{try{return JSON.parse(n.proposed_action);}catch{return null;}})()
              : n.proposed_action,
          }));
          console.log('[notifs] chargées:',normalized.length,'— proposed_action sample:',
            normalized.slice(0,3).map(n=>({id:n.id,cat:n.category,pa:n.proposed_action})));
          setNotifications(normalized);
          setNewNotifCount(normalized.filter(n=>n.status==='new').length);
        }
      }else{
        const body=await r.json().catch(()=>({}));
        const msg=body.error||`Erreur HTTP ${r.status}`;
        setNotifError(msg);
        console.error('GET /api/agent/notifications →',r.status,msg);
      }
    }catch(e){
      setNotifError('Erreur réseau : '+e.message);
      console.error('GET /api/agent/notifications →',e);
    }
    setNotifLoading(false);
  },[]);

  useEffect(()=>{
    fetch('/api/agent/notifications?status=new')
      .then(r=>r.ok?r.json():[]).then(d=>{if(Array.isArray(d))setNewNotifCount(d.length);}).catch(()=>{});
  },[]);

  useEffect(()=>{if(page==='notifications')loadNotifs(notifShowAll);},[page,notifShowAll,loadNotifs]);

  /* ── handleCellSave : mutation partagée Plan + Notifications ── */
  const setValue=useCallback((rowId,wk,montant)=>{
    setValues(vs=>{
      const idx=vs.findIndex(v=>v.rowId===rowId&&v.weekIso===wk);
      if(montant===null) return vs.filter((_,i)=>i!==idx);
      if(idx>=0) return vs.map((v,i)=>i===idx?{...v,montant}:v);
      return [...vs,{rowId,weekIso:wk,montant}];
    });
  },[]);

  const handleCellSave=useCallback((rowId,weekIso,newVal,redistribution,additive)=>{
    if(additive){
      // Mode additif (règlement de factures) : on AJOUTE au montant déjà présent
      // dans la cellule au lieu de l'écraser. Plusieurs factures sur la même
      // ligne/semaine se cumulent. Updater fonctionnel → robuste si on règle
      // plusieurs factures à la suite (toujours l'état le plus récent).
      setValues(vs=>{
        const idx=vs.findIndex(v=>v.rowId===rowId&&v.weekIso===weekIso);
        const existing=idx>=0?(Number(vs[idx].montant)||0):0;
        const sum=existing+newVal;
        if(sum===0) return idx>=0?vs.filter((_,i)=>i!==idx):vs;
        if(idx>=0) return vs.map((v,i)=>i===idx?{...v,montant:sum}:v);
        return [...vs,{rowId,weekIso,montant:sum}];
      });
    } else {
      setValue(rowId,weekIso,newVal);
    }
    if(redistribution){
      setValues(vs=>{
        const idx=vs.findIndex(v=>v.rowId===rowId&&v.weekIso===redistribution.weekIso);
        const existing=idx>=0?vs[idx].montant:0;
        const adjusted=existing+redistribution.adjustment;
        if(adjusted===0){
          return idx>=0?vs.filter((_,i)=>i!==idx):vs;
        }
        if(idx>=0) return vs.map((v,i)=>i===idx?{...v,montant:adjusted}:v);
        return [...vs,{rowId,weekIso:redistribution.weekIso,montant:adjusted}];
      });
    }
  },[setValue]);

  const updateFiche=(projId,field,val)=>{
    if(field==="ville") setProjects(ps=>ps.map(p=>p.id===projId?{...p,ville:val}:p));
    if(field==="nomProgramme") setProjects(ps=>ps.map(p=>p.id===projId?{...p,nom:val}:p));
    setFiches(f=>{
      const curr=f[projId]||EMPTY_FICHE();
      const updated={...curr,[field]:val};
      if(field==="nbLots"){
        const n=parseInt(val)||0;
        const currentLots=curr.lots||[];
        if(n>currentLots.length){
          const extra=Array.from({length:n-currentLots.length},()=>EMPTY_LOT());
          updated.lots=[...currentLots,...extra];
        } else {
          updated.lots=currentLots.slice(0,n);
        }
      }
      return {...f,[projId]:updated};
    });
    if(field==="nbLots"){
      const n=parseInt(val)||0;
      setRows(rs=>{
        const projLotRows=rs.filter(r=>r.projetId===projId&&/^Lot \d+$/.test(r.label));
        const currentCount=projLotRows.length;
        if(n>currentCount){
          const newLotRows=Array.from({length:n-currentCount},(_,i)=>({id:uid(),projetId:projId,label:`Lot ${currentCount+i+1}`,statut:"F"}));
          return [...rs,...newLotRows];
        } else if(n<currentCount){
          let kept=0;
          return rs.filter(r=>{
            if(r.projetId===projId&&/^Lot \d+$/.test(r.label)){kept++;return kept<=n;}
            return true;
          });
        }
        return rs;
      });
    }
  };

  const createProgramme=({ville,nom})=>{
    const id=uid();
    const colorIdx=projects.filter(p=>!p.isGlobal).length;
    setProjects(ps=>[...ps,{id,ville:ville.trim(),nom:nom.trim(),isGlobal:false,color:PROJ_COLORS[colorIdx%PROJ_COLORS.length]}]);
    setFiches(f=>({...f,[id]:{...EMPTY_FICHE(),statut:"En cours",ville:ville.trim(),nomProgramme:nom.trim()}}));
    const newRows=COUT_ROWS_LABELS.map(label=>({id:uid(),projetId:id,label,statut:"F",hidden:label.toLowerCase()==="marge blue"}));
    setRows(rs=>[...rs,...newRows]);
    setFichePage(id);
  };

  const deleteProgramme=(projId)=>{
    const projRowIds=rows.filter(r=>r.projetId===projId).map(r=>r.id);
    setProjects(ps=>ps.filter(p=>p.id!==projId));
    setFiches(f=>{const nf={...f};delete nf[projId];return nf;});
    setRows(rs=>rs.filter(r=>r.projetId!==projId));
    setValues(vs=>vs.filter(v=>!projRowIds.includes(v.rowId)));
    setDeleteProjId(null);
    if(fichePage===projId) setFichePage(null);
  };

  /* ── Helpers flux prévisionnels ────────────────────────────────────────────
     maxWeek        : renvoie la plus tardive de deux ISO-week strings
     computeLotPrices : calcule prixFinal / foncier / travaux par lot (+ achatFNI
                        et prixTotalHorsMarge au niveau programme)
     buildCgpFluxes  : recalcul complet de la row "Marge CGP" pour un programme.
                        Pour chaque lot : flux individuel daté selon son statut
                        (COMPROMIS+17 / LIA+20 / OPTION+22), montant = retroMontantR
                        ou prixFinal*margeCGPpct% en fallback.
                        Lots LIBRE : CGP résiduel réparti 80/20 aux dates foncier. */

  const maxWeek=(w1,w2)=>(!w1?w2:!w2?w1:w1>=w2?w1:w2);

  const computeLotPrices=(fiche)=>{
    const lots=fiche.lots||[];
    const devisTravaux=toN(fiche.devisTravaux);
    const euribor=toN(fiche.euribor),montantCredit=toN(fiche.montantCredit);
    const creditMensuel=montantCredit*(euribor/100+0.025)/12;
    const creditTotal=creditMensuel*8;
    const moe=devisTravaux*0.08;
    const prixDenormandieCalc=toN(fiche.prixAchat)-toN(fiche.montantCommerceRdc);
    const achatFNI=prixDenormandieCalc*1.025+toN(fiche.dossierBanque)+creditTotal;
    const montantTravaux=moe+toN(fiche.dp)+toN(fiche.pc)+toN(fiche.plaquette)+toN(fiche.deplacements)+toN(fiche.avocat)+toN(fiche.hommeArt)+toN(fiche.geometre)+toN(fiche.diagnostics)+toN(fiche.loyerMeuble)+toN(fiche.doTrc)+toN(fiche.gfa)+devisTravaux;
    const prixTotalHorsMarge=achatFNI+montantTravaux;
    const totalMargePct=(toN(fiche.margeBluePct)+toN(fiche.margeCGPpct))/100;
    const caOperation=totalMargePct<1?prixTotalHorsMarge/(1-totalMargePct):prixTotalHorsMarge;
    const totalSP=lots.reduce((s,l)=>s+toN(l.surface)+Math.min(toN(l.ext)/2,8),0);
    const lotsP1=lots.map(l=>{
      const sp=toN(l.surface)+Math.min(toN(l.ext)/2,8);
      const bc=totalSP>0?caOperation*sp/totalSP:0;
      return bc*(1+toN(l.ponderationPct||"")/100);
    });
    const totPP=lotsP1.reduce((s,v)=>s+v,0);
    return lots.map((l,idx)=>{
      const prixPondere=lotsP1[idx];
      const calc=totPP>0?prixPondere*caOperation/totPP:prixPondere;
      const prixFinal=l.prixFinalO!==""?toN(l.prixFinalO):calc;
      const foncier=prixTotalHorsMarge>0?prixFinal*achatFNI/prixTotalHorsMarge:0;
      const travaux=prixFinal-foncier;
      return {prixFinal,foncier,travaux,achatFNI,prixTotalHorsMarge};
    });
  };

  /* Mapping statut → {searchStatut pour historiqueStatut, offsetWeeks foncier} */
  const STATUT_PIVOT={
    COMPROMIS:{s:'COMPROMIS',o:17},AV10:{s:'COMPROMIS',o:17},AV40:{s:'COMPROMIS',o:17},
    AV70:{s:'COMPROMIS',o:17},AV95:{s:'COMPROMIS',o:17},VENDU:{s:'COMPROMIS',o:17},
    LIVRE:{s:'COMPROMIS',o:17},LIA:{s:'LIA',o:20},OPTION:{s:'OPTION',o:22},
  };

  const buildCgpFluxes=(projId,ficheArg)=>{
    const fiche=ficheArg||fiches[projId]||EMPTY_FICHE();
    const lots=fiche.lots||[];
    const dateAchat=fiche.dateAcquisition;
    if(!dateAchat||lots.length===0) return [];
    const projRows=rows.filter(r=>r.projetId===projId);
    const cgpRow=projRows.find(r=>r.label.toLowerCase().includes("marge cgp"));
    if(!cgpRow) return [];
    const margeCGPpct=toN(fiche.margeCGPpct);
    const lotPrices=computeLotPrices(fiche);
    const nb80=Math.ceil(lots.length*0.8);
    const fluxes=[];
    let libreEarlyCgp=0,libreLateCgp=0;
    lots.forEach((lot,idx)=>{
      const statut=lot.statutCommercial||'LIBRE';
      const pivotInfo=STATUT_PIVOT[statut];
      if(!pivotInfo){
        // Lot LIBRE : accumule pour le forecast 80/20
        const cgpEst=lotPrices[idx].prixFinal*(margeCGPpct/100);
        if(idx<nb80) libreEarlyCgp+=cgpEst; else libreLateCgp+=cgpEst;
        return;
      }
      // Lot avec statut connu : flux individuel daté
      const histEntry=[...(lot.historiqueStatut||[])].reverse().find(h=>h.to===pivotInfo.s);
      const pivotDate=histEntry?.date||dateAchat;
      const cgpAmount=toN(lot.retroMontantR)>0
        ?toN(lot.retroMontantR)
        :lotPrices[idx].prixFinal*(margeCGPpct/100);
      const weekIso=shiftWeek(pivotDate,pivotInfo.o);
      if(weekIso) fluxes.push({rowId:cgpRow.id,weekIso,montant:-Math.abs(cgpAmount)});
    });
    // Lots LIBRE : forecast réparti 80/20 aux mêmes dates que le foncier
    // dateDebutTravaux-4sem et dateDebutTravaux+4sem (cohérent avec generateFlows)
    const dateDebutTravauxCgp=fiche.dateDebutTravaux||shiftWeek(dateAchat,39);
    const w80=shiftWeek(dateDebutTravauxCgp,-4),w20=shiftWeek(dateDebutTravauxCgp,4);
    if(libreEarlyCgp>0&&w80) fluxes.push({rowId:cgpRow.id,weekIso:w80,montant:-Math.abs(libreEarlyCgp)});
    if(libreLateCgp>0&&w20)  fluxes.push({rowId:cgpRow.id,weekIso:w20,montant:-Math.abs(libreLateCgp)});
    // Dédoublonnage : si deux lots sont en COMPROMIS la même semaine, on somme leurs CGP
    // pour éviter les violations PRIMARY KEY (row_id, week_iso) à la sauvegarde.
    const cgpByWeek={};
    fluxes.forEach(f=>{
      if(cgpByWeek[f.weekIso]!==undefined){cgpByWeek[f.weekIso].montant+=f.montant;}
      else{cgpByWeek[f.weekIso]={...f};}
    });
    return Object.values(cgpByWeek);
  };

  const generateFlows=(projId)=>{
    const fiche=fiches[projId]||EMPTY_FICHE();
    if(!fiche.dateAcquisition) return;
    const dateAchat=fiche.dateAcquisition;
    const dateDebutTravaux=fiche.dateDebutTravaux||shiftWeek(dateAchat,39);
    const devisTravaux=toN(fiche.devisTravaux);
    const euribor=toN(fiche.euribor),montantCredit=toN(fiche.montantCredit);
    const creditMensuel=montantCredit*(euribor/100+0.025)/12;
    const creditTotal=creditMensuel*8;
    const moe=devisTravaux*0.08;
    const prixDenormandieCalc=toN(fiche.prixAchat)-toN(fiche.montantCommerceRdc);
    const achatFNI=prixDenormandieCalc*1.025+toN(fiche.dossierBanque)+creditTotal;
    const montantTravaux=moe+toN(fiche.dp)+toN(fiche.pc)+toN(fiche.plaquette)+toN(fiche.deplacements)+toN(fiche.avocat)+toN(fiche.hommeArt)+toN(fiche.geometre)+toN(fiche.diagnostics)+toN(fiche.loyerMeuble)+toN(fiche.doTrc)+toN(fiche.gfa)+devisTravaux;
    const prixTotalHorsMarge=achatFNI+montantTravaux;
    const totalMargePct=(toN(fiche.margeBluePct)+toN(fiche.margeCGPpct))/100;
    const caOperation=totalMargePct<1?prixTotalHorsMarge/(1-totalMargePct):prixTotalHorsMarge;
    const dontMargeCGP=caOperation*(toN(fiche.margeCGPpct)/100);
    const lots=fiche.lots||[];
    const nb80=Math.ceil(lots.length*0.8);
    const totalSP=lots.reduce((s,l)=>s+toN(l.surface)+Math.min(toN(l.ext)/2,8),0);
    const lotsPass1=lots.map(l=>{
      const sp=toN(l.surface)+Math.min(toN(l.ext)/2,8);
      const prixBrutCalc=totalSP>0?caOperation*sp/totalSP:0;
      const ponderPct=toN(l.ponderationPct||"");
      return {prixBrutCalc,prixPondere:prixBrutCalc*(1+ponderPct/100)};
    });
    const totalPrixPondere=lotsPass1.reduce((s,lp)=>s+lp.prixPondere,0);
    const lotsCalcLocal=lots.map((l,idx)=>{
      const {prixBrutCalc,prixPondere}=lotsPass1[idx];
      const prixFinalCalc=totalPrixPondere>0?prixPondere*caOperation/totalPrixPondere:prixBrutCalc;
      const prixFinal=l.prixFinalO!==""?toN(l.prixFinalO):prixFinalCalc;
      const foncier=prixTotalHorsMarge>0?prixFinal*achatFNI/prixTotalHorsMarge:0;
      const travaux=prixFinal-foncier;
      return {prixFinal,foncier,travaux};
    });
    const projRowsBase=rows.filter(r=>r.projetId===projId);
    const existingLabels=new Set(projRowsBase.map(r=>r.label.toLowerCase()));
    const missingRows=COUT_ROWS_LABELS
      .filter(label=>!existingLabels.has(label.toLowerCase()))
      .map(label=>({id:uid(),projetId:projId,label,statut:"F",hidden:label.toLowerCase()==="marge blue"}));
    if(missingRows.length>0) setRows(rs=>[...rs,...missingRows]);
    const projRows=[...projRowsBase,...missingRows];
    const findRow=(...kws)=>projRows.find(r=>kws.some(kw=>r.label.toLowerCase().includes(kw.toLowerCase())));
    const dpRow=findRow("dp"); const pcRow=findRow("pc"); const plaqueRow=findRow("plaquette");
    const geoRow=findRow("géomètre","geometre"); const diagRow=findRow("diagnostic");
    const dossierRow=findRow("dossier banque"); const avocatRow=findRow("avocat");
    const hommeRow=findRow("homme de l'art","homme art");
    const creditRow=findRow("crédit mensuel","credit mensuel");
    const gfaRow=findRow("gfa"); const doTrcRow=findRow("trc");
    const travauxRow=findRow("travaux"); const moeRow=findRow("moe");
    const deplRow=findRow("déplacement");
    const loyerRow=findRow("loyer meublé","loyer meuble");
    const cgpRow=findRow("marge cgp"); const margeBlueRow=findRow("marge blue");
    const lotRows=projRows.filter(r=>/^Lot \d+$/.test(r.label));
    const dontMargeBlueFlow=caOperation*(toN(fiche.margeBluePct)/100);
    const newValues=[];
    const addFlow=(row,weekIso,montant)=>{if(row&&weekIso)newValues.push({rowId:row.id,weekIso,montant:montant||0});};
    const pre3m=shiftWeek(dateAchat,-13);
    if(dpRow)    addFlow(dpRow,    pre3m, -Math.abs(toN(fiche.dp)));
    if(pcRow)    addFlow(pcRow,    pre3m, -Math.abs(toN(fiche.pc)));
    if(plaqueRow)addFlow(plaqueRow,pre3m, -Math.abs(toN(fiche.plaquette)));
    if(geoRow)   addFlow(geoRow,   pre3m, -Math.abs(toN(fiche.geometre)));
    if(diagRow)  addFlow(diagRow,  pre3m, -Math.abs(toN(fiche.diagnostics)));
    if(deplRow)  addFlow(deplRow,  pre3m, -Math.abs(toN(fiche.deplacements)));
    if(dossierRow) addFlow(dossierRow, dateAchat, -Math.abs(toN(fiche.dossierBanque)));
    const post1m=shiftWeek(dateAchat,4);
    if(avocatRow) addFlow(avocatRow, post1m, -Math.abs(toN(fiche.avocat)));
    if(hommeRow)  addFlow(hommeRow,  post1m, -Math.abs(toN(fiche.hommeArt)));
    for(let i=0;i<8;i++){
      if(creditRow) addFlow(creditRow, shiftWeek(dateAchat,i*4), -Math.abs(creditMensuel));
    }
    // CGP : flux par lot (prixFinal×margeCGPpct%), répartis 80/20 aux mêmes dates que le foncier
    // (on paye les CGP quand on vend le foncier) : dateDebutTravaux-4sem et dateDebutTravaux+4sem
    const margeCGPpctG=toN(fiche.margeCGPpct);
    let cgpEarlyG=0,cgpLateG=0;
    lotsCalcLocal.forEach((lc,idx)=>{
      const cgpLot=lc.prixFinal*(margeCGPpctG/100);
      if(idx<nb80) cgpEarlyG+=cgpLot; else cgpLateG+=cgpLot;
    });
    if(cgpRow&&cgpEarlyG>0) addFlow(cgpRow,shiftWeek(dateDebutTravaux,-4),-Math.abs(cgpEarlyG));
    if(cgpRow&&cgpLateG>0)  addFlow(cgpRow,shiftWeek(dateDebutTravaux,4),-Math.abs(cgpLateG));
    // Marge Blue : en totalité au dernier palier des travaux (offset 27)
    if(margeBlueRow) addFlow(margeBlueRow,shiftWeek(dateDebutTravaux,27),-Math.abs(dontMargeBlueFlow));
    const pre1mTrav=shiftWeek(dateDebutTravaux,-4);
    if(gfaRow)   addFlow(gfaRow,   pre1mTrav, -Math.abs(toN(fiche.gfa)));
    if(doTrcRow) addFlow(doTrcRow, pre1mTrav, -Math.abs(toN(fiche.doTrc)));
    TRAVAUX_SCHEDULE.forEach(({offsetWeeks,pct})=>{
      if(travauxRow) addFlow(travauxRow, shiftWeek(dateDebutTravaux,offsetWeeks), -Math.abs(devisTravaux*pct));
      if(moeRow)     addFlow(moeRow,     shiftWeek(dateDebutTravaux,offsetWeeks), -Math.abs(moe*pct));
    });
    const loyerMeubleMontant=toN(fiche.loyerMeuble);
    if(loyerMeubleMontant!==0){
      for(let i=0;i<7;i++){
        if(loyerRow) addFlow(loyerRow, shiftWeek(dateDebutTravaux,i*4), loyerMeubleMontant/7);
      }
    }
    lots.forEach((lot,idx)=>{
      const lc=lotsCalcLocal[idx];
      const lotRow=lotRows.find(r=>r.label===`Lot ${idx+1}`);
      if(!lotRow) return;
      const isFirst80=idx<nb80;
      // Foncier : dateDebutTravaux-4sem pour les 80%, dateDebutTravaux+4sem pour les 20%
      const saleDate=shiftWeek(dateDebutTravaux,isFirst80?-4:4);
      addFlow(lotRow, saleDate, Math.abs(lc.foncier));
      const travOffset=isFirst80?0:13;
      TRAVAUX_SCHEDULE.forEach(({offsetWeeks,pct})=>{
        addFlow(lotRow, shiftWeek(dateDebutTravaux,offsetWeeks+travOffset), Math.abs(lc.travaux*pct));
      });
    });
    const affectedRowIds=new Set([
      dpRow?.id,pcRow?.id,plaqueRow?.id,geoRow?.id,diagRow?.id,
      dossierRow?.id,avocatRow?.id,hommeRow?.id,creditRow?.id,
      gfaRow?.id,doTrcRow?.id,travauxRow?.id,moeRow?.id,
      deplRow?.id,loyerRow?.id,cgpRow?.id,margeBlueRow?.id,
      ...lotRows.map(r=>r.id)
    ].filter(Boolean));
    setValues(vs=>{
      const kept=vs.filter(v=>{
        if(!affectedRowIds.has(v.rowId)) return true;
        const r=rows.find(x=>x.id===v.rowId);
        return r&&r.statut==="R";
      });
      return [...kept,...newValues];
    });
  };

  const handleLotStatutChange=(projId,lotIdx,newStatut,date,clientNom,note,indivisionData)=>{
    setFiches(f=>{
      const fiche=f[projId]||EMPTY_FICHE();
      const lots=[...(fiche.lots||[])];
      const lot={...lots[lotIdx]};
      const oldStatut=lot.statutCommercial||"LIBRE";
      lot.historiqueStatut=[...(lot.historiqueStatut||[]),{date,from:oldStatut,to:newStatut,clientNom,note}];
      lot.statutCommercial=newStatut;
      if(clientNom) lot.clientNom=clientNom;
      // Sprint Indivision (étape 2) : enregistrement type d'acquisition au passage en OPTION.
      // indivisionData est null pour les autres statuts.
      if(newStatut==="OPTION" && indivisionData){
        lot.typeAcquisition       = indivisionData.typeAcquisition || "seul";
        lot.coAcheteurClientId    = indivisionData.coAcheteurClientId || "";
        lot.quotePartPrincipalPct = indivisionData.quotePartPrincipalPct!=null
          ? indivisionData.quotePartPrincipalPct
          : 50;
      }
      lots[lotIdx]=lot;
      return {...f,[projId]:{...fiche,lots}};
    });
    if(newStatut==="COMPROMIS"){
      const fiche=fiches[projId]||EMPTY_FICHE();
      const lots=fiche.lots||[];
      const dateAchat=fiche.dateAcquisition;
      const dateDebutTravaux=fiche.dateDebutTravaux||(dateAchat?shiftWeek(dateAchat,39):null);
      if(!dateAchat) return;
      const lotPrices=computeLotPrices(fiche);
      const lc=lotPrices[lotIdx];
      if(!lc) return;
      const projRows=rows.filter(r=>r.projetId===projId);
      const lotRow=projRows.find(r=>r.label===`Lot ${lotIdx+1}`);
      const cgpRow=projRows.find(r=>r.label.toLowerCase().includes("marge cgp"));
      if(!lotRow) return;
      const lot=lots[lotIdx];
      // Foncier réel : foncierReelO si saisi, sinon calculé depuis prixReel, sinon forecast
      const prixReel=toN(lot.prixReel);
      const foncierReel=lot.foncierReelO!==""&&lot.foncierReelO!=null
        ?toN(lot.foncierReelO)
        :prixReel>0
          ?(lc.prixTotalHorsMarge>0?prixReel*lc.achatFNI/lc.prixTotalHorsMarge:0)
          :lc.foncier;
      const travauxReel=prixReel>0?prixReel-foncierReel:lc.travaux;
      // Dates
      const foncierDate=shiftWeek(date,17);
      // Travaux : démarrent au plus tard entre compromis+20 et dateDebutTravaux+3
      const travStart=maxWeek(shiftWeek(date,20),shiftWeek(dateDebutTravaux,3));
      const travPcts=TRAVAUX_SCHEDULE.map(s=>s.pct); // [0.10,0.30,0.30,0.25,0.05]
      const newVals=[];
      if(foncierDate) newVals.push({rowId:lotRow.id,weekIso:foncierDate,montant:Math.abs(foncierReel)});
      [0,6,12,18,24].forEach((off,i)=>{
        const wk=shiftWeek(travStart,off);
        if(wk) newVals.push({rowId:lotRow.id,weekIso:wk,montant:Math.abs(travauxReel*travPcts[i])});
      });
      // CGP : recalcul complet de la row pour tout le programme.
      // setFiches est async : on construit une fiche à jour avec le lot en COMPROMIS
      // ET son entrée historiqueStatut pour que buildCgpFluxes trouve la bonne date pivot.
      const updatedLots=lots.map((l,i)=>i===lotIdx
        ?{...l,statutCommercial:'COMPROMIS',historiqueStatut:[...(l.historiqueStatut||[]),{date,from:l.statutCommercial||'LIBRE',to:'COMPROMIS'}]}
        :l);
      const cgpFluxes=buildCgpFluxes(projId,{...fiche,lots:updatedLots});
      // Mise à jour : supprime les flux F du lot + tous les flux CGP, puis réinsère
      setValues(vs=>{
        const kept=vs.filter(v=>{
          if(v.rowId===lotRow.id){const r=rows.find(x=>x.id===v.rowId);return r&&r.statut==="R";}
          if(cgpRow&&v.rowId===cgpRow.id) return false;
          return true;
        });
        return [...kept,...newVals,...cgpFluxes];
      });
    }
    if(newStatut==="VENDU"){
      setRows(rs=>rs.map(r=>r.projetId===projId&&r.label===`Lot ${lotIdx+1}`?{...r,statut:"R"}:r));
    }
  };

  const updateLotReelFlows=(projId,lotIdx)=>{
    const fiche=fiches[projId]||EMPTY_FICHE();
    const lots=fiche.lots||[];
    const lot=lots[lotIdx];
    if(!lot) return;
    const prixReel=toN(lot.prixReel);
    if(prixReel<=0) return;
    const dateAchat=fiche.dateAcquisition;
    const dateDebutTravaux=fiche.dateDebutTravaux||(dateAchat?shiftWeek(dateAchat,39):null);
    if(!dateAchat) return;
    const lotPrices=computeLotPrices(fiche);
    const lc=lotPrices[lotIdx];
    if(!lc) return;
    const projRows=rows.filter(r=>r.projetId===projId);
    const lotRow=projRows.find(r=>r.label===`Lot ${lotIdx+1}`);
    const cgpRow=projRows.find(r=>r.label.toLowerCase().includes("marge cgp"));
    if(!lotRow) return;
    // Déterminer la date pivot selon le statut du lot
    // COMPROMIS+ → date du passage en COMPROMIS + 17 sem
    // LIA         → date du passage en LIA + 20 sem
    // OPTION      → date du passage en OPTION + 22 sem
    // Fallback    → aujourd'hui (pivot immédiat)
    const statut=lot.statutCommercial||'LIBRE';
    const pivotInfo=STATUT_PIVOT[statut];
    let foncierDate;
    if(pivotInfo){
      const histEntry=[...(lot.historiqueStatut||[])].reverse().find(h=>h.to===pivotInfo.s);
      const pivotDate=histEntry?.date||todayIso;
      foncierDate=shiftWeek(pivotDate,pivotInfo.o);
    } else {
      // LIBRE ou statut inconnu : foncier daté à aujourd'hui
      foncierDate=todayIso;
    }
    // Foncier réel
    const foncierReel=lot.foncierReelO!==""&&lot.foncierReelO!=null
      ?toN(lot.foncierReelO)
      :lc.prixTotalHorsMarge>0?prixReel*lc.achatFNI/lc.prixTotalHorsMarge:0;
    const travauxReel=prixReel-foncierReel;
    // Travaux : max(foncier+3, dateDebutTravaux+3)
    const travStart=maxWeek(
      shiftWeek(foncierDate,3),
      shiftWeek(dateDebutTravaux||todayIso,3)
    );
    const travPcts=TRAVAUX_SCHEDULE.map(s=>s.pct);
    const newVals=[];
    if(foncierDate) newVals.push({rowId:lotRow.id,weekIso:foncierDate,montant:Math.abs(foncierReel)});
    [0,6,12,18,24].forEach((off,i)=>{
      const wk=shiftWeek(travStart,off);
      if(wk) newVals.push({rowId:lotRow.id,weekIso:wk,montant:Math.abs(travauxReel*travPcts[i])});
    });
    // CGP : recalcul complet de la row pour tout le programme
    const cgpFluxes=buildCgpFluxes(projId,fiche);
    // Mise à jour : supprime TOUS les flux du lot (F et R) + tous les flux CGP, puis réinsère.
    // IMPORTANT : on supprime aussi les R pour éviter les doublons PRIMARY KEY (row_id, week_iso)
    // quand updateLotReelFlows est appelé sur un lot déjà VENDU/LIVRE dont les flux R sont aux
    // mêmes semaines que les nouveaux flux calculés.
    setValues(vs=>{
      const kept=vs.filter(v=>{
        if(v.rowId===lotRow.id) return false; // supprime TOUT pour ce lot (F et R)
        if(cgpRow&&v.rowId===cgpRow.id) return false;
        return true;
      });
      return [...kept,...newVals,...cgpFluxes];
    });
    console.log("[TrésoImmo] Flux mis à jour pour Lot "+(lotIdx+1)+": prixReel="+prixReel+", foncierDate="+foncierDate+", travStart="+travStart);
  };

  /* ── Journal de flux ── */
  const saveFlux=(form)=>{
    const isNew=!journal.find(f=>f.id===form.id);
    if(form.rowId&&form.datePaiement&&form.montant!==""){
      const weekIso=shiftWeek(form.datePaiement,0);
      if(weekIso){
        const montantN=toN(form.montant);
        setValues(vs=>{
          const filtered=vs.filter(v=>!(v.rowId===form.rowId&&v.weekIso===weekIso&&rows.find(r=>r.id===v.rowId&&r.statut==="F")));
          const idx=filtered.findIndex(v=>v.rowId===form.rowId&&v.weekIso===weekIso);
          if(idx>=0) return filtered.map((v,i)=>i===idx?{...v,montant:montantN}:v);
          return [...filtered,{rowId:form.rowId,weekIso,montant:montantN}];
        });
        setRows(rs=>rs.map(r=>r.id===form.rowId?{...r,statut:"R"}:r));
      }
    }
    setJournal(j=>isNew?[...j,form]:j.map(f=>f.id===form.id?form:f));
  };
  const deleteFlux=(id)=>{setJournal(j=>j.filter(f=>f.id!==id));};

  /* ── Helpers de style (header) ── */
  const btnS=(bg,sm)=>({background:bg||"#2563eb",color:"#fff",border:"none",borderRadius:7,padding:sm?"4px 10px":"8px 16px",cursor:"pointer",fontSize:sm?11:13,fontWeight:600});

  const navItems=[
    {id:"plan",label:"📋 Plan hebdo"},
    {id:"programmes",label:"🏘 Programmes"},
    {id:"crm",label:"👥 CRM"},
    {id:"journal",label:"📒 Journal",hidden:true},
    {id:"reconciliation",label:"⚖️ Réconciliation"},
    {id:"reception",label:"🏠 Réception"},
    {id:"notifications",label:"🔔 Notifications",badge:newNotifCount||null},
    {id:"historique",label:"📜 Historique"},
  ];

  return (
    <div style={{fontFamily:"system-ui,sans-serif",background:C.bg,color:C.text,minHeight:"100vh"}}>
      {/* Header */}
      <div style={{background:C.card,borderBottom:"1px solid "+C.border,padding:"10px 16px",display:"flex",alignItems:"center",justifyContent:"space-between",position:"sticky",top:0,zIndex:30}}>
        <div style={{display:"flex",alignItems:"center",gap:20}}>
          <span style={{fontWeight:800,fontSize:16}}>🏗 TrésoImmo</span>
          {navItems.filter(i=>!i.hidden).map(item=>(
            <button key={item.id} onClick={()=>{setPage(item.id);setFichePage(null);}}
              style={{background:"none",border:"none",color:page===item.id?C.accent:C.muted,fontSize:13,fontWeight:page===item.id?700:400,cursor:"pointer",borderBottom:page===item.id?"2px solid "+C.accent:"2px solid transparent",padding:"4px 0",display:"flex",alignItems:"center",gap:4}}>
              {item.label}
              {item.badge&&<span style={{background:"#dc2626",color:"#fff",borderRadius:10,padding:"0 6px",fontSize:10,fontWeight:700,lineHeight:"16px",minWidth:16,textAlign:"center"}}>{item.badge}</span>}
            </button>
          ))}
        </div>
        <div style={{display:"flex",gap:8,alignItems:"center"}}>
          {pendingAttestations.length>0&&(
            <button onClick={()=>setShowAttestationList(true)}
              style={{background:"#f59e0b",color:"#fff",border:"none",borderRadius:8,padding:"4px 12px",fontSize:12,fontWeight:700,cursor:"pointer",animation:"pulse 2s infinite"}}
              title="Des attestations d'avancement ont été reçues — cliquer pour envoyer l'appel de fonds">
              🔔 {pendingAttestations.length} attestation{pendingAttestations.length>1?"s":""}
            </button>
          )}
          <span style={{fontSize:12,color:C.muted,background:"#f1f5f9",borderRadius:6,padding:"4px 10px",fontWeight:600}}>👤 {loggedUser||user}</span>
          <button onClick={onReset} style={btnS("#334155",true)}>↩ Import</button>
          {onLogout&&<button onClick={onLogout} style={{...btnS("#64748b",true),fontSize:11}}>⎋ Déco</button>}
        </div>
      </div>

      {/* ═══ PLAN HEBDO ═══ */}
      {page==="plan"&&(
        <PlanTresorerieView
          projects={projects}
          rows={rows}
          values={values}
          fiches={fiches}
          soldeInitial={soldeInitial}
          setValues={setValues}
          setRows={setRows}
          setProjects={setProjects}
          handleCellSave={handleCellSave}
          onNavigateToFiche={id=>{setFichePage(id);setPage("programmes");}}
        />
      )}

      {/* ═══ RÉCONCILIATION ═══ */}
      {page==="reconciliation"&&(
        <ReconciliationPage
          projects={projects}
          rows={rows}
          values={values}
          reconciliations={reconciliations}
          setReconciliations={setReconciliations}
          loggedUser={loggedUser}
        />
      )}

      {/* ═══ NOTIFICATIONS ═══ */}
      {page==="notifications"&&(
        <NotificationsView
          notifications={notifications}
          setNotifications={setNotifications}
          notifLoading={notifLoading}
          setNotifLoading={setNotifLoading}
          notifError={notifError}
          setNotifError={setNotifError}
          notifFilterBal={notifFilterBal}
          setNotifFilterBal={setNotifFilterBal}
          notifFilterCat={notifFilterCat}
          setNotifFilterCat={setNotifFilterCat}
          notifShowAll={notifShowAll}
          setNotifShowAll={setNotifShowAll}
          notifPdfModal={notifPdfModal}
          setNotifPdfModal={setNotifPdfModal}
          analyzingIds={analyzingIds}
          setAnalyzingIds={setAnalyzingIds}
          editingFacture={editingFacture}
          setEditingFacture={setEditingFacture}
          reglerModal={reglerModal}
          setReglerModal={setReglerModal}
          agentRunning={agentRunning}
          setAgentRunning={setAgentRunning}
          loadNotifs={loadNotifs}
          fiches={fiches}
          projects={projects}
          rows={rows}
          values={values}
          handleCellSave={handleCellSave}
          crm={crm}
        />
      )}

      {/* ═══ PROGRAMMES ═══ */}
      {page==="programmes"&&(
        fichePage?(
          <FicheDetail
            proj={projects.find(p=>p.id===fichePage)}
            fiche={fiches[fichePage]||EMPTY_FICHE()}
            onUpdate={(field,val)=>updateFiche(fichePage,field,val)}
            onBack={()=>setFichePage(null)}
            soldeFinOp={values.filter(v=>{const r=rows.find(r=>r.id===v.rowId);return r&&r.projetId===fichePage&&!r.hidden;}).reduce((s,v)=>s+v.montant,0)}
            rows={rows}
            values={values}
            crm={crm}
            onGenerateFlows={generateFlows}
            onLotStatutChange={(lotIdx,newStatut,date,clientNom,note,indivisionData)=>handleLotStatutChange(fichePage,lotIdx,newStatut,date,clientNom,note,indivisionData)}
            onArchitecteEmailSent={handleArchitecteEmailSent}
            onUpdateLotReelFlows={(lotIdx)=>updateLotReelFlows(fichePage,lotIdx)}
          />
        ):(
          <ProgrammesView projects={projects} fiches={fiches} values={values} rows={rows} onSelect={id=>setFichePage(id)} onCreate={createProgramme} onDelete={id=>setDeleteProjId(id)}/>
        )
      )}

      {/* ═══ CRM ═══ */}
      {page==="crm"&&<CRMView crm={crm} setCrm={setCrm}/>}

      {/* ═══ JOURNAL DE FLUX ═══ */}
      {page==="journal"&&<JournalFluxView journal={journal} onSave={saveFlux} onDelete={deleteFlux} projects={projects} rows={rows} crm={crm} fiches={fiches}/>}

      {/* ═══ RÉCEPTION ═══ */}
      {page==="reception"&&<ReceptionPage
        projects={projects} fiches={fiches} crm={crm} receptions={receptions} setReceptions={setReceptions}
        onLivraison={(programmeId, lotIdx)=>{
          handleLotStatutChange(programmeId, lotIdx, "LIVRE", new Date().toISOString().slice(0,10), null, "Passage en LIVRE — réception validée");
        }}
      />}

      {/* ═══ HISTORIQUE MODIFICATIONS PLAN ═══ */}
      {page==="historique"&&<PlanChangesView C={C}/>}

      {/* Modale suppression programme */}
      {deleteProjId&&(
        <DeleteProgrammeModal
          proj={projects.find(p=>p.id===deleteProjId)||{nom:"ce programme"}}
          onConfirm={()=>deleteProgramme(deleteProjId)}
          onClose={()=>setDeleteProjId(null)}
        />
      )}

      {/* ── Modale liste des attestations reçues de l'architecte ── */}
      {showAttestationList&&pendingAttestations.length>0&&(
        <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.5)",zIndex:3000,display:"flex",alignItems:"center",justifyContent:"center",padding:16}} onClick={()=>setShowAttestationList(false)}>
          <div style={{background:"#fff",borderRadius:16,padding:28,width:600,maxWidth:"96vw",maxHeight:"85vh",overflowY:"auto",boxShadow:"0 8px 40px #0003",border:"1px solid #e2e8f0"}} onClick={e=>e.stopPropagation()}>
            <div style={{fontWeight:800,fontSize:17,marginBottom:4}}>🔔 Attestations d'avancement reçues</div>
            <div style={{fontSize:12,color:"#64748b",marginBottom:16}}>L'architecte a répondu. Vous pouvez maintenant envoyer l'appel de fonds au client.</div>
            {pendingAttestations.map((att,i)=>{
              const req=archRequests.find(r=>r.ref===att.ref)||att;
              const proj=req.projId?projects.find(p=>p.id===req.projId):null;
              const projNom=proj?(proj.ville||proj.nom):req.programmeNom||att.ref||'Programme inconnu';
              return (
                <div key={i} style={{background:"#f8fafc",borderRadius:10,padding:14,marginBottom:10,border:"1px solid #e2e8f0"}}>
                  <div style={{fontWeight:700,fontSize:13,marginBottom:4}}>
                    <span style={{background:"#fef9c3",color:"#713f12",borderRadius:4,padding:"1px 6px",fontSize:11,fontWeight:700,marginRight:8}}>{req.pct||'?'}%</span>
                    {projNom} — Lot {(req.lotIdx!==undefined?req.lotIdx+1:att.ref)||'?'}
                  </div>
                  <div style={{fontSize:11,color:"#64748b",marginBottom:10}}>
                    {att.receivedAt&&<span>Reçue le {new Date(att.receivedAt).toLocaleString('fr-FR')}</span>}
                    {att.emailFrom&&<span style={{marginLeft:8}}>— de {att.emailFrom}</span>}
                    {att.ref&&<span style={{marginLeft:8,fontFamily:"monospace",fontSize:10,color:"#94a3b8"}}>[{att.ref}]</span>}
                  </div>
                  <div style={{display:"flex",gap:8}}>
                    {req.projId!==undefined&&req.lotIdx!==undefined&&req.callIdx!==undefined?(
                      <button onClick={()=>{setAttestationAppel(req);setShowAttestationList(false);}}
                        style={{background:"#2563eb",color:"#fff",border:"none",borderRadius:8,padding:"7px 16px",fontSize:13,fontWeight:700,cursor:"pointer"}}>
                        📧 Envoyer l'appel de fonds au client →
                      </button>
                    ):(
                      <span style={{fontSize:12,color:"#94a3b8",fontStyle:"italic"}}>
                        Programme non identifié automatiquement — naviguez vers le lot concerné et utilisez le bouton 📧 Appel de fonds.
                      </span>
                    )}
                    <button onClick={async()=>{
                      const remaining=pendingAttestations.filter((_,j)=>j!==i);
                      setPendingAttestations(remaining);
                      if(remaining.length===0) await fetch('/api/agent/attestations',{method:'DELETE'}).catch(()=>{});
                    }} style={{background:"#f1f5f9",border:"1px solid #e2e8f0",borderRadius:8,padding:"7px 12px",fontSize:12,cursor:"pointer",color:"#64748b"}}>
                      ✓ Ignorer
                    </button>
                  </div>
                </div>
              );
            })}
            <div style={{display:"flex",justifyContent:"flex-end",marginTop:8}}>
              <button onClick={()=>setShowAttestationList(false)} style={{background:"#f1f5f9",border:"1px solid #e2e8f0",borderRadius:8,padding:"8px 18px",fontSize:13,cursor:"pointer"}}>Fermer</button>
            </div>
          </div>
        </div>
      )}

      {/* ── AppelFondsModal déclenché depuis une attestation ── */}
      {attestationAppel&&(()=>{
        const aproj=projects.find(p=>p.id===attestationAppel.projId);
        const afiche=fiches[attestationAppel.projId]||EMPTY_FICHE();
        const alot=(afiche.lots||[])[attestationAppel.lotIdx];
        if(!aproj||!alot) return null;
        return (
          <AppelFondsModal
            lot={alot}
            lotIdx={attestationAppel.lotIdx}
            proj={aproj}
            fiche={afiche}
            lc={{}}
            rows={rows}
            values={values}
            crm={crm}
            callIdxOverride={attestationAppel.callIdx}
            onClose={async()=>{
              const remaining=pendingAttestations.filter(a=>a.ref!==attestationAppel.ref);
              setPendingAttestations(remaining);
              if(remaining.length===0) await fetch('/api/agent/attestations',{method:'DELETE'}).catch(()=>{});
              setArchRequests(reqs=>reqs.filter(r=>!(r.projId===attestationAppel.projId&&r.lotIdx===attestationAppel.lotIdx)));
              setAttestationAppel(null);
            }}
          />
        );
      })()}
    </div>
  );
}

function App() {
  const [loggedUser,setLoggedUser]=useState(null);
  const [displayName,setDisplayName]=useState('');
  const [appState,setAppState]=useState(null);
  const [stateVersion,setStateVersion]=useState(null);
  // Sprint A4 (étape 4) : versions par domaine remontées par GET /api/state
  // enrichi. Propagées à MainApp via initialDomainVersions pour initialiser
  // les 4 versionRefs des hooks useDomainSave.
  const [domainVersions,setDomainVersions]=useState(null);
  const [remountKey,setRemountKey]=useState(0);
  const dirtyRef=useRef(false);
  const stateVersionRef=useRef(null);
  // Sprint A4 (étape 5) : version-ref miroir de domainVersions, mutée à la fois
  // par le bootstrap (useEffect ci-dessous) et par les saves locaux (callback
  // handleDomainVersionChange câblé dans MainApp). Source de vérité pour
  // l'anti-ping-pong du listener SSE 'domain-updated'.
  const domainVersionsRef=useRef(null);
  useEffect(()=>{ stateVersionRef.current = stateVersion; },[stateVersion]);
  useEffect(()=>{ domainVersionsRef.current = domainVersions ? {...domainVersions} : {}; },[domainVersions]);

  // Sprint A4 (étape 5) : callback appelé par chaque useDomainSave après un
  // save 200 OK. Mute domainVersionsRef SANS re-render (évite la boucle
  // setState→re-render→re-save). C'est cette mutation qui permet au listener
  // SSE de skip notre propre echo (version reçue === version ref locale).
  const handleDomainVersionChange = useCallback((domain, newVersion) => {
    if (!domainVersionsRef.current) domainVersionsRef.current = {};
    domainVersionsRef.current[domain] = newVersion;
  }, []);

  useEffect(()=>{
    fetch('/api/me')
      .then(r=>r.json())
      .then(d=>{
        setLoggedUser(d.user||false);
        if(d.displayName) setDisplayName(d.displayName);
      })
      .catch(()=>setLoggedUser(false));
  },[]);

  useEffect(()=>{
    if(!loggedUser){setAppState(null);setStateVersion(null);setDomainVersions(null);return;}
    fetch('/api/state')
      .then(r=>{
        if(r.status===401){setLoggedUser(false);return null;}
        return r.json();
      })
      .then(resp=>{
        if(resp===null) return;
        const s  = (resp && typeof resp==='object' && 'data' in resp) ? resp.data : resp;
        const v  = (resp && typeof resp==='object' && 'version' in resp) ? resp.version : null;
        const dv = (resp && typeof resp==='object' && 'domainVersions' in resp) ? resp.domainVersions : null;
        setStateVersion(v);
        setDomainVersions(dv);
        if(s&&s.projects) setAppState(s);
        else setAppState(false);
      })
      .catch(()=>setAppState(false));
  },[loggedUser]);

  useEffect(()=>{
    if(!loggedUser) return;
    let es;
    try { es = new EventSource('/api/events'); }
    catch(e){ console.warn('SSE indisponible:', e); return; }
    const onUpdate = (ev)=>{
      try{
        const payload = JSON.parse(ev.data||'{}')
        const v = payload && payload.version;
        if(!v) return;
        if(v === stateVersionRef.current) return;
        if(dirtyRef.current) return;
        fetch('/api/state')
          .then(r=>r.ok ? r.json() : null)
          .then(resp=>{
            if(!resp || !resp.data || !resp.data.projects) return;
            setAppState(resp.data);
            setStateVersion(resp.version);
            // Sprint A4 (étape 4) : refresh aussi les versions par domaine
            // pour ré-initialiser les versionRefs de MainApp (via remount).
            if ('domainVersions' in resp) setDomainVersions(resp.domainVersions);
            setRemountKey(k=>k+1);
          })
          .catch(()=>{});
      }catch{}
    };
    es.addEventListener('state-updated', onUpdate);

    // Sprint A4 (étape 5) : SSE granulaire par domaine.
    // Émis par chaque PUT /api/state/:domain (cf. routes/state.js
    // handleDomainPut). Permet aux autres onglets de refresh leur vue sans
    // attendre un PUT global (qui n'arrive plus que pour receptions/import
    // depuis A4-4).
    //
    // Anti-ping-pong : on skip si la version reçue ≤ celle stockée localement
    // dans domainVersionsRef.current[domain]. Cette ref est mise à jour :
    //   - au bootstrap (useEffect sur domainVersions)
    //   - après chaque save local 200 OK (callback handleDomainVersionChange
    //     passé en prop à MainApp puis aux 4 useDomainSave)
    // Donc notre propre echo SSE arrive avec une version déjà connue → skip.
    //
    // Garde-fou dirtyRef (global, option A) : on skip aussi si un save local
    // est en flight, pour ne pas écraser des modifs utilisateur non encore
    // sauvegardées.
    const onDomainUpdate = (ev)=>{
      try{
        const payload = JSON.parse(ev.data||'{}');
        const domain  = payload && payload.domain;
        const version = payload && payload.version;
        if(!domain || !version) return;
        const localVer = domainVersionsRef.current && domainVersionsRef.current[domain];
        if(localVer && version <= localVer) return;  // notre propre echo ou plus vieux
        if(dirtyRef.current) return;                 // modif locale en attente → skip

        // Refetch global : le GET /api/state ramène data + version + les 4
        // domainVersions en une requête. Plus simple qu'un refetch ciblé
        // /api/state/:domain car les states (crm, fiches, …) vivent dans
        // MainApp et un update surgical demanderait de lifter tout le state.
        // Le remount via setRemountKey ré-initialise les versionRefs depuis
        // les nouvelles initialDomainVersions.
        fetch('/api/state')
          .then(r=>r.ok ? r.json() : null)
          .then(resp=>{
            if(!resp || !resp.data || !resp.data.projects) return;
            setAppState(resp.data);
            setStateVersion(resp.version);
            if ('domainVersions' in resp) setDomainVersions(resp.domainVersions);
            setRemountKey(k=>k+1);
          })
          .catch(()=>{});
      }catch{}
    };
    es.addEventListener('domain-updated', onDomainUpdate);

    es.onerror = ()=>{ /* no-op */ };
    return ()=>{ try{ es.close(); }catch{} };
  },[loggedUser]);

  const handleImport=async(data,solde)=>{
    await fetch('/api/state',{method:'DELETE'});
    const state={...data,soldeInitial:solde,fiches:{},crm:{entreprises:[],clients:[],cgps:[],banques:[],comptes:[]},journal:[]};
    const putResp = await fetch('/api/state',{
      method:'PUT',
      headers:{'Content-Type':'application/json'},
      body:JSON.stringify({data:state, version:null})
    });
    try {
      const b = await putResp.json();
      if (b && b.version) setStateVersion(b.version);
    } catch {}
    setAppState(state);
    setShowImportOverlay(false);
  };

  const [showImportOverlay,setShowImportOverlay]=React.useState(false);
  const handleReset=()=>setShowImportOverlay(true);

  const handleLogout=async()=>{
    await fetch('/api/logout',{method:'POST'});
    setLoggedUser(false);
    setAppState(null);
  };

  if(loggedUser===null) return (
    <div style={{display:'flex',alignItems:'center',justifyContent:'center',height:'100vh',flexDirection:'column',gap:16,color:'#64748b'}}>
      <div style={{width:40,height:40,border:'4px solid #e2e8f0',borderTopColor:'#2563eb',borderRadius:'50%',animation:'spin 0.8s linear infinite'}}/>
      <style>{`@keyframes spin{to{transform:rotate(360deg)}}`}</style>
      <div>Chargement de TrésoImmo…</div>
    </div>
  );

  if(!loggedUser) return <LoginScreen/>;

  if(appState===null) return (
    <div style={{display:'flex',alignItems:'center',justifyContent:'center',height:'100vh',flexDirection:'column',gap:16,color:'#64748b'}}>
      <div style={{width:40,height:40,border:'4px solid #e2e8f0',borderTopColor:'#2563eb',borderRadius:'50%',animation:'spin 0.8s linear infinite'}}/>
      <style>{`@keyframes spin{to{transform:rotate(360deg)}}`}</style>
      <div>Chargement de TrésoImmo…</div>
    </div>
  );

  if(!appState||showImportOverlay) return <ImportScreen onImport={handleImport} onCancel={appState&&showImportOverlay?()=>setShowImportOverlay(false):null}/>;
  return <MainApp
    key={remountKey}
    data={{projects:appState.projects,rows:appState.rows,values:appState.values}}
    soldeInitial={appState.soldeInitial||0}
    savedFiches={appState.fiches||null}
    savedCrm={appState.crm||null}
    savedJournal={appState.journal||null}
    savedArchRequests={appState.archRequests||[]}
    savedReconciliations={appState.reconciliations||{}}
    savedReceptions={appState.receptions||[]}
    onReset={handleReset}
    loggedUser={displayName||loggedUser}
    onLogout={handleLogout}
    initialVersion={stateVersion}
    initialDomainVersions={domainVersions}
    onVersionChange={setStateVersion}
    onDomainVersionChange={handleDomainVersionChange}
    dirtyRef={dirtyRef}
  />;
}

ReactDOM.render(<App/>, document.getElementById("root"));
