/* =========================================================
   ANTARES — Consuntivi
   Il consuntivo è il conto finale del lavoro fatto, per il cliente (db/24).
   Nasce copiando le voci da uno o più preventivi; da lì le voci si tolgono,
   si modificano e se ne aggiungono. Ogni voce resta legata al suo progetto.
   Fatture e collaboratori NON sono voci: si vedono nel riquadro
   "Costi e incassi reali", che serve a capire il margine e non va negli export.
   Richiede la migrazione db/24_consuntivi.sql
   ========================================================= */

const { useState: useS5 } = React;
const { fmt: fmt5, fmtPct: fmtPct5 } = window.AntaresData;
const Icon5 = window.AntaresScreens1.Icon;
const { Modal: Modal5, EmptyState: Empty5 } = window.AntaresUI;
// stessi calcoli del preventivo (screens-3.jsx): oneri, totali di riga
const {
  totRiga: totRiga5, riepilogoPersonale: riepPers5,
  oneriParams: oneriParams5, isDipendente: isDip5,
} = window.AntaresScreens3.helpers;
const CX5 = window.AntaresConsuntivo; // js/consuntivoExport.js

/* ---- stati: il consuntivo parla di lavoro già fatto ---- */
const STATI_CONSUNTIVO = [
  { id: "in_lavorazione", label: "In lavorazione",     colore: "blue" },
  { id: "inviato",        label: "Inviato al cliente", colore: "yellow" },
  { id: "saldato",        label: "Saldato",            colore: "teal" },
];
const statoInfo5 = (id) => STATI_CONSUNTIVO.find(s => s.id === id) || STATI_CONSUNTIVO[0];

const numIt5 = (v) => parseFloat(String(v ?? "").replace(",", ".")) || 0;
const consuntiviAll = () => window.AntaresData.CONSUNTIVI || [];
const consuntivoPronto = () => !!(window.AntaresData.CONSUNTIVI_STATO || {}).pronto;
const consuntivoDelProgetto = (progettoId) =>
  consuntiviAll().find(c => c.progetti.some(p => p.progettoId === progettoId)) || null;
const refreshCons5 = () => window.AntaresStore.refresh("consuntivi");

function leggiOpz5(chiave, base) {
  try { return { ...base, ...JSON.parse(localStorage.getItem(chiave) || "{}") }; }
  catch { return { ...base }; }
}
function salvaOpz5(chiave, opz) {
  try { localStorage.setItem(chiave, JSON.stringify(opz)); } catch {}
}

/* ---------- trascinamento (drag & drop) delle righe ----------
   Un solo stato per tabella: cosa si sta trascinando e dove cadrebbe.
   Si trascina dalla maniglia ⋮⋮ (maniglia(tipo, id)), così i campi e i
   pulsanti della riga restano normali; si lascia cadere su righe o testate
   (bersaglio(tipo, id, accetta)). classe(id) disegna la linea di inserimento.
   onSposta(da, a): da = { tipo, id }, a = { tipo, id, dopo }. */
function useTrascina5(onSposta) {
  const [drag, setDrag] = useS5(null);   // { tipo, id }
  const [sopra, setSopra] = useS5(null); // { id, dopo }
  const dopoMeta = (e) => {
    const r = e.currentTarget.getBoundingClientRect();
    return e.clientY > r.top + r.height / 2;
  };
  const maniglia = (tipo, id) => ({
    draggable: true,
    className: "dnd-maniglia",
    title: "Trascina per spostare",
    "aria-hidden": true,
    onDragStart: (e) => {
      e.stopPropagation();
      e.dataTransfer.effectAllowed = "move";
      try { e.dataTransfer.setData("text/plain", String(id)); } catch (_) {}
      // l'immagine trascinata è la riga intera, non solo la maniglia
      const riga = e.currentTarget.closest("[data-dnd]");
      if (riga && e.dataTransfer.setDragImage) e.dataTransfer.setDragImage(riga, 16, 16);
      setDrag({ tipo, id });
    },
    onDragEnd: () => { setDrag(null); setSopra(null); },
  });
  const bersaglio = (tipo, id, accetta = [tipo]) => ({
    "data-dnd": tipo,
    onDragOver: (e) => {
      if (!drag || !accetta.includes(drag.tipo)) return;
      e.preventDefault();
      e.stopPropagation();
      if (drag.id === id) { if (sopra) setSopra(null); return; }
      e.dataTransfer.dropEffect = "move";
      const dopo = dopoMeta(e);
      if (!sopra || sopra.id !== id || sopra.dopo !== dopo) setSopra({ id, dopo });
    },
    onDrop: (e) => {
      if (!drag || !accetta.includes(drag.tipo)) return;
      e.preventDefault();
      e.stopPropagation();
      const da = drag, dopo = dopoMeta(e);
      setDrag(null); setSopra(null);
      if (da.id !== id) onSposta(da, { tipo, id, dopo });
    },
  });
  const classe = (id) => [
    drag && drag.id === id ? "dnd-trascinata" : "",
    sopra && sopra.id === id ? (sopra.dopo ? "dnd-dopo" : "dnd-prima") : "",
  ].filter(Boolean).join(" ");
  return { maniglia, bersaglio, classe, attivo: !!drag };
}

// Sposta un elemento in una lista di gruppi [{ id, voci: [id...] }].
// a: { tipo: "voce", id, dopo } = accanto a un'altra voce;
//    { tipo: "gruppo", id } = in fondo a quel gruppo.
function spostaNeiGruppi5(gruppi, voceId, a) {
  const out = gruppi.map(g => ({ ...g, voci: g.voci.filter(x => x !== voceId) }));
  if (a.tipo === "gruppo") {
    const g = out.find(x => x.id === a.id);
    if (g) g.voci.push(voceId);
    return out;
  }
  for (const g of out) {
    const i = g.voci.indexOf(a.id);
    if (i >= 0) { g.voci.splice(a.dopo ? i + 1 : i, 0, voceId); return out; }
  }
  return gruppi;
}
// riordina una lista semplice di id
function spostaInLista5(lista, id, a) {
  const out = lista.filter(x => x !== id);
  const i = out.indexOf(a.id);
  if (i < 0) return lista;
  out.splice(a.dopo ? i + 1 : i, 0, id);
  return out;
}

const dataIt5 = (iso) => {
  if (!iso) return "";
  const [y, m, d] = String(iso).slice(0, 10).split("-");
  return `${d}/${m}/${y}`;
};
const righeTesto5 = (t) => String(t || "").split("\n").map(x => x.trim()).filter(Boolean);

// Disegna le pagine di un PDF (byte) con pdf.js, una sotto l'altra.
// Senza pdf.js ripiega sul visore del browser in un iframe.
function PaginePDF5({ dati }) {
  const box = React.useRef(null);
  const [err, setErr] = useS5("");
  const [url, setUrl] = useS5(null);
  React.useEffect(() => {
    if (!dati) return;
    let annullato = false;
    if (!window.pdfjsLib) {
      const u = URL.createObjectURL(new Blob([dati], { type: "application/pdf" }));
      setUrl(u);
      return () => { annullato = true; URL.revokeObjectURL(u); };
    }
    (async () => {
      try {
        const el = box.current;
        if (!el) return;
        // pdf.js si prende il buffer: gli passo una copia
        const pdf = await window.pdfjsLib.getDocument({ data: new Uint8Array(dati.slice(0)) }).promise;
        const larghezza = Math.max(280, el.clientWidth - 32);
        const dpr = window.devicePixelRatio || 1;
        const pagine = [];
        for (let i = 1; i <= pdf.numPages; i++) {
          const pagina = await pdf.getPage(i);
          const base = pagina.getViewport({ scale: 1 });
          const vista = pagina.getViewport({ scale: (larghezza / base.width) * dpr });
          const c = document.createElement("canvas");
          c.width = vista.width; c.height = vista.height;
          c.style.width = (vista.width / dpr) + "px";
          c.style.height = (vista.height / dpr) + "px";
          c.setAttribute("aria-label", "Pagina " + i + " di " + pdf.numPages);
          await pagina.render({ canvasContext: c.getContext("2d"), viewport: vista }).promise;
          if (annullato) return;
          pagine.push(c);
        }
        // tutte insieme: niente sfarfallio mentre si scrive
        if (!annullato) { el.replaceChildren(...pagine); setErr(""); }
      } catch (ex) {
        if (!annullato) setErr(ex.message || String(ex));
      }
    })();
    return () => { annullato = true; };
  }, [dati]);
  if (url) return <iframe title="Anteprima del documento" src={url + "#toolbar=0&navpanes=0&view=FitH"} />;
  return (
    <>
      {err && <div className="login-error" style={{ margin: 12 }}>Anteprima non disponibile: {err}</div>}
      <div className="doc-export-pagine" ref={box} role="img" aria-label="Anteprima del documento" />
    </>
  );
}

/* ============================================================
   ESPORTAZIONE CON ANTEPRIMA — preventivo e consuntivo
   Richiesta di Diletta (2/10): cambiare titolo, data e scritte prima di
   esportare, scegliere cosa si vede (colonne, voci, prezzi singoli o solo
   i totali per macro area) guardando il documento prima di scaricarlo.
   sorgente: voci, progetti, macroAree, pagamenti, esclusioni… (js/consuntivoExport.js)
   testiIniziali: { etichetta, titolo, cliente, data (iso), nota, sottotitolo, condizioni }
   onSalvaTesti(testi, aggiornaDati): chi apre la finestra decide dove ricordarli
   ============================================================ */
function AnteprimaDocumentoModal({ sorgente, chiaveOpz, testiIniziali, onSalvaTesti, etichettaAggiornaDati,
  azioniMacroAree, onClose }) {
  const [testi, setTesti] = useS5(testiIniziali);
  const [opz, setOpz] = useS5(() => ({ ...leggiOpz5(chiaveOpz, CX5.OPZ_DEFAULT), escludi: [] }));
  const [aggiornaDati, setAggiornaDati] = useS5(true);
  const [dati, setDati] = useS5(null);
  const [errAnt, setErrAnt] = useS5("");
  const [busy, setBusy] = useS5(false);
  const [elencoVoci, setElencoVoci] = useS5(false);
  const setO = (k) => (v) => setOpz(o => ({ ...o, [k]: v }));
  const setT = (k) => (e) => setTesti(t => ({ ...t, [k]: e.target.value }));

  const tipo = sorgente.tipoDoc || "Consuntivo";
  const multi = (sorgente.progetti || []).length > 1;
  const macro = opz.modo === "macroaree";
  const src = {
    ...sorgente,
    etichettaTitolo: (testi.etichetta || "").trim() || tipo,
    titolo: testi.titolo, cliente: testi.cliente, nota: testi.nota,
    dataIt: dataIt5(testi.data), sottotitolo: testi.sottotitolo || "",
    condizioni: righeTesto5(testi.condizioni),
  };
  const doc = CX5.costruisciDocumento(src, opz);
  const nVoci = doc.sezioni.reduce((n, x) => n + x.righe.length, 0);

  // l'anteprima si rigenera poco dopo l'ultima modifica
  const firma = JSON.stringify([testi, opz, sorgente.voci, sorgente.macroAree]);
  React.useEffect(() => {
    let annullato = false;
    const t = setTimeout(async () => {
      try {
        const nuovi = await CX5.anteprimaPDF(src, opz);
        if (annullato) return;
        setDati(nuovi); setErrAnt("");
      } catch (ex) {
        if (!annullato) setErrAnt(ex.message || String(ex));
      }
    }, 350);
    return () => { annullato = true; clearTimeout(t); };
  }, [firma]);

  const scarica = async (formato) => {
    setBusy(true);
    try {
      // le scelte di impostazione restano per la prossima volta; le voci tolte no
      salvaOpz5(chiaveOpz, { ...opz, escludi: [] });
      if (onSalvaTesti) await onSalvaTesti(testi, aggiornaDati);
      if (formato === "pdf") await CX5.exportPDF(src, opz); else CX5.exportXLSX(src, opz);
    } finally { setBusy(false); }
  };
  const ripristina = () => setOpz({ ...CX5.OPZ_DEFAULT, escludi: [] });

  const escluse = new Set(opz.escludi || []);
  const toggleVoce = (id, dentro) => setOpz(o => {
    const set = new Set(o.escludi || []);
    if (dentro) set.delete(id); else set.add(id);
    return { ...o, escludi: [...set] };
  });
  const nomeProgetto = (pid) => (sorgente.progetti || []).find(p => p.progettoId === pid)?.nome || "";
  const ciSonoPagamenti = (sorgente.pagamenti || []).length > 0;
  const ciSonoEsclusioni = (sorgente.esclusioni || []).length > 0;
  const ciSonoNote = (sorgente.voci || []).some(v => v.nota);
  const senzaColonne = macro && !opz.dettaglioVoci;

  return (
    <Modal5
      title={"Esporta " + tipo.toLowerCase()}
      subtitle="Cambia testi e contenuto: l'anteprima si aggiorna da sola"
      onClose={onClose} width={1240}
      footer={<>
        <span className="help modal-foot-left">
          {nVoci} {nVoci === 1 ? "voce" : "voci"} · imponibile <strong className="mono">{fmt5(doc.imponibile)}</strong>
          {" · "}<button type="button" className="link-btn" onClick={ripristina}>ripristina le impostazioni</button>
        </span>
        <button type="button" className="btn" onClick={onClose}>Chiudi</button>
        <button type="button" className="btn" onClick={() => scarica("excel")} disabled={busy}><Icon5 name="download" /> Scarica Excel</button>
        <button type="button" className="btn primary" onClick={() => scarica("pdf")} disabled={busy}><Icon5 name="pdf" /> Scarica PDF</button>
      </>}
    >
      <div className="doc-export">
        <div className="doc-export-opzioni">
          <section>
            <div className="label">Testi del documento</div>
            <div className="doc-export-due">
              <div className="field">
                <label className="label-sm">Intestazione</label>
                <input className="input" value={testi.etichetta} onChange={setT("etichetta")} placeholder={tipo} />
              </div>
              <div className="field">
                <label className="label-sm">Data</label>
                <input className="input" type="date" value={testi.data || ""} onChange={setT("data")} />
              </div>
            </div>
            <div className="field">
              <label className="label-sm">Oggetto</label>
              <input className="input" value={testi.titolo} onChange={setT("titolo")} />
            </div>
            <div className="field">
              <label className="label-sm">Cliente</label>
              <input className="input" value={testi.cliente} onChange={setT("cliente")} />
            </div>
            <div className="field">
              <label className="label-sm">Riferimento accanto alla data</label>
              <input className="input" value={testi.sottotitolo || ""} onChange={setT("sottotitolo")} placeholder="es. Versione v2" />
            </div>
            <div className="field">
              <label className="label-sm">Nota sotto l'intestazione</label>
              <textarea className="input" rows={2} value={testi.nota} onChange={setT("nota")} />
            </div>
            <div className="field">
              <label className="label-sm">Condizioni in fondo (una per riga)</label>
              <textarea className="input" rows={3} value={testi.condizioni} onChange={setT("condizioni")} />
            </div>
            {onSalvaTesti && etichettaAggiornaDati && (
              <Spunta5 checked={aggiornaDati} onChange={setAggiornaDati} testo={etichettaAggiornaDati} />
            )}
          </section>

          <section>
            <div className="label">Impostazione</div>
            <div className="segmented" style={{ display: "flex" }} role="group" aria-label="Impostazione del documento">
              <button type="button" className={!macro ? "active" : ""} style={{ flex: 1 }} onClick={() => setO("modo")("voci")}>Per voci</button>
              <button type="button" className={macro ? "active" : ""} style={{ flex: 1 }} onClick={() => setO("modo")("macroaree")}>Per macro aree</button>
            </div>
            {macro && (
              <>
                <Spunta5 checked={opz.dettaglioVoci} onChange={setO("dettaglioVoci")} testo="Elenca le voci in ogni macro area"
                  aiuto="Senza spunta il cliente vede solo il totale di ogni macro area." />
                <Spunta5 checked={opz.colTotale !== false} onChange={setO("colTotale")} disabled={!opz.dettaglioVoci}
                  testo="Importo di ogni voce" aiuto="Senza spunta resta solo il totale della macro area." />
                <Spunta5 checked={opz.riepilogoAree} onChange={setO("riepilogoAree")} disabled={!opz.dettaglioVoci}
                  testo="Tabella dei totali per macro area in testa" />
                {azioniMacroAree}
              </>
            )}
          </section>

          <section>
            <div className="label">Colonne</div>
            <Spunta5 checked={opz.colQta} onChange={setO("colQta")} disabled={senzaColonne} testo="Quantità" />
            <Spunta5 checked={opz.colGiorni} onChange={setO("colGiorni")} disabled={senzaColonne} testo="Giorni" />
            <Spunta5 checked={opz.colPrezzo} onChange={setO("colPrezzo")} disabled={senzaColonne} testo="Prezzo unitario" />
            <Spunta5 checked={opz.colNote} onChange={setO("colNote")} disabled={senzaColonne || !ciSonoNote}
              testo="Note delle voci" aiuto={ciSonoNote ? "Accanto alla descrizione." : "Nessuna voce ha una nota."} />
            {multi && (
              <Spunta5 checked={opz.colProgetto} onChange={setO("colProgetto")}
                disabled={senzaColonne || (!macro && opz.raggruppaProgetto)} testo="Progetto" />
            )}
          </section>

          <section>
            <div className="label">Sezioni</div>
            {multi && !macro && (
              <Spunta5 checked={opz.raggruppaProgetto} onChange={setO("raggruppaProgetto")} testo="Una tabella per ogni progetto" />
            )}
            {!macro && (
              <>
                <Spunta5 checked={opz.sezStraordinari} onChange={setO("sezStraordinari")} testo="Straordinari"
                  aiuto="Se la togli, gli straordinari non entrano nemmeno nel totale." />
                <Spunta5 checked={opz.sezFuoriPreventivo} onChange={setO("sezFuoriPreventivo")} testo="Fuori preventivo" />
              </>
            )}
            {ciSonoPagamenti && <Spunta5 checked={opz.sezPagamenti} onChange={setO("sezPagamenti")} testo="Modalità di pagamento" />}
            {ciSonoEsclusioni && <Spunta5 checked={opz.sezEsclusioni} onChange={setO("sezEsclusioni")} testo="Cosa non è incluso" />}
            <Spunta5 checked={opz.sezCondizioni} onChange={setO("sezCondizioni")} testo="Condizioni in fondo" />
          </section>

          <section>
            <div className="label">Voci</div>
            <Spunta5 checked={opz.righeZero} onChange={setO("righeZero")} testo="Includi le voci a importo zero" />
            <Spunta5 checked={opz.righeNascoste} onChange={setO("righeNascoste")} testo="Includi le voci non visibili al cliente"
              aiuto="⚠️ Sono quelle marcate 🚫: di norma costi interni." />
            <button type="button" className="btn ghost" style={{ marginTop: 6 }} onClick={() => setElencoVoci(v => !v)}>
              {elencoVoci ? "Nascondi l'elenco" : "Scegli le singole voci"}{escluse.size > 0 ? ` · ${escluse.size} tolte` : ""}
            </button>
            {elencoVoci && (
              <div className="doc-export-voci">
                <div className="help" style={{ marginBottom: 4 }}>Valgono solo per questo documento: la voce resta nel {tipo.toLowerCase()}.</div>
                {(sorgente.voci || []).map(v => {
                  const nascosta = v.visibile_cliente === false && !opz.righeNascoste;
                  return (
                    <label key={v.id} className={nascosta ? "is-disabled" : ""}>
                      <input type="checkbox" disabled={nascosta} checked={!nascosta && !escluse.has(v.id)}
                        onChange={e => toggleVoce(v.id, e.target.checked)} />
                      <span>
                        {v.visibile_cliente === false && "🚫 "}{v.voce}
                        {multi && <span className="muted"> · {nomeProgetto(v.progetto_id)}</span>}
                      </span>
                      <span className="mono">{fmt5(CX5.rigaCliente(v).totale)}</span>
                    </label>
                  );
                })}
              </div>
            )}
          </section>
        </div>

        <div className="doc-export-anteprima">
          {errAnt ? <div className="login-error" style={{ margin: 12 }}>{errAnt}</div>
            : dati ? <PaginePDF5 dati={dati} />
            : <div className="doc-export-attesa"><span className="spinner" /> Preparo l'anteprima…</div>}
        </div>
      </div>
    </Modal5>
  );
}

// preventivi che si possono ancora mettere in un consuntivo: un progetto sta in uno solo
function preventiviDisponibili() {
  const occupati = new Set(consuntiviAll().flatMap(c => c.progetti.map(p => p.progettoId)));
  const PREV = window.AntaresData.PREVENTIVI || {};
  const progetti = window.AntaresData.PROGETTI || [];
  return Object.keys(PREV)
    .filter(pid => !occupati.has(pid))
    .map(pid => {
      const d = PREV[pid];
      const pr = progetti.find(p => p.id === pid);
      const versioni = d.versioni || [];
      const righe = [
        ...(d.preventivo?.ricavi || []).map(r => ({ ...r, tipo: "ricavo" })),
        ...(d.preventivo?.costi || []).map(r => ({ ...r, tipo: "costo" })),
      ];
      return {
        progettoId: pid,
        nome: d.progetto || pr?.nome || pid,
        cliente: d.cliente || pr?.cliente || "",
        anno: pr?.anno || null,
        versioni,
        versioneAttivaId: d.versioneAttivaId || versioni[versioni.length - 1]?.versioneId || null,
        totaleAttiva: CX5.totaleCliente(righe),
      };
    })
    .filter(p => p.versioneAttivaId)
    .sort((a, b) => (b.anno || 0) - (a.anno || 0) || String(a.nome).localeCompare(String(b.nome)));
}

function Spunta5({ checked, onChange, testo, aiuto, disabled }) {
  return (
    <label style={{ display: "flex", gap: 8, alignItems: "flex-start", cursor: disabled ? "default" : "pointer", padding: "6px 0", opacity: disabled ? 0.5 : 1 }}>
      <input type="checkbox" checked={!!checked} disabled={disabled} onChange={e => onChange(e.target.checked)} style={{ marginTop: 3 }} />
      <span>
        <span style={{ fontSize: 13 }}>{testo}</span>
        {aiuto && <div className="help" style={{ marginTop: 2 }}>{aiuto}</div>}
      </span>
    </label>
  );
}

function AvvisoMigrazione5() {
  const st = window.AntaresData.CONSUNTIVI_STATO || {};
  return (
    <div className="card">
      <div className="card-body">
        <Empty5 title="I consuntivi non sono ancora attivi">
          Manca l'aggiornamento del database (<span className="mono">db/24_consuntivi.sql</span>).
          {st.errore && <div className="help" style={{ marginTop: 6 }}>Dettaglio: {st.errore}</div>}
        </Empty5>
      </div>
    </div>
  );
}

/* ============================================================
   PAGINA CONSUNTIVI — elenco + dettaglio
   ============================================================ */
function Consuntivi() {
  const [aperto, setAperto] = useS5(null);
  const [filtro, setFiltro] = useS5("tutti");
  const [nuovo, setNuovo] = useS5(false);
  const lista = consuntiviAll();

  const testata = (
    <div className="page-head">
      <div className="page-title-block">
        <span className="eyebrow">Pianificazione commessa</span>
        <h1 className="h1">Consuntivi</h1>
      </div>
      {consuntivoPronto() && (
        <div className="page-actions">
          <button className="btn primary" onClick={() => setNuovo(true)}>
            <Icon5 name="plus" size={12} /> Nuovo consuntivo
          </button>
        </div>
      )}
    </div>
  );
  const modaleNuovo = nuovo && (
    <NuovoConsuntivoModal
      onClose={() => setNuovo(false)}
      onDone={(id) => { setNuovo(false); if (id) setAperto(id); }}
    />
  );

  if (!consuntivoPronto()) {
    return <div className="stack">{testata}<AvvisoMigrazione5 /></div>;
  }
  if (aperto && lista.some(c => c.id === aperto)) {
    return (
      <div className="stack">
        {testata}
        <ConsuntivoDettaglio consuntivoId={aperto} onBack={() => setAperto(null)} />
        {modaleNuovo}
      </div>
    );
  }

  const filtrati = filtro === "tutti" ? lista : lista.filter(c => c.stato === filtro);
  const perStato = (id) => {
    const cc = lista.filter(c => c.stato === id);
    return { n: cc.length, totale: cc.reduce((s, c) => s + CX5.totaleCliente(c.voci), 0) };
  };
  const orfane = window.AntaresData.CONS_VOCI_ORFANE || [];

  return (
    <div className="stack">
      {testata}

      <div className="kpi-grid">
        {STATI_CONSUNTIVO.map(s => {
          const x = perStato(s.id);
          return (
            <div key={s.id} className={"kpi " + (s.colore === "teal" ? "" : s.colore)}>
              <div className="kpi-label">{s.label}</div>
              <div className="kpi-value">{fmt5(x.totale)}</div>
              <div className="kpi-sub">
                {x.n} {x.n === 1 ? "consuntivo" : "consuntivi"} · imponibile
                {s.id === "inviato" && x.n > 0 ? " da incassare" : ""}
              </div>
            </div>
          );
        })}
      </div>

      <div className="card">
        <div className="card-bar" />
        <div className="card-head" style={{ flexWrap: "wrap", gap: 8 }}>
          <h3 className="h3">Tutti i consuntivi</h3>
          <div className="segmented compact">
            <button className={filtro === "tutti" ? "active" : ""} onClick={() => setFiltro("tutti")}>Tutti</button>
            {STATI_CONSUNTIVO.map(s => (
              <button key={s.id} className={filtro === s.id ? "active" : ""} onClick={() => setFiltro(s.id)}>{s.label}</button>
            ))}
          </div>
        </div>
        {orfane.length > 0 && (
          <div className="help" style={{ padding: "0 16px 10px" }}>
            {orfane.length === 1
              ? "C'è 1 voce inserita a mano su un progetto che non ha ancora un consuntivo"
              : `Ci sono ${orfane.length} voci inserite a mano su progetti che non hanno ancora un consuntivo`}
            : entrano nel consuntivo quando lo crei.
          </div>
        )}
        <div className="table-wrap">
          <table className="table">
            <thead>
              <tr>
                <th>Consuntivo</th>
                <th>Cliente</th>
                <th>Data</th>
                <th className="num">Voci</th>
                <th className="num">Totale al cliente</th>
                <th>Stato</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {filtrati.length === 0 && (
                <tr><td colSpan={7}>
                  <Empty5
                    title={lista.length === 0 ? "Nessun consuntivo" : "Nessun consuntivo in questo stato"}
                    action={lista.length === 0 && (
                      <button className="btn primary" style={{ marginTop: 12 }} onClick={() => setNuovo(true)}>
                        <Icon5 name="plus" size={12} /> Crea il primo consuntivo
                      </button>
                    )}
                  >
                    {lista.length === 0 && "Si crea da uno o più preventivi: le voci vengono copiate e poi le modifichi."}
                  </Empty5>
                </td></tr>
              )}
              {filtrati.map(c => {
                const st = statoInfo5(c.stato);
                return (
                  <tr key={c.id} style={{ cursor: "pointer" }} onClick={() => setAperto(c.id)}>
                    <td>
                      <strong>{c.titolo}</strong>
                      <div className="muted" style={{ fontSize: 12 }}>
                        {c.progetti.length === 0 ? "nessun progetto" : c.progetti.map(p => p.nome).join(" · ")}
                      </div>
                    </td>
                    <td>{c.cliente || <span className="muted">—</span>}</td>
                    <td className="mono">{c.dataIt}</td>
                    <td className="num mono">{c.voci.length}</td>
                    <td className="num"><strong>{fmt5(CX5.totaleCliente(c.voci))}</strong></td>
                    <td><span className={"pill " + st.colore}>{st.label}</span></td>
                    <td className="row-actions">
                      <button className="btn-row" onClick={(e) => { e.stopPropagation(); setAperto(c.id); }}>Apri</button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>
      {modaleNuovo}
    </div>
  );
}

/* ============================================================
   DETTAGLIO CONSUNTIVO — usato sia qui sia nella scheda del preventivo
   ============================================================ */
function ConsuntivoDettaglio({ consuntivoId, embedded = false, onBack }) {
  const [modale, setModale] = useS5(null); // {tipo:"voce"|"dati"|"aggiungi"|"export"|"macroaree", ...}
  const [busy, setBusy] = useS5(false);
  const cons = consuntiviAll().find(c => c.id === consuntivoId);

  if (!cons) {
    return (
      <div className="card">
        <div className="card-body">
          <Empty5 title="Consuntivo non trovato"
            action={onBack && <button className="btn" style={{ marginTop: 12 }} onClick={onBack}>Torna all'elenco</button>}>
            Forse è stato appena eliminato.
          </Empty5>
        </div>
      </div>
    );
  }

  const esegui = async (fn) => {
    setBusy(true);
    try {
      const res = await fn();
      if (res && res.error) { alert(res.error.message); return false; }
      await refreshCons5();
      return true;
    } catch (ex) {
      alert(ex.message || String(ex));
      return false;
    } finally { setBusy(false); }
  };

  const cambiaStato = (stato) => esegui(() => window.AntaresStore.saveConsuntivo(cons.id, { stato }));
  const rimuoviProgetto = (p) => {
    const n = cons.voci.filter(v => v.progetto_id === p.progettoId).length;
    const ultimo = cons.progetti.length === 1;
    if (!confirm(`Togliere "${p.nome}" dal consuntivo?\nVengono eliminate anche le sue ${n} voci. Il preventivo non viene toccato.`
      + (ultimo ? "\n\nÈ l'unico progetto: il consuntivo resta vuoto." : ""))) return;
    esegui(() => window.AntaresStore.rimuoviProgettoDaConsuntivo(cons.id, p.progettoId));
  };
  const copiaVoci = (p) => esegui(() => window.AntaresStore.copiaVociDalPreventivo(cons.id, p.progettoId, p.versioneId));
  const eliminaVoce = (v) => {
    if (!confirm(`Togliere la voce "${v.voce}" dal consuntivo?`)) return;
    esegui(() => window.AntaresStore.deleteVoceConsuntivo(v.id));
  };
  const eliminaConsuntivo = async () => {
    if (!confirm(`Eliminare il consuntivo "${cons.titolo}" con tutte le sue ${cons.voci.length} voci?\nI preventivi non vengono toccati.`)) return;
    const ok = await esegui(() => window.AntaresStore.deleteConsuntivo(cons.id));
    if (ok && onBack) onBack();
  };

  // progetti collegati le cui voci di preventivo non sono (più) nel consuntivo,
  // es. il consuntivo recuperato dalle voci inserite a mano prima di questa versione
  const senzaVoci = cons.progetti.filter(p =>
    p.versioneId && p.righePreventivo.length > 0 &&
    !cons.voci.some(v => v.progetto_id === p.progettoId && v.origine_riga_id));

  const totCliente = CX5.totaleCliente(cons.voci);
  const totPreventivi = CX5.totaleCliente(cons.progetti.flatMap(p => p.righePreventivo));
  const conPreventivo = cons.progetti.some(p => p.versioneId);
  const delta = conPreventivo ? totCliente - totPreventivi : 0;
  const extra = cons.voci.filter(v => v.tipo_extra);
  const aggiunte = cons.voci.filter(v => !v.origine_riga_id).length;
  const st = statoInfo5(cons.stato);

  return (
    <>
      <div className="card">
        <div className="card-bar" />
        <div className="card-head" style={{ alignItems: "flex-start", flexWrap: "wrap", gap: 12 }}>
          <div style={{ minWidth: 0 }}>
            {onBack && (
              <button className="btn ghost" style={{ marginBottom: 8 }} onClick={onBack}>← Tutti i consuntivi</button>
            )}
            <h3 className="h3">{cons.titolo}</h3>
            <div className="muted" style={{ fontSize: 12 }}>
              {cons.cliente || "Cliente non indicato"} · {cons.dataIt}
              {embedded && <> · <span className={"pill " + st.colore}>{st.label}</span></>}
            </div>
            {cons.nota && <div className="help" style={{ marginTop: 4 }}>{cons.nota}</div>}
          </div>
          <div className="row" style={{ gap: 6 }}>
            {!embedded && (
              <select className="select" style={{ width: "auto" }} value={cons.stato} disabled={busy} onChange={e => cambiaStato(e.target.value)} aria-label="Stato del consuntivo">
                {STATI_CONSUNTIVO.map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
              </select>
            )}
            <button className="btn ghost" onClick={() => setModale({ tipo: "dati" })}><Icon5 name="edit" size={12} /> Dati</button>
            <button className="btn ghost" onClick={() => setModale({ tipo: "macroaree" })}>
              <Icon5 name="chart" /> Macro aree
            </button>
            <button className="btn primary" onClick={() => setModale({ tipo: "export" })}><Icon5 name="pdf" /> Esporta</button>
            <button className="btn danger" disabled={busy} onClick={eliminaConsuntivo}>Elimina</button>
          </div>
        </div>
        <div className="card-body">
          <div className="label" style={{ marginBottom: 6 }}>Progetti nel consuntivo</div>
          <div className="cons-progetti">
            {cons.progetti.map(p => (
              <span className="cons-progetto-chip" key={p.progettoId}>
                {p.nome}
                {p.versioneId
                  ? p.versioneEtichetta && <span className="muted mono" style={{ fontSize: 11 }}>{p.versioneEtichetta}</span>
                  : <span className="muted" style={{ fontSize: 11 }}>senza preventivo</span>}
                <button type="button" title="Togli dal consuntivo" aria-label={"Togli " + p.nome + " dal consuntivo"} disabled={busy} onClick={() => rimuoviProgetto(p)}>×</button>
              </span>
            ))}
            {cons.progetti.length === 0 && <span className="muted" style={{ fontSize: 13 }}>Nessun progetto</span>}
            <button className="btn ghost" onClick={() => setModale({ tipo: "aggiungi" })}>
              <Icon5 name="plus" size={12} /> Aggiungi progetti
            </button>
          </div>
          {senzaVoci.map(p => (
            <div key={p.progettoId} className="row" style={{ marginTop: 10, justifyContent: "space-between", padding: "8px 12px", background: "var(--surface-2)", borderRadius: "var(--r)" }}>
              <span style={{ fontSize: 13 }}>
                Le {p.righePreventivo.length} voci del preventivo di <strong>{p.nome}</strong> ({p.versioneEtichetta || "versione collegata"}) non sono nel consuntivo.
              </span>
              <button className="btn" disabled={busy} onClick={() => copiaVoci(p)}>Copia le voci dal preventivo</button>
            </div>
          ))}
        </div>
      </div>

      <div className="kpi-grid">
        <div className="kpi">
          <div className="kpi-label">Totale al cliente</div>
          <div className="kpi-value">{fmt5(totCliente)}</div>
          <div className="kpi-sub">imponibile, IVA esclusa · voci visibili</div>
        </div>
        <div className={"kpi " + (delta < 0 ? "coral" : "blue")}>
          <div className="kpi-label">Rispetto ai preventivi</div>
          <div className="kpi-value">{Math.abs(delta) < 0.005 ? "—" : fmt5(delta, { sign: true })}</div>
          <div className="kpi-sub">{conPreventivo ? "preventivi " + fmt5(totPreventivi) : "nessun preventivo collegato"}</div>
        </div>
        <div className={"kpi " + (extra.length > 0 ? "yellow" : "")}>
          <div className="kpi-label">Straordinari e fuori preventivo</div>
          <div className="kpi-value">{fmt5(CX5.totaleCliente(extra))}</div>
          <div className="kpi-sub">{extra.length} {extra.length === 1 ? "voce" : "voci"}</div>
        </div>
        <div className="kpi">
          <div className="kpi-label">Voci</div>
          <div className="kpi-value">{cons.voci.length}</div>
          <div className="kpi-sub">
            {cons.voci.length === 0 ? "ancora nessuna voce"
              : !conPreventivo ? "inserite a mano"
              : aggiunte === 0 ? "tutte dai preventivi"
              : aggiunte === 1 ? "1 aggiunta rispetto ai preventivi"
              : `${aggiunte} aggiunte rispetto ai preventivi`}
          </div>
        </div>
      </div>

      <VociConsuntivoCard cons={cons} tipo="ricavo"
        onAdd={() => setModale({ tipo: "voce", tipoVoce: "ricavo" })}
        onEdit={(v) => setModale({ tipo: "voce", tipoVoce: "ricavo", editing: v })}
        onDelete={eliminaVoce} />
      <VociConsuntivoCard cons={cons} tipo="costo"
        onAdd={() => setModale({ tipo: "voce", tipoVoce: "costo" })}
        onEdit={(v) => setModale({ tipo: "voce", tipoVoce: "costo", editing: v })}
        onDelete={eliminaVoce} />

      <CostiRealiCard cons={cons} />

      {modale?.tipo === "voce" && (
        <VoceConsuntivoModal cons={cons} tipo={modale.tipoVoce} editing={modale.editing} onClose={() => setModale(null)} />
      )}
      {modale?.tipo === "dati" && <DatiConsuntivoModal cons={cons} onClose={() => setModale(null)} />}
      {modale?.tipo === "aggiungi" && (
        <NuovoConsuntivoModal consuntivo={cons} onClose={() => setModale(null)} onDone={() => setModale(null)} />
      )}
      {modale?.tipo === "export" && (
        <EsportaConsuntivoModal cons={cons} onClose={() => setModale(null)}
          onMacroAree={() => setModale({ tipo: "macroaree", tornaExport: true })} />
      )}
      {modale?.tipo === "macroaree" && (
        <MacroAreeModal cons={cons}
          onClose={() => setModale(modale.tornaExport ? { tipo: "export" } : null)}
          onSalvata={() => setModale(modale.tornaExport ? { tipo: "export" } : null)} />
      )}
    </>
  );
}

/* ---------- tabella voci (ricavi o costi), raggruppate per progetto ---------- */
function RigaVoce5({ v, costo, senzaPreventivo, onEdit, onDelete, dnd }) {
  const nascosta = v.visibile_cliente === false;
  const alCliente = totRiga5(v) * (1 + (Number(v.markup) || 0) / 100);
  return (
    <tr className={[v.tipo_extra ? "row-warn" : "", dnd ? dnd.classe(v.id) : ""].filter(Boolean).join(" ")}
      style={nascosta ? { opacity: 0.6 } : undefined} {...(dnd ? dnd.bersaglio("voce", v.id) : {})}>
      <td className="dnd-cella">{dnd && <span {...dnd.maniglia("voce", v.id)}>⋮⋮</span>}</td>
      <td>
        {nascosta && <span title="Non visibile al cliente" style={{ marginRight: 6 }}>🚫</span>}
        {v.voce}
        {v.tipo_extra === "straordinario" && <span className="pill yellow" style={{ marginLeft: 8 }}>straordinario</span>}
        {v.tipo_extra === "fuori_preventivo" && <span className="pill coral" style={{ marginLeft: 8 }}>fuori preventivo</span>}
        {!v.origine_riga_id && !senzaPreventivo && (
          <span className="pill blue" style={{ marginLeft: 8 }} title="Non viene dal preventivo: aggiunta nel consuntivo">aggiunta</span>
        )}
        {v.nota && <div className="muted" style={{ fontSize: 12 }}>{v.nota}</div>}
      </td>
      {costo && <td className="muted" style={{ fontSize: 12 }}>{v.categoria || "—"}</td>}
      <td className="num mono">{v.qta}</td>
      <td className="num mono muted">{v.giorni > 0 ? v.giorni : "—"}</td>
      <td className="num">{fmt5(v.prezzo)}</td>
      {costo && <td className="num muted">{v.markup ? "+" + v.markup + "%" : "—"}</td>}
      <td className="num"><strong>{fmt5(totRiga5(v))}</strong></td>
      {costo && <td className="num">{nascosta ? "—" : fmt5(alCliente)}</td>}
      <td className="row-actions">
        <button className="btn-row" style={{ marginRight: 4 }} onClick={() => onEdit(v)}>Modifica</button>
        <button className="btn-row danger" onClick={() => onDelete(v)} aria-label={"Togli " + v.voce}>×</button>
      </td>
    </tr>
  );
}

function VociConsuntivoCard({ cons, tipo, onAdd, onEdit, onDelete }) {
  const costo = tipo === "costo";
  const voci = cons.voci.filter(v => v.tipo === tipo);
  const multi = cons.progetti.length > 1;
  const noti = new Set(cons.progetti.map(p => p.progettoId));
  const gruppi = multi
    ? cons.progetti.map(p => ({ key: p.progettoId, nome: p.nome, voci: voci.filter(v => v.progetto_id === p.progettoId) }))
    : [{ key: "tutte", nome: null, voci }];
  const altrove = multi ? voci.filter(v => !noti.has(v.progetto_id)) : [];
  if (altrove.length > 0) gruppi.push({ key: "altrove", nome: "Progetto non più nel consuntivo", voci: altrove });
  const nCol = costo ? 10 : 8;
  const totale = voci.reduce((s, v) => s + totRiga5(v), 0);
  const pers = costo ? riepPers5(voci, oneriParams5()) : null;

  // trascinamento: l'ordine cambia dentro lo stesso progetto
  const [errDnd, setErrDnd] = useS5("");
  const dnd = useTrascina5(async (da, a) => {
    const g = gruppi.find(x => x.voci.some(v => v.id === da.id));
    if (!g || !g.voci.some(v => v.id === a.id)) {
      setErrDnd("Le voci si spostano dentro lo stesso progetto.");
      return;
    }
    const ids = spostaInLista5(g.voci.map(v => v.id), da.id, a);
    const nuove = ids.map((id, i) => {
      const v = g.voci.find(x => x.id === id);
      return { id: v.id, ordinePrima: v.ordine, ordine: i };
    });
    setErrDnd("");
    const res = await window.AntaresStore.riordinaVociConsuntivo(nuove);
    if (res.error) { setErrDnd(res.error.message); return; }
    await refreshCons5();
  });

  return (
    <div className="card">
      <div className={"card-bar" + (costo ? " coral" : "")} />
      <div className="card-head">
        <div>
          <h3 className="h3">{costo ? "Costi" : "Ricavi"}</h3>
          <div className="muted" style={{ fontSize: 12 }}>
            {costo
              ? "Al cliente vanno col markup già dentro il prezzo, come nel preventivo"
              : "Voci di ricavo del lavoro fatto"}
          </div>
        </div>
        {/* ogni voce appartiene a un progetto: senza preventivi non c'è dove metterla */}
        <button className="btn ghost" onClick={onAdd} disabled={cons.progetti.length === 0}
          title={cons.progetti.length === 0 ? "Aggiungi prima un progetto al consuntivo" : undefined}>
          <Icon5 name="plus" size={12} /> Aggiungi voce
        </button>
      </div>
      {errDnd && <div className="login-error" style={{ margin: "0 14px 10px" }}>{errDnd}</div>}
      <div className="table-wrap">
        <table className="table preventivo-table cons-voci">
          <thead>
            <tr>
              <th className="dnd-cella" aria-label="Sposta"></th>
              <th>{costo ? "Voce di costo" : "Voce di ricavo"}</th>
              {costo && <th>Categoria</th>}
              <th className="num">Q.tà</th>
              <th className="num">Giorni</th>
              <th className="num">Prezzo</th>
              {costo && <th className="num">Markup</th>}
              <th className="num">Totale</th>
              {costo && <th className="num">Al cliente</th>}
              <th></th>
            </tr>
          </thead>
          <tbody>
            {voci.length === 0 && (
              <tr><td colSpan={nCol} className="empty">
                Nessuna voce di {costo ? "costo" : "ricavo"}. Usa "Aggiungi voce".
              </td></tr>
            )}
            {gruppi.map(g => (
              <React.Fragment key={g.key}>
                {g.nome && (
                  <tr className="row-group">
                    <td colSpan={nCol}>
                      {g.nome}
                      <span className="muted" style={{ fontWeight: 400, marginLeft: 8 }}>
                        {g.voci.length} {g.voci.length === 1 ? "voce" : "voci"} · {fmt5(g.voci.reduce((s, v) => s + totRiga5(v), 0))}
                      </span>
                    </td>
                  </tr>
                )}
                {g.voci.map(v => (
                  <RigaVoce5 key={v.id} v={v} costo={costo} onEdit={onEdit} onDelete={onDelete} dnd={dnd}
                    senzaPreventivo={!cons.progetti.find(p => p.progettoId === v.progetto_id)?.versioneId} />
                ))}
              </React.Fragment>
            ))}
            <tr className="row-total">
              <td className="dnd-cella"></td>
              <td>{costo ? "Totale costi" : "Totale ricavi"}</td>
              {costo && <td></td>}
              <td></td><td></td><td></td>
              {costo && <td></td>}
              <td className="num"><strong className={costo ? "neg" : "pos"}>{fmt5(totale)}</strong></td>
              {costo && <td className="num"><strong>{fmt5(CX5.totaleCliente(voci))}</strong></td>}
              <td></td>
            </tr>
          </tbody>
        </table>
      </div>
      {costo && pers && pers.oneri > 0 && (
        <div style={{ padding: "8px 14px", borderTop: "1px solid var(--border)" }}>
          <div className="row" style={{ justifyContent: "space-between" }}>
            <span className="muted">+ Oneri datore dipendenti (contributi, IRAP, INAIL, TFR, busta, CU)</span>
            <span className="mono">{fmt5(pers.oneri)}</span>
          </div>
          <div className="row" style={{ justifyContent: "space-between", marginTop: 4 }}>
            <strong>Totale costo azienda</strong>
            <strong className="mono neg">{fmt5(totale + pers.oneri)}</strong>
          </div>
        </div>
      )}
    </div>
  );
}

/* ---------- riquadro costi e incassi reali (non va negli export) ---------- */
function CostiRealiCard({ cons }) {
  const [dettaglio, setDettaglio] = useS5(false);
  const multi = cons.progetti.length > 1;
  const perProgetto = cons.progetti.map(p => ({ p, r: window.AntaresStore.costiReali(p.progettoId) }));
  const somma = (k) => perProgetto.reduce((s, x) => s + x.r[k], 0);
  const fatturato = somma("fatturato");
  const incassato = somma("incassato");
  const costi = somma("costi");
  const margine = fatturato - costi;
  const pct = fatturato > 0 ? (margine / fatturato) * 100 : null;
  const daFatturare = CX5.totaleCliente(cons.voci) - fatturato;
  const nessunDato = perProgetto.every(x =>
    x.r.attive.length === 0 && x.r.passive.length === 0 && x.r.collaboratori.length === 0);

  const documenti = perProgetto.flatMap(({ p, r }) => [
    ...r.attive.map(f => ({ key: "f" + f.id, data: f.data, progetto: p.nome, chi: f.controparte, tipo: "Fattura attiva", colore: "teal", importo: f.imponibile, stato: f.stato })),
    ...r.passive.map(f => ({ key: "f" + f.id, data: f.data, progetto: p.nome, chi: f.controparte, tipo: "Fattura passiva", colore: "coral", importo: f.imponibile, stato: f.stato })),
    ...r.collaboratori.map(c => ({ key: "c" + c.id, data: "", progetto: p.nome, chi: c.nome + (c.ruolo ? " · " + c.ruolo : ""), tipo: "Collaboratore", colore: "blue", importo: c.costoReale, stato: "" })),
  ]);

  const Dato = ({ label, valore, sub, cls }) => (
    <div>
      <div className="kpi-label">{label}</div>
      <div className={"mono " + (cls || "")} style={{ fontSize: 20, fontWeight: 600 }}>{valore}</div>
      {sub && <div className="help">{sub}</div>}
    </div>
  );

  return (
    <div className="card">
      <div className="card-bar muted" />
      <div className="card-head" style={{ flexWrap: "wrap", gap: 8 }}>
        <div>
          <h3 className="h3">Costi e incassi reali</h3>
          <div className="muted" style={{ fontSize: 12 }}>
            Da fatture e collaboratori collegati {multi ? "ai progetti" : "al progetto"} · per capire il margine · non compare negli export
          </div>
        </div>
        {!nessunDato && (
          <button className="btn ghost" onClick={() => setDettaglio(d => !d)}>
            {dettaglio ? "Nascondi fatture" : "Mostra fatture"}
          </button>
        )}
      </div>
      <div className="card-body">
        {nessunDato ? (
          <Empty5 title="Nessuna fattura collegata">
            Quando colleghi fatture o collaboratori {multi ? "a questi progetti" : "a questo progetto"}, qui vedi incassi, costi e margine reale.
          </Empty5>
        ) : (
          <div className="cons-reale-grid">
            <Dato label="Fatturato" valore={fmt5(fatturato)} sub="fatture attive, imponibile" />
            <Dato label="Incassato" valore={fmt5(incassato)}
              sub={fatturato - incassato > 0.005 ? "da incassare " + fmt5(fatturato - incassato) : "nulla da incassare"} />
            <Dato label="Costi reali" valore={fmt5(costi)} sub="fatture passive + collaboratori" cls="neg" />
            <Dato label="Margine reale" valore={fmt5(margine)} cls={margine < 0 ? "neg" : "pos"}
              sub={pct == null ? "nessun fatturato" : fmtPct5(pct) + " sul fatturato"} />
            <Dato label="Da fatturare" valore={fmt5(Math.max(0, daFatturare))} sub="totale consuntivo meno fatturato" />
          </div>
        )}
      </div>
      {!nessunDato && multi && (
        <div className="table-wrap" style={{ borderTop: "1px solid var(--border)" }}>
          <table className="table">
            <thead>
              <tr>
                <th>Progetto</th>
                <th className="num">Fatturato</th>
                <th className="num">Incassato</th>
                <th className="num">Costi reali</th>
                <th className="num">Margine</th>
                <th className="num">%</th>
              </tr>
            </thead>
            <tbody>
              {perProgetto.map(({ p, r }) => (
                <tr key={p.progettoId}>
                  <td>{p.nome}</td>
                  <td className="num">{fmt5(r.fatturato)}</td>
                  <td className="num">{fmt5(r.incassato)}</td>
                  <td className="num">{fmt5(r.costi)}</td>
                  <td className="num"><strong className={r.margine < 0 ? "neg" : "pos"}>{fmt5(r.margine)}</strong></td>
                  <td className="num muted">{r.pct == null ? "—" : fmtPct5(r.pct)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
      {!nessunDato && dettaglio && (
        <div className="table-wrap" style={{ borderTop: "1px solid var(--border)" }}>
          <table className="table">
            <thead>
              <tr>
                <th>Data</th>
                {multi && <th>Progetto</th>}
                <th>Controparte</th>
                <th>Tipo</th>
                <th className="num">Imponibile</th>
                <th>Stato</th>
              </tr>
            </thead>
            <tbody>
              {documenti.map(d => (
                <tr key={d.key}>
                  <td className="mono">{d.data || "—"}</td>
                  {multi && <td className="muted" style={{ fontSize: 12 }}>{d.progetto}</td>}
                  <td>{d.chi}</td>
                  <td><span className={"pill " + d.colore}>{d.tipo}</span></td>
                  <td className="num">{fmt5(d.importo)}</td>
                  <td className="muted" style={{ fontSize: 12 }}>{d.stato || "—"}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

/* ============================================================
   MODALI
   ============================================================ */

/* ---------- nuovo consuntivo, oppure aggiunta di preventivi a uno esistente ---------- */
function NuovoConsuntivoModal({ consuntivo = null, preselezionati = {}, onClose, onDone }) {
  const disponibili = preventiviDisponibili();
  const [selezione, setSelezione] = useS5(() => {
    const iniziale = {};
    Object.entries(preselezionati || {}).forEach(([pid, vid]) => {
      const p = disponibili.find(x => x.progettoId === pid);
      if (p) iniziale[pid] = (vid && p.versioni.some(v => v.versioneId === vid)) ? vid : p.versioneAttivaId;
    });
    return iniziale;
  });
  const [titolo, setTitolo] = useS5("");
  const [cliente, setCliente] = useS5(null); // null = quello suggerito dal primo preventivo scelto
  const [cerca, setCerca] = useS5("");
  const [busy, setBusy] = useS5(false);
  const [err, setErr] = useS5("");
  // "preventivi" = copia le voci dai preventivi · "libero" = senza preventivo (produzioni veloci)
  const [modo, setModo] = useS5(disponibili.length === 0 ? "libero" : "preventivi");
  const [progLibero, setProgLibero] = useS5(""); // "" = nuovo progetto creato al volo
  const [nomeProg, setNomeProg] = useS5("");
  const [linea, setLinea] = useS5("Service");
  const [data, setData] = useS5(() => new Date().toISOString().slice(0, 10));
  const libero = modo === "libero";

  const occupati = new Set(consuntiviAll().flatMap(c => c.progetti.map(p => p.progettoId)));
  const progettiLiberi = (window.AntaresData.PROGETTI || [])
    .filter(p => !occupati.has(p.id))
    .sort((a, b) => (b.anno || 0) - (a.anno || 0) || String(a.nome).localeCompare(String(b.nome)));
  const progettoScelto = progettiLiberi.find(p => p.id === progLibero) || null;

  const scelti = disponibili.filter(p => selezione[p.progettoId]);
  const clienteRif = String(consuntivo?.cliente
    || (libero ? progettoScelto?.cliente : scelti[0]?.cliente) || "").trim();
  const clienteEff = cliente == null ? clienteRif : cliente;
  const titoloSuggerito = libero ? (progettoScelto?.nome || "")
    : scelti.length === 1 ? scelti[0].nome
    : scelti.length > 1 ? `${clienteEff || "Consuntivo"} · ${scelti.length} progetti` : "";

  const q = cerca.trim().toLowerCase();
  const visibili = disponibili.filter(p =>
    !q || selezione[p.progettoId] || (p.nome + " " + p.cliente).toLowerCase().includes(q));
  const stessoCliente = (p) => clienteRif && p.cliente.trim().toLowerCase() === clienteRif.toLowerCase();
  const gruppi = (clienteRif
    ? [
        { titolo: "Stesso cliente · " + clienteRif, voci: visibili.filter(stessoCliente) },
        { titolo: "Altri preventivi", voci: visibili.filter(p => !stessoCliente(p)) },
      ]
    : [{ titolo: "Preventivi senza consuntivo", voci: visibili }]
  ).filter(g => g.voci.length > 0);

  const scegli = (p, on) => setSelezione(s => {
    const n = { ...s };
    if (on) n[p.progettoId] = p.versioneAttivaId; else delete n[p.progettoId];
    return n;
  });

  const confermaLibero = async () => {
    const titoloEff = titolo.trim() || titoloSuggerito;
    if (!consuntivo && !titoloEff) { setErr("Scrivi il titolo del consuntivo"); return; }
    if (consuntivo && !progLibero && !nomeProg.trim()) { setErr("Scrivi il nome del progetto"); return; }
    setBusy(true); setErr("");
    try {
      const res = consuntivo
        ? await window.AntaresStore.collegaProgettoSenzaPreventivo(consuntivo.id, progLibero || null,
            { nome: nomeProg, cliente: consuntivo.cliente, linea, data: consuntivo.data })
        : await window.AntaresStore.creaConsuntivoSenzaPreventivo(
            { titolo: titoloEff, cliente: clienteEff, data, progettoId: progLibero || null, linea });
      if (res.error) { setErr(res.error.message); setBusy(false); return; }
      await refreshCons5();
      onDone && onDone(consuntivo?.id || res.id);
    } catch (ex) {
      setErr(ex.message || String(ex));
      setBusy(false);
    }
  };

  const conferma = async () => {
    if (libero) return confermaLibero();
    const versioni = Object.values(selezione).filter(Boolean);
    if (versioni.length === 0) {
      setErr("Spunta almeno un preventivo dalla lista, oppure scegli \"Senza preventivo\".");
      return;
    }
    setBusy(true); setErr("");
    try {
      let id = consuntivo?.id || null;
      let res;
      if (consuntivo) {
        res = await window.AntaresStore.aggiungiPreventiviAConsuntivo(consuntivo.id, versioni);
      } else {
        res = await window.AntaresStore.creaConsuntivo(titolo.trim() || titoloSuggerito, clienteEff, versioni);
        id = res.id;
      }
      if (res.error) { setErr(res.error.message); setBusy(false); return; }
      await refreshCons5();
      onDone && onDone(id);
    } catch (ex) {
      setErr(ex.message || String(ex));
      setBusy(false);
    }
  };

  return (
    <Modal5
      title={consuntivo ? "Aggiungi al consuntivo" : "Nuovo consuntivo"}
      subtitle={libero ? "Senza preventivo: le voci le inserisci tu nel consuntivo"
        : "Le voci dei preventivi scelti vengono copiate: poi le modifichi liberamente"}
      onClose={onClose} width={640}
      footer={<>
        <span className="help modal-foot-left">
          {libero ? "Le voci le aggiungi dopo"
            : scelti.length === 0 ? "Spunta almeno un preventivo"
            : `${scelti.length} ${scelti.length === 1 ? "preventivo scelto" : "preventivi scelti"}`}
        </span>
        <button type="button" className="btn" onClick={onClose}>Annulla</button>
        <button type="button" className="btn primary" onClick={conferma} disabled={busy}>
          {busy ? "Un momento…" : consuntivo ? "Aggiungi" : "Crea consuntivo"}
        </button>
      </>}
    >
      {err && <div className="login-error" style={{ marginBottom: 12 }}>{err}</div>}
      <div className="segmented" style={{ display: "flex", marginBottom: 12 }} role="group" aria-label="Come nasce il consuntivo">
        <button type="button" className={!libero ? "active" : ""} style={{ flex: 1 }}
          onClick={() => { setModo("preventivi"); setErr(""); }}>Dai preventivi</button>
        <button type="button" className={libero ? "active" : ""} style={{ flex: 1 }}
          onClick={() => { setModo("libero"); setErr(""); }}>Senza preventivo</button>
      </div>
      {!consuntivo && (
        <div className="form-grid" style={{ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", marginBottom: 12 }}>
          <div className="field">
            <label className="label">Titolo</label>
            <input className="input" value={titolo} onChange={e => setTitolo(e.target.value)}
              placeholder={titoloSuggerito || "es. Dirette di settembre"} />
          </div>
          <div className="field">
            <label className="label">Cliente</label>
            <input className="input" value={clienteEff} onChange={e => setCliente(e.target.value)} />
          </div>
          {libero && (
            <div className="field">
              <label className="label">Data</label>
              <input className="input" type="date" value={data} onChange={e => setData(e.target.value)} />
            </div>
          )}
        </div>
      )}
      {libero ? (
        <>
          <div className="form-grid" style={{ gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", marginBottom: 8 }}>
            <div className="field">
              <label className="label">Progetto</label>
              <select className="select" value={progLibero} onChange={e => setProgLibero(e.target.value)}>
                <option value="">{consuntivo ? "Nuovo progetto" : "Nuovo progetto, con il titolo del consuntivo"}</option>
                {progettiLiberi.map(p => (
                  <option key={p.id} value={p.id}>{p.nome}{p.cliente ? " · " + p.cliente : ""}</option>
                ))}
              </select>
            </div>
            {!progLibero && consuntivo && (
              <div className="field">
                <label className="label">Nome del progetto</label>
                <input className="input" value={nomeProg} onChange={e => setNomeProg(e.target.value)} placeholder="es. Riprese extra 20 settembre" />
              </div>
            )}
            {!progLibero && (
              <div className="field">
                <label className="label">Linea</label>
                <select className="select" value={linea} onChange={e => setLinea(e.target.value)}>
                  {(window.AntaresData.LINEE_BUSINESS || ["Service"]).map(L => <option key={L}>{L}</option>)}
                </select>
              </div>
            )}
          </div>
          <div className="help">
            Il progetto serve a riconoscere le voci e a collegarci fatture e collaboratori per vedere il margine.
            {!progLibero && " Lo creo io, non devi passare dalla gestione progetti."}
          </div>
        </>
      ) : disponibili.length === 0 ? (
        <Empty5 title="Nessun preventivo disponibile">
          Tutti i progetti con un preventivo sono già in un consuntivo. Scegli "Senza preventivo" per inserire le voci a mano.
        </Empty5>
      ) : (
        <>
          <div className="field" style={{ marginBottom: 8 }}>
            <input className="input" placeholder="Cerca progetto o cliente" value={cerca}
              onChange={e => setCerca(e.target.value)} aria-label="Cerca preventivi" />
          </div>
          <div className="cons-lista-prev">
            {gruppi.length === 0 && <div className="cons-area-vuota">Nessun preventivo corrisponde alla ricerca.</div>}
            {gruppi.map(g => (
              <React.Fragment key={g.titolo}>
                <div className="cons-lista-gruppo">{g.titolo}</div>
                {g.voci.map(p => {
                  const sel = selezione[p.progettoId];
                  return (
                    <label key={p.progettoId}>
                      <input type="checkbox" checked={!!sel} onChange={e => scegli(p, e.target.checked)} />
                      <span style={{ minWidth: 0 }}>
                        <strong>{p.nome}</strong>
                        {p.cliente && <span className="muted"> · {p.cliente}</span>}
                        <div className="help">versione attiva: {fmt5(p.totaleAttiva)} al cliente</div>
                      </span>
                      {p.versioni.length > 1 ? (
                        <select className="select" value={sel || p.versioneAttivaId} disabled={!sel}
                          aria-label={"Versione di " + p.nome}
                          onChange={e => setSelezione(s => ({ ...s, [p.progettoId]: e.target.value }))}>
                          {p.versioni.map(v => (
                            <option key={v.versioneId} value={v.versioneId}>{v.etichetta} · {v.data}</option>
                          ))}
                        </select>
                      ) : (
                        <span className="muted mono" style={{ fontSize: 12 }}>{p.versioni[0]?.etichetta}</span>
                      )}
                    </label>
                  );
                })}
              </React.Fragment>
            ))}
          </div>
        </>
      )}
      {!libero && (
        <div className="help" style={{ marginTop: 8 }}>
          Un progetto sta in un solo consuntivo: qui compaiono solo i preventivi ancora liberi.
          Non c'è un preventivo? Scegli "Senza preventivo".
        </div>
      )}
    </Modal5>
  );
}

/* ---------- dati del consuntivo ---------- */
function DatiConsuntivoModal({ cons, onClose }) {
  const [titolo, setTitolo] = useS5(cons.titolo || "");
  const [cliente, setCliente] = useS5(cons.cliente || "");
  const [data, setData] = useS5(cons.data || new Date().toISOString().slice(0, 10));
  const [nota, setNota] = useS5(cons.nota || "");
  const [busy, setBusy] = useS5(false);
  const [err, setErr] = useS5("");

  const salva = async () => {
    if (!titolo.trim()) { setErr("Il titolo è obbligatorio"); return; }
    if (!data) { setErr("La data è obbligatoria"); return; }
    setBusy(true); setErr("");
    const res = await window.AntaresStore.saveConsuntivo(cons.id, {
      titolo: titolo.trim(), cliente: cliente.trim() || null, data, nota: nota.trim() || null,
    });
    if (res.error) { setErr(res.error.message); setBusy(false); return; }
    await refreshCons5();
    onClose();
  };

  return (
    <Modal5 title="Dati del consuntivo" onClose={onClose} width={520}
      footer={<>
        <button type="button" className="btn" onClick={onClose}>Annulla</button>
        <button type="button" className="btn primary" onClick={salva} disabled={busy}>{busy ? "Salvo…" : "Salva"}</button>
      </>}>
      {err && <div className="login-error" style={{ marginBottom: 12 }}>{err}</div>}
      <div className="field" style={{ marginBottom: 12 }}>
        <label className="label">Titolo</label>
        <input className="input" value={titolo} onChange={e => setTitolo(e.target.value)} autoFocus />
        <span className="help">Compare come "Oggetto" nel PDF e nell'Excel.</span>
      </div>
      <div className="form-grid" style={{ gridTemplateColumns: "2fr 1fr", marginBottom: 12 }}>
        <div className="field">
          <label className="label">Cliente</label>
          <input className="input" value={cliente} onChange={e => setCliente(e.target.value)} />
        </div>
        <div className="field">
          <label className="label">Data</label>
          <input className="input" type="date" value={data} onChange={e => setData(e.target.value)} />
        </div>
      </div>
      <div className="field">
        <label className="label">Nota</label>
        <textarea className="input" rows={3} value={nota} onChange={e => setNota(e.target.value)}
          placeholder="es. Riprese del 12 e 13 settembre, diretta inclusa" />
        <span className="help">Compare sotto l'intestazione del documento per il cliente.</span>
      </div>
    </Modal5>
  );
}

/* ---------- voce del consuntivo (nuova o da modificare) ---------- */
const CATEGORIE_BASE5 = ["Collaboratori", "Noleggio", "Trasferte", "Trasporti", "Catering", "Servizi", "Materiali", "Altro"];

function VoceConsuntivoModal({ cons, tipo, editing, onClose }) {
  const costo = tipo === "costo";
  const multi = cons.progetti.length > 1;
  const [progettoId, setProgettoId] = useS5(editing?.progetto_id || cons.progetti[0]?.progettoId || "");
  const [voce, setVoce]           = useS5(editing?.voce || "");
  const [categoria, setCategoria] = useS5(editing?.categoria || "");
  const [regime, setRegime]       = useS5(editing?.regime || "");
  const [qta, setQta]             = useS5(String(editing?.qta ?? 1));
  const [giorni, setGiorni]       = useS5(editing?.giorni ? String(editing.giorni) : "");
  const [prezzo, setPrezzo]       = useS5(editing ? String(editing.prezzo) : "");
  const [markup, setMarkup]       = useS5(String(editing?.markup || 0));
  const [nota, setNota]           = useS5(editing?.nota || "");
  const [oneriACliente, setOneriACliente] = useS5(editing?.oneri_a_cliente !== false);
  const [visibile, setVisibile]   = useS5(editing?.visibile_cliente !== false);
  const [tipoExtra, setTipoExtra] = useS5(editing?.tipo_extra || "");
  const [busy, setBusy]           = useS5(false);
  const [err, setErr]             = useS5("");

  const categorie = [...new Set([...CATEGORIE_BASE5, ...cons.voci.map(v => v.categoria).filter(Boolean)])];
  const bozza = { qta: numIt5(qta) || 1, giorni: numIt5(giorni), prezzo: numIt5(prezzo) };
  const totale = totRiga5(bozza);

  const salva = async () => {
    if (!voce.trim()) { setErr("La descrizione è obbligatoria"); return; }
    if (!progettoId) { setErr("Scegli il progetto della voce"); return; }
    setBusy(true); setErr("");
    try {
      const payload = {
        progetto_id: progettoId, tipo, voce: voce.trim(),
        // anche i ricavi: la categoria è la macro area copiata dal preventivo
        categoria: categoria.trim() || null,
        regime: costo ? (regime || null) : null,
        qta: bozza.qta, giorni: bozza.giorni, prezzo: bozza.prezzo,
        markup: costo ? numIt5(markup) : 0,
        nota: nota.trim() || null,
        oneri_a_cliente: oneriACliente,
        visibile_cliente: visibile,
        tipo_extra: tipoExtra || null,
      };
      if (editing) payload.id = editing.id;
      else payload.ordine = cons.voci.reduce((m, v) => Math.max(m, v.ordine || 0), 0) + 1;
      const res = await window.AntaresStore.saveVoceConsuntivo(cons.id, payload);
      if (res.error) { setErr(res.error.message); setBusy(false); return; }
      await refreshCons5();
      onClose();
    } catch (ex) {
      setErr(ex.message || String(ex));
      setBusy(false);
    }
  };

  return (
    <Modal5
      title={editing ? "Modifica voce" : (costo ? "Nuova voce di costo" : "Nuova voce di ricavo")}
      subtitle={editing && editing.origine_riga_id ? "Copiata dal preventivo: la modifica vale solo per il consuntivo" : "Voce del consuntivo"}
      onClose={onClose} width={560}
      footer={<>
        <button type="button" className="btn" onClick={onClose}>Annulla</button>
        <button type="button" className="btn primary" onClick={salva} disabled={busy}>
          {busy ? "Salvo…" : (editing ? "Aggiorna" : "Aggiungi")}
        </button>
      </>}
    >
      {err && <div className="login-error" style={{ marginBottom: 12 }}>{err}</div>}
      {multi && (
        <div className="field" style={{ marginBottom: 12 }}>
          <label className="label">Progetto</label>
          <select className="select" value={progettoId} onChange={e => setProgettoId(e.target.value)}>
            {cons.progetti.map(p => <option key={p.progettoId} value={p.progettoId}>{p.nome}</option>)}
          </select>
        </div>
      )}
      <div className="field" style={{ marginBottom: 12 }}>
        <label className="label">Descrizione</label>
        <input className="input" value={voce} onChange={e => setVoce(e.target.value)} autoFocus
          placeholder={costo ? "es. Parcheggio e ZTL giornate riprese" : "es. Giornata di riprese aggiuntiva"} />
      </div>
      {!costo && (
        <div className="field" style={{ marginBottom: 12 }}>
          <label className="label">Macro area</label>
          <input className="input" list="cons-categorie" value={categoria} onChange={e => setCategoria(e.target.value)} placeholder="es. Prestazioni e servizi" />
          <datalist id="cons-categorie">
            {categorie.map(c => <option key={c} value={c} />)}
          </datalist>
        </div>
      )}
      {costo && (
        <div className="form-grid" style={{ gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", marginBottom: 12 }}>
          <div className="field">
            <label className="label">Categoria / macro area</label>
            <input className="input" list="cons-categorie" value={categoria} onChange={e => setCategoria(e.target.value)} placeholder="es. Trasporti" />
            <datalist id="cons-categorie">
              {categorie.map(c => <option key={c} value={c} />)}
            </datalist>
          </div>
          <div className="field">
            <label className="label">Tipo rapporto</label>
            <select className="select" value={regime} onChange={e => setRegime(e.target.value)}>
              <option value="">— non specificato —</option>
              <option value="dipendente_spett">Dipendente (assunto)</option>
              <option value="piva">P.IVA</option>
              <option value="ritenuta">Ritenuta d'acconto</option>
              <option value="esente">Esente / altro</option>
            </select>
          </div>
        </div>
      )}
      <div style={{ display: "grid", gridTemplateColumns: costo ? "repeat(4, 1fr)" : "repeat(3, 1fr)", gap: 12 }}>
        <div className="field">
          <label className="label">Q.tà</label>
          <input className="input num" value={qta} onChange={e => setQta(e.target.value)} />
        </div>
        <div className="field">
          <label className="label">Giorni</label>
          <input className="input num" value={giorni} onChange={e => setGiorni(e.target.value)} placeholder="opz." />
        </div>
        <div className="field">
          <label className="label">{numIt5(giorni) > 0 ? "Prezzo / giorno" : "Prezzo"}</label>
          <input className="input num" value={prezzo} onChange={e => setPrezzo(e.target.value)} placeholder="0,00" />
        </div>
        {costo && (
          <div className="field">
            <label className="label">Markup %</label>
            <input className="input num" value={markup} onChange={e => setMarkup(e.target.value)} />
          </div>
        )}
      </div>
      <div className="help" style={{ textAlign: "right", marginTop: 8 }}>
        Totale riga: <strong className="mono">{fmt5(totale)}</strong>
        {costo && numIt5(markup) > 0 && <> · al cliente <strong className="mono">{fmt5(totale * (1 + numIt5(markup) / 100))}</strong></>}
      </div>
      <div className="field" style={{ marginTop: 12 }}>
        <label className="label">Nota</label>
        <input className="input" value={nota} onChange={e => setNota(e.target.value)} placeholder="promemoria interno sulla voce" />
        <span className="help">Non compare nel documento per il cliente.</span>
      </div>

      {costo && isDip5({ regime }) && (
        <div style={{ marginTop: 12, padding: 12, background: "var(--surface-2)", borderRadius: "var(--r)" }}>
          <Spunta5 checked={oneriACliente} onChange={setOneriACliente}
            testo="Ribalta gli oneri datore sul cliente"
            aiuto="Gli oneri restano comunque nel costo e nel margine: la scelta riguarda solo il prezzo al cliente." />
        </div>
      )}

      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: 12, marginTop: 12, padding: 12, background: "var(--surface-2)", borderRadius: "var(--r)" }}>
        <Spunta5 checked={visibile} onChange={setVisibile}
          testo="Visibile al cliente" aiuto="Senza spunta la voce non compare nel PDF e nell'Excel." />
        <div className="field">
          <label className="label">Tipo voce</label>
          <select className="select" value={tipoExtra} onChange={e => setTipoExtra(e.target.value)}>
            <option value="">Normale</option>
            <option value="straordinario">Straordinario (aggiunto in corso)</option>
            <option value="fuori_preventivo">Fuori preventivo (extra non concordato)</option>
          </select>
        </div>
      </div>
    </Modal5>
  );
}

/* ---------- export per voci: scelta di colonne, raggruppamento e righe ---------- */
// nomi delle macro aree dei preventivi collegati: le voci copiate restano lì
const areeNoteCons5 = (cons) => new Set(cons.progetti.flatMap(p =>
  (p.righePreventivo || []).filter(r => r.areaPreventivo).map(r => String(r.categoria).trim().toLowerCase())));

function EsportaConsuntivoModal({ cons, onClose, onMacroAree }) {
  const chiaveTesti = "antares.testiDoc.cons." + cons.id;
  const locali = leggiOpz5(chiaveTesti, { etichetta: "Consuntivo", condizioni: "" });
  const sorgente = { ...cons, tipoDoc: "Consuntivo", areeNote: areeNoteCons5(cons) };
  const salvaTesti = async (t, aggiorna) => {
    salvaOpz5(chiaveTesti, { etichetta: t.etichetta, condizioni: t.condizioni });
    const patch = { titolo: t.titolo.trim() || cons.titolo, cliente: t.cliente.trim() || null, data: t.data || cons.data, nota: t.nota.trim() || null };
    const cambiato = patch.titolo !== cons.titolo || (patch.cliente || "") !== (cons.cliente || "")
      || patch.data !== cons.data || (patch.nota || "") !== (cons.nota || "");
    if (aggiorna && cambiato) {
      const res = await window.AntaresStore.saveConsuntivo(cons.id, patch);
      if (res.error) alert("Il documento esce lo stesso, ma i dati del consuntivo non sono stati aggiornati: " + res.error.message);
      else refreshCons5();
    }
  };
  return (
    <AnteprimaDocumentoModal
      sorgente={sorgente}
      chiaveOpz="antares.exportConsuntivo2"
      testiIniziali={{
        etichetta: locali.etichetta, titolo: cons.titolo, cliente: cons.cliente || "", data: cons.data,
        nota: cons.nota || "", sottotitolo: "", condizioni: locali.condizioni,
      }}
      onSalvaTesti={salvaTesti}
      etichettaAggiornaDati="Salva oggetto, cliente, data e nota anche nel consuntivo"
      azioniMacroAree={
        <button type="button" className="btn ghost" style={{ marginTop: 6 }} onClick={onMacroAree}>
          <Icon5 name="chart" /> Disponi le macro aree
        </button>
      }
      onClose={onClose}
    />
  );
}

/* ---------- macro aree del consuntivo: proposta da sistemare (nomi, ordine, voci) ---------- */
function MacroAreeModal({ cons, onClose, onSalvata }) {
  const note = areeNoteCons5(cons);
  const [layout, setLayout] = useS5(() => CX5.riconciliaMacroAree(cons.macroAree, cons.voci, note));
  const [busy, setBusy] = useS5(false);
  const [err, setErr] = useS5("");
  const [modificato, setModificato] = useS5(false);

  const multi = cons.progetti.length > 1;
  const perId = {};
  cons.voci.forEach(v => { perId[v.id] = v; });
  const nomeProgetto = (pid) => cons.progetti.find(p => p.progettoId === pid)?.nome || pid;
  const totaleArea = (a) => a.voci.map(id => perId[id]).filter(v => v && v.visibile_cliente !== false)
    .reduce((s, v) => s + CX5.rigaCliente(v).totale, 0);

  const cambia = (fn) => { setModificato(true); setLayout(l => ({ aree: fn(l.aree.map(a => ({ ...a, voci: [...a.voci] }))) })); };
  const rinomina = (id, nome) => cambia(aree => aree.map(a => a.id === id ? { ...a, nome } : a));
  const spostaArea = (i, dir) => cambia(aree => {
    const j = i + dir;
    if (j < 0 || j >= aree.length) return aree;
    [aree[i], aree[j]] = [aree[j], aree[i]];
    return aree;
  });
  const spostaVoce = (voceId, a) => cambia(aree => spostaNeiGruppi5(aree, voceId, a));
  const nuovaArea = () => cambia(aree => [...aree, { id: "a" + Date.now().toString(36), nome: "Nuova macro area", voci: [] }]);
  const eliminaArea = (id) => cambia(aree => {
    if (aree.length <= 1) return aree;
    const area = aree.find(a => a.id === id);
    const resto = aree.filter(a => a.id !== id);
    if (area.voci.length > 0) {
      // le voci non si perdono: vanno in "Altre voci"
      let dest = resto.find(a => a.nome.trim().toLowerCase() === "altre voci");
      if (!dest) { dest = { id: "a" + Date.now().toString(36), nome: "Altre voci", voci: [] }; resto.push(dest); }
      dest.voci.push(...area.voci);
    }
    return resto;
  });
  const ripristina = () => {
    if (!confirm("Tornare alla proposta automatica? Nomi e spostamenti fatti a mano si perdono.")) return;
    setModificato(true);
    setLayout(CX5.proponiMacroAree(cons.voci, note));
  };

  const dnd = useTrascina5((da, a) => {
    if (da.tipo === "area" && a.tipo === "area") {
      const ordine = spostaInLista5(layout.aree.map(x => x.id), da.id, a);
      cambia(aree => ordine.map(id => aree.find(x => x.id === id)));
    } else if (da.tipo === "voce") {
      spostaVoce(da.id, a.tipo === "area" ? { tipo: "gruppo", id: a.id } : a);
    }
  });

  const salva = async () => {
    setBusy(true); setErr("");
    const pulito = { aree: layout.aree.map(a => ({ ...a, nome: a.nome.trim() || "Macro area" })) };
    const res = await window.AntaresStore.saveConsuntivo(cons.id, { macro_aree: pulito });
    setBusy(false);
    if (res.error) { setErr(res.error.message); return; }
    await refreshCons5();
    (onSalvata || onClose)();
  };

  return (
    <Modal5
      title="Macro aree del consuntivo"
      subtitle="Trascina le voci da un'area all'altra e le aree per riordinarle; rinomina, aggiungi o togli aree"
      onClose={onClose} width={780}
      footer={<>
        <button type="button" className="btn modal-foot-left" onClick={ripristina} disabled={busy}>Ripristina proposta</button>
        <button type="button" className="btn" onClick={onClose}>Annulla</button>
        <button type="button" className="btn primary" onClick={salva} disabled={busy || !modificato}>{busy ? "Salvo…" : "Salva"}</button>
      </>}
    >
      {err && <div className="login-error" style={{ marginBottom: 12 }}>{err}</div>}
      <div className="row" style={{ justifyContent: "space-between", marginBottom: 12 }}>
        <span className="help">{layout.aree.length} macro aree · la disposizione resta salvata nel consuntivo</span>
        <button type="button" className="btn ghost" onClick={nuovaArea}><Icon5 name="plus" size={12} /> Nuova macro area</button>
      </div>

      {layout.aree.map((a, i) => (
        <div className={"cons-area " + dnd.classe(a.id)} key={a.id} {...dnd.bersaglio("area", a.id, ["area", "voce"])}>
          <div className="cons-area-head">
            <span {...dnd.maniglia("area", a.id)}>⋮⋮</span>
            <input className="input" value={a.nome} onChange={e => rinomina(a.id, e.target.value)} aria-label="Nome della macro area" />
            <span className="mono" style={{ fontSize: 13, whiteSpace: "nowrap" }}>{fmt5(totaleArea(a))}</span>
            <button type="button" className="btn-row" onClick={() => spostaArea(i, -1)} disabled={i === 0} aria-label="Sposta su">↑</button>
            <button type="button" className="btn-row" onClick={() => spostaArea(i, 1)} disabled={i === layout.aree.length - 1} aria-label="Sposta giù">↓</button>
            <button type="button" className="btn-row danger" onClick={() => eliminaArea(a.id)} disabled={layout.aree.length <= 1}
              aria-label="Elimina macro area" title="Elimina la macro area: le sue voci vanno in Altre voci">×</button>
          </div>
          {a.voci.length === 0 ? (
            <div className="cons-area-vuota">Nessuna voce: trascinane qui dalle altre macro aree.</div>
          ) : a.voci.map(id => {
            const v = perId[id];
            if (!v) return null;
            return (
              <div className={"cons-area-voce " + dnd.classe(id)} key={id} {...dnd.bersaglio("voce", id, ["voce"])}
                style={v.visibile_cliente === false ? { opacity: 0.55 } : undefined}>
                <span style={{ minWidth: 0 }}>
                  <span {...dnd.maniglia("voce", id)}>⋮⋮</span>
                  {v.visibile_cliente === false && <span title="Non visibile al cliente">🚫 </span>}
                  {v.voce}
                  {multi && <span className="pill" style={{ marginLeft: 6 }}>{nomeProgetto(v.progetto_id)}</span>}
                  {v.tipo === "ricavo" && <span className="pill teal" style={{ marginLeft: 6 }}>ricavo</span>}
                </span>
                <span className="mono">{fmt5(CX5.rigaCliente(v).totale)}</span>
                <select className="select" value={a.id} aria-label={"Sposta " + v.voce + " in un'altra macro area"}
                  onChange={e => spostaVoce(id, { tipo: "gruppo", id: e.target.value })}>
                  {layout.aree.map(x => (
                    <option key={x.id} value={x.id}>{x.id === a.id ? "In: " : "Sposta in: "}{x.nome.trim() || "(senza nome)"}</option>
                  ))}
                </select>
              </div>
            );
          })}
        </div>
      ))}
    </Modal5>
  );
}

window.AntaresScreens5 = {
  Consuntivi, ConsuntivoDettaglio, NuovoConsuntivoModal,
  STATI_CONSUNTIVO, consuntivoDelProgetto, consuntivoPronto,
  // condivisi con la pagina Preventivi (screens-3.jsx)
  AnteprimaDocumentoModal, useTrascina5, spostaNeiGruppi5, spostaInLista5, leggiOpz5, salvaOpz5,
};
