// ============================================================
// pages/Oda.jsx — FASE 10.5 — Ordini di Acquisto (OdA / Purchase Order)
// ============================================================
// Pagina leaner (non clone di RdA.jsx): lista + KPI + filtri + detail modal con
// i workflow collegati (riusa window.WorkflowInstancesCard, entity-agnostico).
// Dati: merge [...extras.oda, ...seed.ODA] deduped (extras vince), come RdA.jsx.

const ODA_STATUS_TONE = {
  draft: 'var(--text-3)',
  issued: 'var(--brand-1)',
  confirmed: 'var(--ok)',
  delivered: 'var(--ok)',
  closed: 'var(--text-3)',
  cancelled: 'var(--err)',
};
const ODA_STATUS_LABEL = {
  draft: 'Bozza',
  issued: 'Emessa',
  confirmed: 'Confermata',
  delivered: 'Consegnata',
  closed: 'Chiusa',
  cancelled: 'Annullata',
};

function OdaStatusChip({ status }) {
  const color = ODA_STATUS_TONE[status] || 'var(--text-3)';
  return (
    <span
      className="tag"
      style={{ color, borderColor: color, background: `color-mix(in oklch, ${color} 12%, transparent)` }}
    >
      {ODA_STATUS_LABEL[status] || status}
    </span>
  );
}

function Oda() {
  const { seed, extras, routeParam, user, seedCustom } = useStore();
  const [sel, setSel] = React.useState(null);
  const [showNew, setShowNew] = React.useState(false);
  const [statusFilter, setStatusFilter] = React.useState('all');
  const [q, setQ] = React.useState('');
  const canCreateOda = typeof window !== 'undefined' && window.can ? window.can('po.create', user, seedCustom) : true;

  // Dedup per id (extras vince sul seed) — coerente con RdA.jsx.
  const allOda = React.useMemo(() => {
    const extra = extras?.oda || [];
    const extraIds = new Set(extra.map((o) => o.id));
    const seedFiltered = (seed.ODA || []).filter((o) => !extraIds.has(o.id));
    return [...extra, ...seedFiltered];
  }, [extras, seed]);

  const filtered = React.useMemo(() => {
    const needle = q.trim().toLowerCase();
    return allOda.filter((o) => {
      if (statusFilter !== 'all' && o.status !== statusFilter) return false;
      if (!needle) return true;
      return [o.id, o.project, o.vendor, o.title, o.rdaId]
        .filter(Boolean)
        .some((v) => String(v).toLowerCase().includes(needle));
    });
  }, [allOda, statusFilter, q]);

  const pg = usePaginated(filtered, 10);
  React.useEffect(() => { pg.setPage(1); }, [statusFilter, q]);

  // routeParam auto-apply: navigate('oda', <id>) apre il detail (es. dopo "Genera OdA").
  React.useEffect(() => {
    if (!routeParam) return;
    const target = allOda.find((o) => o.id === routeParam);
    if (target) setSel(target);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [routeParam]);

  const totalValue = allOda.reduce((a, b) => a + (b.amount || 0), 0);
  const nIssued = allOda.filter((o) => o.status === 'issued').length;
  const nConfirmed = allOda.filter((o) => ['confirmed', 'delivered', 'closed'].includes(o.status)).length;

  return (
    <div className="page fade-in" data-page="oda">
      <div className="page-header">
        <div>
          <div className="eyebrow">Procurement</div>
          <h1 className="page-title">Ordini di Acquisto</h1>
          <div className="page-sub">PO emessi a valle delle RdA approvate, oppure creati direttamente (testata + posizioni).</div>
        </div>
        <div className="actions">
          <Btn
            variant={canCreateOda ? 'primary' : 'ghost'}
            size="sm"
            disabled={!canCreateOda}
            title={canCreateOda ? 'Crea un nuovo Ordine di Acquisto' : window.whyDisabled('po.create')}
            onClick={() => { if (canCreateOda) setShowNew(true); }}
            data-testid="oda-new-btn"
          ><Icon name="plus" size={12} /> Nuovo OdA</Btn>
        </div>
      </div>

      <div className="grid grid-4" style={{ marginBottom: 14 }}>
        <div className="card"><Stat label="OdA totali" value={allOda.length} /></div>
        <div className="card"><Stat label="Emesse" value={nIssued} delta="in lavorazione" /></div>
        <div className="card"><Stat label="Confermate / chiuse" value={nConfirmed} tone="up" /></div>
        <div className="card"><Stat label="Valore totale" value={fmtEUR(totalValue, true)} /></div>
      </div>

      <div className="card">
        <div className="row" style={{ gap: 10, alignItems: 'center', marginBottom: 12, flexWrap: 'wrap' }}>
          <div className="row" style={{ gap: 6, padding: '4px 10px', border: '1px solid var(--line)', borderRadius: 6, background: 'var(--bg-2)', minWidth: 240 }}>
            <Icon name="search" size={12} />
            <input
              placeholder="Cerca per id, progetto, vendor, RdA…"
              value={q}
              onChange={(e) => setQ(e.target.value)}
              data-testid="oda-search"
              style={{ background: 'transparent', border: 'none', outline: 'none', flex: 1, fontSize: 12 }}
            />
          </div>
          <select
            value={statusFilter}
            onChange={(e) => setStatusFilter(e.target.value)}
            data-testid="oda-status-filter"
            style={{ background: 'var(--bg-2)', border: '1px solid var(--line)', color: 'var(--text-0)', padding: '5px 8px', borderRadius: 6, fontSize: 11.5 }}
          >
            <option value="all">Tutti gli stati</option>
            {Object.keys(ODA_STATUS_LABEL).map((s) => <option key={s} value={s}>{ODA_STATUS_LABEL[s]}</option>)}
          </select>
          <span className="spacer" />
          <span style={{ color: 'var(--text-3)', fontSize: 12 }}>{filtered.length} risultati</span>
        </div>

        {filtered.length === 0 ? (
          <div className="empty" style={{ padding: 40, textAlign: 'center', color: 'var(--text-3)' }}>
            <Icon name="rda" size={28} />
            <div style={{ marginTop: 8 }}>Nessun OdA. Genera il primo da una RdA approvata (RdA → dettaglio → "Genera OdA").</div>
          </div>
        ) : (
          <>
            <table className="tbl" data-testid="oda-table">
              <thead>
                <tr>
                  <th>OdA</th><th>Progetto</th><th>Vendor</th><th className="num">Importo</th>
                  <th>Stato</th><th>RdA origine</th><th>Creato</th>
                </tr>
              </thead>
              <tbody>
                {pg.slice.map((o) => (
                  <tr key={o.id} className="clickable" onClick={() => setSel(o)} data-oda-id={o.id}>
                    <td className="mono" style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-1)' }}>{o.id}</td>
                    <td className="mono" style={{ fontSize: 11 }}>{o.project || '—'}</td>
                    <td>{o.vendor || '—'}</td>
                    <td className="num">{fmtEUR(o.amount, true)}</td>
                    <td><OdaStatusChip status={o.status} /></td>
                    <td className="mono" style={{ fontSize: 11 }}>{o.rdaId || '—'}</td>
                    <td className="mono" style={{ fontSize: 11, color: 'var(--text-2)' }}>{fmtDate(o.created)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
            <Pagination {...pg} />
          </>
        )}
      </div>

      {sel && <OdaDetailModal oda={sel} onClose={() => setSel(null)} />}
      {showNew && (
        <NewOdaModal
          onClose={() => setShowNew(false)}
          onCreated={(created) => { setShowNew(false); setSel(created); }}
        />
      )}
    </div>
  );
}

// Stati OdA oltre i quali le righe sono congelate (allineato al BE).
const ODA_FROZEN_STATUSES = ['confirmed', 'delivered', 'closed', 'cancelled'];

// Formattatore importi a 2 decimali (le posizioni hanno cent; la testata euro interi).
function eur2(n) {
  const v = Number(n || 0);
  return '€ ' + v.toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}

// ============================================================
// DocViewerPane — anteprima inline di un documento allegato.
// Il download endpoint serve `Content-Disposition: attachment` (→ l'iframe non
// renderizza) e l'iframe non manda l'header attore: quindi scarichiamo il file
// via fetch (con X-Actor-Persona-Id) e lo mostriamo da un blob URL (inline).
// ============================================================
function DocViewerPane({ docId, mimeType, filename, user }) {
  const [blobUrl, setBlobUrl] = React.useState(null);
  const [state, setState] = React.useState('loading'); // loading | ready | error

  React.useEffect(() => {
    let cancelled = false;
    let url = null;
    setState('loading');
    setBlobUrl(null);
    (async () => {
      try {
        const r = await fetch(`/api/documents/${encodeURIComponent(docId)}/download`, {
          credentials: 'same-origin',
          cache: 'no-store',
          headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
        });
        if (!r.ok) { if (!cancelled) setState('error'); return; }
        const blob = await r.blob();
        url = URL.createObjectURL(blob);
        if (cancelled) { URL.revokeObjectURL(url); return; }
        setBlobUrl(url);
        setState('ready');
      } catch {
        if (!cancelled) setState('error');
      }
    })();
    return () => { cancelled = true; if (url) URL.revokeObjectURL(url); };
  }, [docId, user?.id]);

  const isPdf = /pdf/i.test(mimeType || '') || /\.pdf$/i.test(filename || '');
  const isImage = /^image\//i.test(mimeType || '');

  if (state === 'loading') {
    return <div style={{ margin: 'auto', color: 'var(--text-3)', fontSize: 12 }}>Carico anteprima…</div>;
  }
  if (state === 'error' || !blobUrl) {
    return (
      <div style={{ margin: 'auto', textAlign: 'center', color: 'var(--text-3)', padding: 24 }}>
        <Icon name="file_pdf" size={28} />
        <div style={{ marginTop: 8, fontSize: 12 }}>Anteprima non disponibile.</div>
        <div style={{ marginTop: 6 }}>
          <a href={`/api/documents/${encodeURIComponent(docId)}/download`} target="_blank" rel="noreferrer" className="btn ghost sm">Scarica documento</a>
        </div>
      </div>
    );
  }
  if (isPdf) {
    return <iframe src={blobUrl} title={filename || 'documento'} style={{ width: '100%', height: '100%', border: 0 }} />;
  }
  if (isImage) {
    return (
      <div style={{ width: '100%', height: '100%', overflow: 'auto', textAlign: 'center' }}>
        <img src={blobUrl} alt={filename || 'documento'} style={{ maxWidth: '100%' }} />
      </div>
    );
  }
  return (
    <div style={{ margin: 'auto', textAlign: 'center', color: 'var(--text-3)', padding: 24 }}>
      <Icon name="file_pdf" size={28} />
      <div style={{ marginTop: 8, fontSize: 12 }}>Anteprima non disponibile per questo formato.</div>
      <div style={{ marginTop: 6 }}>
        <a href={blobUrl} download={filename || 'documento'} className="btn ghost sm">Scarica documento</a>
      </div>
      <div style={{ marginTop: 6, fontSize: 10.5 }}>L'estrazione AI funziona meglio su PDF e immagini.</div>
    </div>
  );
}

// s127 — ODA_ANALYZER: analisi AI del singolo OdA. Riusa AiInsightPanel
// (/api/ai/invoke) con contesto ricco (testata SAP + posizioni + RdA origine).
// Output markdown → AiMarkdown (tabelle + grafici).
const ODA_ANALYZER_SYSTEM = "Sei ODA_ANALYZER, analista procurement CAPEX di una piattaforma di governance degli investimenti industriali. Analizzi UN Ordine di Acquisto (OdA / Purchase Order): testata SAP (vendor, incoterms, termini, date), posizioni (righe EKPO) e conformità rispetto alla RdA di origine. Rispondi in italiano, markdown ricco, conciso, SOLO sui dati forniti (non inventare). Struttura: (1) **Giudizio** in 1-2 frasi con livello di rischio (basso/medio/alto). (2) Se ci sono almeno 2 posizioni, una TABELLA markdown delle righe (Riga, Descrizione, Qtà, Prezzo €, Netto €, numeri allineati a destra con ---:) e, se utile, un GRAFICO a barre del netto per riga come blocco ```chart con JSON {\"type\":\"bar\",\"title\":\"Netto per riga\",\"data\":[{\"label\":\"riga 10\",\"value\":1234}]} (valori numerici puri). (3) **Conformità vs RdA origine**: confronta vendor e importo, segnala scostamenti; se la RdA d'origine non è fornita, indicalo. (4) **Rischi consegna/termini**: incoterms, data consegna attesa, termini pagamento. (5) **Azioni consigliate**: lista puntata. Niente preamboli.";

function buildOdaAnalysisPrompt(oda, positions, rdaOrigin) {
  const L = [];
  const pos = Array.isArray(positions) ? positions : [];
  const sumNet = pos.reduce((a, p) => a + Number(p.netValueEur || 0), 0);
  L.push('ORDINE DI ACQUISTO (OdA) da analizzare:');
  L.push(`- Codice: ${oda.id}`);
  L.push(`- Oggetto: ${oda.title || 'n/d'}`);
  L.push(`- Progetto: ${oda.project || 'n/d'}`);
  L.push(`- Vendor: ${oda.vendor || 'n/d'}${oda.vendorCode ? ' (' + oda.vendorCode + ')' : ''}`);
  L.push(`- Tipo documento (SAP BSART): ${oda.docType || 'n/d'}`);
  L.push(`- Incoterms: ${oda.incoterms || 'n/d'}${oda.incotermsLocation ? ' ' + oda.incotermsLocation : ''}`);
  L.push(`- Importo testata: ${oda.amount ?? oda.amountEur ?? 0} € · netto posizioni: ${Math.round(sumNet)} €`);
  L.push(`- Stato: ${oda.status} · Data ordine: ${oda.orderDate || 'n/d'} · Consegna attesa: ${oda.expectedDelivery || 'n/d'}`);
  L.push(`- Termini pagamento: ${oda.paymentTerms || 'n/d'}`);
  if (pos.length) {
    L.push('', `POSIZIONI (${pos.length}):`);
    pos.forEach((p) => L.push(`- riga ${p.lineNo}: ${p.description} | qty ${p.quantity} ${p.uom} | prezzo ${p.unitPriceEur} € | netto ${p.netValueEur} €${p.materialCode ? ' | mat ' + p.materialCode : ''}`));
  } else {
    L.push('', 'POSIZIONI: nessuna riga.');
  }
  if (oda.rdaId && rdaOrigin) {
    L.push('', 'RdA DI ORIGINE:');
    L.push(`- Codice: ${rdaOrigin.id} · Stato: ${rdaOrigin.status}`);
    L.push(`- Vendor: ${rdaOrigin.vendor || 'n/d'}${rdaOrigin.vendorCode ? ' (' + rdaOrigin.vendorCode + ')' : ''}`);
    L.push(`- Importo RdA: ${rdaOrigin.amount ?? 0} €`);
    L.push(`- Completezza checklist RdA: ${Math.round((rdaOrigin.completeness || 0) * 100)}%`);
  } else if (oda.rdaId) {
    L.push('', `RdA DI ORIGINE: ${oda.rdaId} (dettaglio non disponibile nel contesto).`);
  } else {
    L.push('', 'RdA DI ORIGINE: nessuna (OdA diretta).');
  }
  return L.join('\n');
}

function OdaAiAnalysis({ oda, positions, allRda }) {
  const rdaOrigin = oda.rdaId && Array.isArray(allRda) ? allRda.find((r) => r.id === oda.rdaId) || null : null;
  if (positions === null) return <div style={{ fontSize: 11, color: 'var(--text-3)', padding: '4px 0' }}>Preparo il contesto…</div>;
  return (
    <AiInsightPanel
      system={ODA_ANALYZER_SYSTEM}
      prompt={buildOdaAnalysisPrompt(oda, positions, rdaOrigin)}
      idleLabel="Analizza OdA con AI"
      emptyHint="Valuta posizioni, conformità vs RdA e rischi consegna, poi propone azioni."
      runKey={oda.id}
    />
  );
}

function OdaDetailModal({ oda, onClose }) {
  const { navigate, user, seedCustom, pushToast, seed, extras } = useStore();
  const WfCard = (typeof window !== 'undefined' && window.WorkflowInstancesCard) || null;
  // Stato locale: testata fresca (con totali ricalcolati) + posizioni dal BE.
  const [detail, setDetail] = React.useState(oda); // fallback alla riga della lista
  const [positions, setPositions] = React.useState(null); // null = loading
  const [err, setErr] = React.useState(null);
  // Viewer documento allegato (laterale) + verifica AI.
  const [docOpen, setDocOpen] = React.useState(false);
  const [docMeta, setDocMeta] = React.useState(null); // { id, mimeType, filename }
  const [docLoading, setDocLoading] = React.useState(false);
  const [aiResult, setAiResult] = React.useState(null); // { values, provider }
  const [extracting, setExtracting] = React.useState(false);
  // s126 — edit testata OdA nel dettaglio (gated po.edit + freeze stati terminali).
  const [editingHdr, setEditingHdr] = React.useState(false);
  const [hdr, setHdr] = React.useState(null);
  const [hdrSaving, setHdrSaving] = React.useState(false);

  const reload = React.useCallback(async () => {
    if (!oda?.id) return;
    setErr(null);
    try {
      const r = await fetch(`/api/oda/${encodeURIComponent(oda.id)}`, {
        credentials: 'same-origin', cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok || !j.data) { setErr(j?.error || `HTTP ${r.status}`); setPositions([]); return; }
      setDetail(j.data);
      setPositions(Array.isArray(j.data.positions) ? j.data.positions : []);
    } catch (e) {
      setErr(String(e?.message || e)); setPositions([]);
    }
  }, [oda?.id, user?.id]);

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

  const d = detail || oda;
  const hasDoc = !!(d && d.sourceDocumentId);

  const toggleDoc = async () => {
    if (docOpen) { setDocOpen(false); return; }
    if (!hasDoc) return;
    if (!docMeta) {
      setDocLoading(true);
      try {
        const r = await fetch(`/api/documents/${encodeURIComponent(d.sourceDocumentId)}`, {
          credentials: 'same-origin', cache: 'no-store',
          headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
        });
        const j = await r.json().catch(() => ({}));
        if (!r.ok || !j.data) { pushToast({ title: 'Documento non disponibile', desc: j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
        setDocMeta({ id: d.sourceDocumentId, mimeType: j.data.mimeType || '', filename: j.data.title || j.data.originalFilename || 'documento' });
      } catch (e) {
        pushToast({ title: 'Errore caricamento documento', desc: String(e?.message || e), tone: 'err' }); return;
      } finally { setDocLoading(false); }
    }
    setDocOpen(true);
  };

  const extractVerify = async () => {
    if (!hasDoc || extracting) return;
    setExtracting(true);
    try {
      const r = await fetch('/api/ai/extract', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify({ docId: d.sourceDocumentId, schema: ODA_EXTRACT_SCHEMA, instruction: ODA_EXTRACT_INSTRUCTION }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok || !j.data) { pushToast({ title: 'Estrazione fallita', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      setAiResult({ values: j.data.values || {}, provider: j.data.provider });
      pushToast({ title: 'Letto dall\'AI', desc: `${j.data.provider || 'AI'} · confronta con i dati a sinistra`, tone: 'ok' });
    } catch (e) {
      pushToast({ title: 'Errore di rete', desc: String(e?.message || e), tone: 'err' });
    } finally { setExtracting(false); }
  };

  if (!oda) return null;
  const hasPositions = (positions?.length || 0) > 0;
  const headerAmount = hasPositions ? (d.netTotal ?? d.amount) : d.amount;
  const canEditOda = window.can('po.edit', user, seedCustom);
  const odaFrozen = ODA_FROZEN_STATUSES.includes(d?.status);
  const setH = (k, v) => setHdr((s) => ({ ...s, [k]: v }));
  const startEditHdr = () => {
    setHdr({
      title: d.title || '', vendor: d.vendor || '', vendorCode: d.vendorCode || '',
      currency: d.currency || 'EUR', paymentTerms: d.paymentTerms || '', docType: d.docType || '',
      orderDate: d.orderDate || '', expectedDelivery: d.expectedDelivery || '',
      incoterms: d.incoterms || '', incotermsLocation: d.incotermsLocation || '',
      amount: String(d.amount ?? ''),
    });
    setEditingHdr(true);
  };
  const saveHdr = async () => {
    if (hdrSaving || !hdr) return;
    setHdrSaving(true);
    try {
      const body = {
        title: hdr.title.trim(), vendor: hdr.vendor.trim(), vendorCode: hdr.vendorCode.trim() || null,
        currency: hdr.currency.trim() || 'EUR', paymentTerms: hdr.paymentTerms.trim() || null,
        docType: hdr.docType.trim() || null, orderDate: hdr.orderDate || null,
        expectedDelivery: hdr.expectedDelivery || null, incoterms: hdr.incoterms.trim() || null,
        incotermsLocation: hdr.incotermsLocation.trim() || null,
      };
      if (!hasPositions) body.amount = Number(hdr.amount) || 0;
      const r = await fetch(`/api/oda/${encodeURIComponent(d.id)}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { pushToast({ title: 'Modifica fallita', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      setEditingHdr(false); await reload();
      pushToast({ title: 'OdA aggiornato', desc: 'Testata salvata. Audit log registrato.', tone: 'ok' });
    } catch (e) { pushToast({ title: 'Errore di rete', desc: String(e?.message || e), tone: 'err' }); }
    finally { setHdrSaving(false); }
  };
  // RdA origine: attiva il tasto SOLO se la RdA esiste davvero (stesso elenco che
  // usa la pagina RdA per l'auto-select); altrimenti grigio/disattivato.
  const allRda = [...(extras?.rda || []), ...(seed?.RDA || [])];
  const rdaExists = !!(d.rdaId && allRda.some((r) => r.id === d.rdaId));
  const downloadUrl = docMeta ? `/api/documents/${encodeURIComponent(docMeta.id)}/download` : null;
  const isPdf = docMeta && /pdf/i.test(docMeta.mimeType || '');
  const isImage = docMeta && /^image\//i.test(docMeta.mimeType || '');

  // Righe di confronto per il pannello "letto dall'AI".
  const aiRows = aiResult ? [
    ['Vendor', aiResult.values.vendor, aiResult.values.vendor_confidence],
    ['Cod. vendor', aiResult.values.vendorCode, aiResult.values.vendorCode_confidence],
    ['Tipo doc', aiResult.values.docType, aiResult.values.docType_confidence],
    ['Data ordine', aiResult.values.orderDate, aiResult.values.orderDate_confidence],
    ['Consegna', aiResult.values.expectedDelivery, aiResult.values.expectedDelivery_confidence],
    ['Termini pag.', aiResult.values.paymentTerms, aiResult.values.paymentTerms_confidence],
    ['Incoterms', aiResult.values.incoterms, aiResult.values.incoterms_confidence],
    ['Totale netto', aiResult.values.netTotal, aiResult.values.netTotal_confidence],
  ] : [];

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          display: 'flex', background: 'var(--bg-1)', border: '1px solid var(--line)',
          borderRadius: 10, maxHeight: '92vh', overflow: 'hidden',
          width: docOpen ? 'min(1400px, 97vw)' : 'min(940px, 95vw)',
        }}
        data-testid="oda-detail-modal"
      >
        {/* ── Pannello DETTAGLIO ── */}
        <div style={{ width: docOpen ? 600 : '100%', flexShrink: 0, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
          <div className="modal-header" style={{ padding: '12px 16px', borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', gap: 8 }}>
            <div className="title" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{d.id} · {d.title}</div>
            {hasDoc && (
              <Btn variant={docOpen ? 'primary' : 'ghost'} size="sm" onClick={toggleDoc} disabled={docLoading} data-testid="oda-doc-toggle">
                <Icon name={docOpen ? 'x' : 'docs'} size={12} /> {docLoading ? 'Apro…' : (docOpen ? 'Chiudi documento' : 'Apri documento')}
              </Btn>
            )}
            <button className="btn ghost icon" onClick={onClose}><Icon name="x" /></button>
          </div>
          <div style={{ overflow: 'auto', padding: 16, flex: 1 }}>
            <div className="col" style={{ gap: 14 }}>
              {!hasDoc && (
                <div style={{ fontSize: 11, color: 'var(--text-3)', background: 'var(--bg-2)', border: '1px dashed var(--line)', borderRadius: 6, padding: '6px 10px' }}>
                  <Icon name="info" size={11} /> Nessun documento allegato a questo OdA.
                </div>
              )}
              {!editingHdr ? (
                <div className="col" style={{ gap: 14 }}>
                  <div className="grid grid-3">
                    <div><div className="eyebrow">Progetto</div><div style={{ fontWeight: 500 }}>{d.project || '—'}</div></div>
                    <div><div className="eyebrow">Vendor</div><div style={{ fontWeight: 500 }}>{d.vendor || '—'}{d.vendorCode ? <span className="mono" style={{ fontSize: 10, color: 'var(--text-3)', marginLeft: 6 }}>{d.vendorCode}</span> : null}</div></div>
                    <div>
                      <div className="eyebrow">Totale {hasPositions ? '(da righe)' : ''}</div>
                      <div style={{ fontFamily: 'var(--font-display)', fontSize: 22 }}>{fmtEUR(headerAmount, true)} {d.currency && d.currency !== 'EUR' ? d.currency : ''}</div>
                    </div>
                  </div>
                  <div className="grid grid-3">
                    <div><div className="eyebrow">Stato</div><OdaStatusChip status={d.status} /></div>
                    <div><div className="eyebrow">RdA origine</div><div>{d.rdaId || '—'}</div></div>
                    <div><div className="eyebrow">Termini pagamento</div><div>{d.paymentTerms || '—'}</div></div>
                  </div>
                  <div className="grid grid-3">
                    <div><div className="eyebrow">Data ordine</div><div>{d.orderDate || '—'}</div></div>
                    <div><div className="eyebrow">Consegna prevista</div><div>{d.expectedDelivery || '—'}</div></div>
                    <div><div className="eyebrow">Incoterms</div><div>{d.incoterms ? `${d.incoterms}${d.incotermsLocation ? ' · ' + d.incotermsLocation : ''}` : '—'}</div></div>
                  </div>
                  {d.notes && (
                    <div><div className="eyebrow">Note</div><div style={{ color: 'var(--text-2)' }}>{d.notes}</div></div>
                  )}
                  {canEditOda && (
                    <div className="row" style={{ justifyContent: 'flex-end' }}>
                      <Btn variant="ghost" size="xs" onClick={startEditHdr} disabled={odaFrozen} title={odaFrozen ? `OdA in stato "${d.status}" non modificabile` : 'Modifica i dati di testata'}>
                        <Icon name="edit" size={11}/> Modifica testata
                      </Btn>
                    </div>
                  )}
                </div>
              ) : (
                <div className="card flush" style={{ border: '1px solid var(--accent)', borderRadius: 10, padding: 12 }}>
                  <div className="eyebrow" style={{ marginBottom: 8 }}>Modifica testata OdA</div>
                  <div className="grid grid-2" style={{ gap: 10 }}>
                    <div className="field" style={{ gridColumn: 'span 2' }}><label>Oggetto</label><input value={hdr.title} onChange={(e) => setH('title', e.target.value)} /></div>
                    <div className="field"><label>Vendor</label><input value={hdr.vendor} onChange={(e) => setH('vendor', e.target.value)} /></div>
                    <div className="field"><label>Codice vendor</label><input className="mono" value={hdr.vendorCode} onChange={(e) => setH('vendorCode', e.target.value)} /></div>
                    <div className="field"><label>Tipo doc</label><input value={hdr.docType} onChange={(e) => setH('docType', e.target.value)} /></div>
                    <div className="field"><label>Termini pagamento</label><input value={hdr.paymentTerms} onChange={(e) => setH('paymentTerms', e.target.value)} /></div>
                    <div className="field"><label>Data ordine</label><input type="date" value={hdr.orderDate} onChange={(e) => setH('orderDate', e.target.value)} /></div>
                    <div className="field"><label>Consegna prevista</label><input type="date" value={hdr.expectedDelivery} onChange={(e) => setH('expectedDelivery', e.target.value)} /></div>
                    <div className="field"><label>Incoterms</label><input value={hdr.incoterms} onChange={(e) => setH('incoterms', e.target.value)} /></div>
                    <div className="field"><label>Luogo resa</label><input value={hdr.incotermsLocation} onChange={(e) => setH('incotermsLocation', e.target.value)} /></div>
                    <div className="field"><label>Valuta</label><input value={hdr.currency} onChange={(e) => setH('currency', e.target.value)} /></div>
                    <div className="field"><label>Importo €{hasPositions ? ' (da righe)' : ''}</label><input type="number" value={hasPositions ? String(headerAmount) : hdr.amount} onChange={(e) => setH('amount', e.target.value)} disabled={hasPositions} /></div>
                  </div>
                  <div className="row" style={{ justifyContent: 'flex-end', gap: 8, marginTop: 10 }}>
                    <Btn variant="ghost" size="sm" onClick={() => setEditingHdr(false)} disabled={hdrSaving}>Annulla</Btn>
                    <Btn variant="primary" size="sm" onClick={saveHdr} disabled={hdrSaving || !hdr.vendor.trim() || !hdr.title.trim()}>{hdrSaving ? 'Salvataggio…' : 'Salva testata'}</Btn>
                  </div>
                </div>
              )}

              <OdaPositionsSection
                oda={d}
                positions={positions}
                err={err}
                user={user}
                seedCustom={seedCustom}
                pushToast={pushToast}
                onChanged={reload}
              />

              <div className="card flush" style={{ border: '1px solid var(--line)', borderRadius: 10 }}>
                <div className="card-header"><div className="title"><Icon name="sparkle" size={12}/> Analisi AI dell'OdA</div></div>
                <div className="card-body">
                  <OdaAiAnalysis oda={d} positions={positions} allRda={allRda} />
                </div>
              </div>

              {WfCard && <WfCard entityType="oda" entityId={d.id} />}
            </div>
          </div>
          <div className="modal-footer" style={{ padding: '10px 16px', borderTop: '1px solid var(--line)', display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
            <Btn
              variant="ghost" size="sm"
              disabled={!rdaExists}
              onClick={() => { if (rdaExists) navigate('rda', d.rdaId); }}
              title={rdaExists ? `Vai alla RdA di origine (${d.rdaId})` : (d.rdaId ? `RdA d'origine non disponibile (${d.rdaId})` : 'Nessuna RdA d\'origine')}
              data-testid="oda-rda-origin"
            >
              <Icon name="rda" size={12} /> RdA origine
            </Btn>
            <Btn variant="ghost" size="sm" onClick={onClose}>Chiudi</Btn>
          </div>
        </div>

        {/* ── Pannello VIEWER documento (laterale, toggle) ── */}
        {docOpen && docMeta && (
          <div style={{ flex: 1, borderLeft: '1px solid var(--line)', display: 'flex', flexDirection: 'column', minWidth: 0 }}>
            <div className="row" style={{ gap: 8, alignItems: 'center', padding: '10px 12px', borderBottom: '1px solid var(--line)' }}>
              <Icon name="file_pdf" size={13} />
              <span style={{ fontSize: 12, fontWeight: 600, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{docMeta.filename}</span>
              <Btn variant="ai" size="sm" onClick={extractVerify} disabled={extracting} data-testid="oda-detail-extract">
                <Icon name="sparkle" size={12} /> {extracting ? 'Leggo…' : 'Estrai con AI (verifica)'}
              </Btn>
            </div>
            {aiResult && (
              <div style={{ padding: '8px 12px', borderBottom: '1px solid var(--line)', background: 'var(--bg-2)', maxHeight: 160, overflow: 'auto' }}>
                <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginBottom: 4 }}>
                  Letto dall'AI ({aiResult.provider}) — confronta con i dati a sinistra:
                </div>
                <div className="grid grid-2" style={{ gap: '2px 12px' }}>
                  {aiRows.map(([label, val, c]) => (
                    <div key={label} className="row" style={{ gap: 6, alignItems: 'center', fontSize: 11 }}>
                      <span style={{ color: 'var(--text-3)', minWidth: 78 }}>{label}</span>
                      <span style={{ fontWeight: 600, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{val == null || val === '' ? '—' : String(val)}</span>
                      <ConfBadge c={c} />
                    </div>
                  ))}
                </div>
              </div>
            )}
            <div style={{ flex: 1, background: 'var(--bg-2)', minHeight: 420, display: 'flex' }}>
              <DocViewerPane docId={docMeta.id} mimeType={docMeta.mimeType} filename={docMeta.filename} user={user} />
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// ============================================================
// Sezione Posizioni (righe d'ordine, SAP EKPO) — tabella + CRUD + riconciliazione
// ============================================================
function OdaPositionsSection({ oda, positions, err, user, seedCustom, pushToast, onChanged }) {
  const [form, setForm] = React.useState(null); // {mode:'create'|'edit', pos?}
  const frozen = ODA_FROZEN_STATUSES.includes(oda.status);
  const canEditPerm = typeof window !== 'undefined' && window.can ? window.can('po.edit', user, seedCustom) : true;
  const canEdit = canEditPerm && !frozen;
  const whyDisabled = frozen
    ? `OdA in stato "${oda.status}": righe congelate`
    : (typeof window !== 'undefined' && window.whyDisabled ? window.whyDisabled('po.edit') : 'Permesso mancante');

  const sumNet = (positions || []).reduce((a, p) => a + Number(p.netValueEur || 0), 0);
  const reconciled = Math.abs(Math.round(sumNet) - (oda.netTotal ?? 0)) < 1;

  const removePosition = async (pos) => {
    if (!canEdit) return;
    if (!window.confirm(`Eliminare la riga ${pos.lineNo} — ${pos.description}?`)) return;
    try {
      const r = await fetch(`/api/oda/${encodeURIComponent(oda.id)}/positions/${encodeURIComponent(pos.id)}`, {
        method: 'DELETE', credentials: 'same-origin',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { pushToast({ title: 'Eliminazione fallita', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      pushToast({ title: 'Riga eliminata', tone: 'ok' });
      onChanged?.();
    } catch (e) {
      pushToast({ title: 'Errore di rete', desc: String(e?.message || e), tone: 'err' });
    }
  };

  return (
    <div style={{ borderTop: '1px solid var(--line)', paddingTop: 12 }}>
      <div className="row" style={{ alignItems: 'center', marginBottom: 8 }}>
        <div className="eyebrow" style={{ flex: 1 }}>Posizioni {positions ? `(${positions.length})` : ''}</div>
        <Btn
          variant={canEdit ? 'primary' : 'ghost'} size="xs"
          disabled={!canEdit}
          title={canEdit ? 'Aggiungi una riga d\'ordine' : whyDisabled}
          onClick={() => { if (canEdit) setForm({ mode: 'create' }); }}
          data-testid="oda-pos-add"
        ><Icon name="plus" size={11} /> Aggiungi riga</Btn>
      </div>

      {err && <div style={{ color: 'var(--err)', fontSize: 11.5, marginBottom: 8 }}>Errore: {err}</div>}
      {positions === null ? (
        <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Caricamento posizioni…</div>
      ) : positions.length === 0 ? (
        <div style={{ fontSize: 11.5, color: 'var(--text-3)', padding: '8px 0' }}>
          Nessuna posizione. {canEdit ? 'Aggiungi la prima riga d\'ordine.' : ''}
        </div>
      ) : (
        <>
          <table className="tbl" data-testid="oda-pos-table" style={{ fontSize: 11.5 }}>
            <thead>
              <tr>
                <th className="num">#</th><th>Materiale</th><th>Descrizione</th>
                <th className="num">Q.tà</th><th>UM</th><th className="num">Prezzo unit.</th>
                <th className="num">Valore netto</th><th>WBS / CdC</th><th>Consegna</th>
                {canEdit && <th></th>}
              </tr>
            </thead>
            <tbody>
              {positions.map((p) => (
                <tr key={p.id} data-pos-id={p.id}>
                  <td className="num mono">{p.lineNo}</td>
                  <td className="mono" style={{ fontSize: 10.5 }}>{p.materialCode || '—'}</td>
                  <td>{p.description}</td>
                  <td className="num">{Number(p.quantity).toLocaleString('it-IT', { maximumFractionDigits: 3 })}</td>
                  <td>{p.uom}</td>
                  <td className="num">{eur2(p.unitPriceEur)}{p.priceUnit > 1 ? <span style={{ color: 'var(--text-3)', fontSize: 10 }}> /{p.priceUnit}</span> : null}</td>
                  <td className="num" style={{ fontWeight: 600 }}>{eur2(p.netValueEur)}</td>
                  <td className="mono" style={{ fontSize: 10 }}>{p.wbsElement || p.costCenter || '—'}</td>
                  <td className="mono" style={{ fontSize: 10.5 }}>{p.deliveryDate || '—'}</td>
                  {canEdit && (
                    <td style={{ whiteSpace: 'nowrap', textAlign: 'right' }}>
                      <button className="btn ghost icon" title="Modifica riga" onClick={() => setForm({ mode: 'edit', pos: p })} data-testid={`oda-pos-edit-${p.lineNo}`}><Icon name="edit" size={12} /></button>
                      <button className="btn ghost icon" title="Elimina riga" onClick={() => removePosition(p)} data-testid={`oda-pos-del-${p.lineNo}`}><Icon name="trash" size={12} /></button>
                    </td>
                  )}
                </tr>
              ))}
            </tbody>
            <tfoot>
              <tr style={{ borderTop: '2px solid var(--line)' }}>
                <td colSpan={6} className="num" style={{ fontWeight: 600, color: 'var(--text-2)' }}>Totale netto righe</td>
                <td className="num" style={{ fontWeight: 700 }}>{eur2(sumNet)}</td>
                <td colSpan={canEdit ? 3 : 2}>
                  {reconciled
                    ? <Chip kind="ok" dot>testata = somma righe</Chip>
                    : <Chip kind="warn" dot>testata {fmtEUR(oda.netTotal, true)} ≠ righe</Chip>}
                </td>
              </tr>
            </tfoot>
          </table>
        </>
      )}

      {frozen && positions && positions.length > 0 && (
        <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 6 }}>
          <Icon name="info" size={10} /> OdA in stato "{ODA_STATUS_LABEL[oda.status] || oda.status}": le righe sono congelate (sola lettura).
        </div>
      )}

      {form && (
        <OdaPositionForm
          oda={oda}
          mode={form.mode}
          pos={form.pos}
          user={user}
          pushToast={pushToast}
          onClose={() => setForm(null)}
          onSaved={() => { setForm(null); onChanged?.(); }}
        />
      )}
    </div>
  );
}

// ============================================================
// Form posizione (create/edit) — campi SAP-aligned, anteprima valore netto
// ============================================================
function OdaPositionForm({ oda, mode, pos, user, pushToast, onClose, onSaved, onSubmitLocal }) {
  const init = mode === 'edit' && pos ? {
    description: pos.description || '', materialCode: pos.materialCode || '',
    quantity: String(pos.quantity ?? ''), uom: pos.uom || 'PC',
    unitPriceEur: String(pos.unitPriceEur ?? ''), priceUnit: String(pos.priceUnit ?? 1),
    deliveryDate: pos.deliveryDate || '', wbsElement: pos.wbsElement || '',
    costCenter: pos.costCenter || '', glAccount: pos.glAccount || '',
    plantCode: pos.plantCode || '', taxCode: pos.taxCode || '',
    accountAssignmentCat: pos.accountAssignmentCat || '', notes: pos.notes || '',
  } : {
    description: '', materialCode: '', quantity: '1', uom: 'PC', unitPriceEur: '',
    priceUnit: '1', deliveryDate: '', wbsElement: '', costCenter: '', glAccount: '',
    plantCode: '', taxCode: '', accountAssignmentCat: '', notes: '',
  };
  const [f, setF] = React.useState(init);
  const [saving, setSaving] = React.useState(false);
  const set = (k, v) => setF((s) => ({ ...s, [k]: v }));

  const qty = parseFloat(String(f.quantity).replace(',', '.')) || 0;
  const unit = parseFloat(String(f.unitPriceEur).replace(',', '.')) || 0;
  const pu = parseInt(f.priceUnit, 10) > 0 ? parseInt(f.priceUnit, 10) : 1;
  const netPreview = Math.round((qty * unit / pu) * 100) / 100;

  const submit = async () => {
    if (!f.description.trim()) { pushToast({ title: 'Descrizione obbligatoria', tone: 'err' }); return; }
    const body = {
      description: f.description.trim(),
      materialCode: f.materialCode.trim() || null,
      quantity: qty,
      uom: f.uom.trim() || 'PC',
      unitPriceEur: unit,
      priceUnit: pu,
      deliveryDate: f.deliveryDate || null,
      wbsElement: f.wbsElement.trim() || null,
      costCenter: f.costCenter.trim() || null,
      glAccount: f.glAccount.trim() || null,
      plantCode: f.plantCode.trim() || null,
      taxCode: f.taxCode.trim() || null,
      accountAssignmentCat: f.accountAssignmentCat.trim() || null,
      notes: f.notes.trim() || null,
    };
    // Modalità LOCALE (creazione OdA): ritorna la riga in memoria, niente API.
    if (onSubmitLocal) {
      onSubmitLocal({ ...body, netValueEur: netPreview });
      return;
    }
    const url = mode === 'edit'
      ? `/api/oda/${encodeURIComponent(oda.id)}/positions/${encodeURIComponent(pos.id)}`
      : `/api/oda/${encodeURIComponent(oda.id)}/positions`;
    setSaving(true);
    try {
      const r = await fetch(url, {
        method: mode === 'edit' ? 'PATCH' : 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { pushToast({ title: mode === 'edit' ? 'Modifica fallita' : 'Riga non creata', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      pushToast({ title: mode === 'edit' ? 'Riga aggiornata' : 'Riga aggiunta', tone: 'ok' });
      onSaved?.();
    } catch (e) {
      pushToast({ title: 'Errore di rete', desc: String(e?.message || e), tone: 'err' });
    } finally { setSaving(false); }
  };

  return (
    <Modal open onClose={onClose} title={mode === 'edit' ? `Modifica riga ${pos?.lineNo}` : 'Nuova riga d\'ordine'} size="md" footer={
      <>
        <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
        <Btn variant="primary" size="sm" onClick={submit} disabled={saving} data-testid="oda-pos-submit">{saving ? 'Salvo…' : 'Salva riga'}</Btn>
      </>
    }>
      <div className="col" style={{ gap: 10 }}>
        <div className="field">
          <label>Descrizione *</label>
          <input value={f.description} onChange={(e) => set('description', e.target.value)} placeholder="es. Cobot UR10e + controller" data-testid="oda-pos-desc" />
        </div>
        <div className="grid grid-3" style={{ gap: 8 }}>
          <div className="field"><label>Quantità</label><input type="number" min="0" step="0.001" value={f.quantity} onChange={(e) => set('quantity', e.target.value)} /></div>
          <div className="field"><label>UM</label><input value={f.uom} onChange={(e) => set('uom', e.target.value)} placeholder="PC" /></div>
          <div className="field"><label>Materiale (MATNR)</label><input value={f.materialCode} onChange={(e) => set('materialCode', e.target.value)} placeholder="cod. articolo" /></div>
        </div>
        <div className="grid grid-3" style={{ gap: 8 }}>
          <div className="field"><label>Prezzo unitario €</label><input type="number" min="0" step="0.01" value={f.unitPriceEur} onChange={(e) => set('unitPriceEur', e.target.value)} /></div>
          <div className="field"><label>Per N unità</label><input type="number" min="1" step="1" value={f.priceUnit} onChange={(e) => set('priceUnit', e.target.value)} /></div>
          <div className="field"><label>Valore netto</label><input value={eur2(netPreview)} readOnly style={{ background: 'var(--bg-2)', fontWeight: 600 }} data-testid="oda-pos-net" /></div>
        </div>
        <div className="grid grid-3" style={{ gap: 8 }}>
          <div className="field"><label>Elemento WBS</label><input value={f.wbsElement} onChange={(e) => set('wbsElement', e.target.value)} placeholder="P-2026-300.1.1" /></div>
          <div className="field"><label>Centro di costo</label><input value={f.costCenter} onChange={(e) => set('costCenter', e.target.value)} /></div>
          <div className="field"><label>Conto Co.Ge.</label><input value={f.glAccount} onChange={(e) => set('glAccount', e.target.value)} /></div>
        </div>
        <div className="grid grid-3" style={{ gap: 8 }}>
          <div className="field"><label>Stabilimento (WERKS)</label><input value={f.plantCode} onChange={(e) => set('plantCode', e.target.value)} /></div>
          <div className="field"><label>Cod. IVA</label><input value={f.taxCode} onChange={(e) => set('taxCode', e.target.value)} /></div>
          <div className="field"><label>Consegna riga</label><input type="date" value={f.deliveryDate} onChange={(e) => set('deliveryDate', e.target.value)} /></div>
        </div>
        <div className="field"><label>Note riga</label><input value={f.notes} onChange={(e) => set('notes', e.target.value)} /></div>
        <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
          Campi allineati a SAP EKPO (MATNR/MENGE/MEINS/NETPR/WBS/KOSTL/WERKS) per la futura integrazione.
        </div>
      </div>
    </Modal>
  );
}

// ============================================================
// Estrazione AI testata OdA — schema "flat" per /api/ai/extract con un campo
// gemello <campo>_confidence (0..100) per ogni valore + overall_confidence.
// Così riusiamo l'infra multi-provider esistente ottenendo anche le confidenze.
// ============================================================
const ODA_EXTRACT_FIELDS = [
  ['title', 'string', 'Oggetto/titolo dell\'ordine'],
  ['vendor', 'string', 'Ragione sociale del fornitore'],
  ['vendorCode', 'string', 'Codice/partita IVA del fornitore (SAP LIFNR) se presente'],
  ['docType', 'string', 'Tipo documento d\'acquisto (es. NB), se indicato'],
  ['orderDate', 'date', 'Data dell\'ordine (YYYY-MM-DD)'],
  ['expectedDelivery', 'date', 'Data di consegna prevista (YYYY-MM-DD)'],
  ['paymentTerms', 'string', 'Termini di pagamento (es. 60 gg d.f.)'],
  ['incoterms', 'string', 'Resa/Incoterms (es. DAP, EXW)'],
  ['incotermsLocation', 'string', 'Luogo della resa'],
  ['currency', 'string', 'Valuta (es. EUR)'],
  ['netTotal', 'number', 'Totale netto dell\'ordine in euro interi'],
];
const ODA_EXTRACT_SCHEMA = (() => {
  const properties = {};
  for (const [name, type, desc] of ODA_EXTRACT_FIELDS) {
    properties[name] = { type, description: desc };
    properties[`${name}_confidence`] = { type: 'number', description: `Confidenza 0-100 sul valore di ${name} (100=esplicito e certo, 0=non trovato)` };
  }
  // Righe d'ordine come ARRAY nidificato → righe ILLIMITATE, schema compatto
  // (il backend ai-extract supporta type:'array' su Claude/Gemini/DeepSeek).
  properties.positions = {
    type: 'array',
    description: 'Righe d\'ordine (posizioni) trovate nel documento, in ordine',
    items: {
      type: 'object',
      properties: {
        description: { type: 'string', description: 'Descrizione della riga' },
        materialCode: { type: 'string', description: 'Codice materiale/articolo' },
        quantity: { type: 'number', description: 'Quantità' },
        uom: { type: 'string', description: 'Unità di misura (es. PC, KG, M)' },
        unitPrice: { type: 'number', description: 'Prezzo unitario netto' },
      },
    },
  };
  properties.lines_confidence = { type: 'number', description: 'Confidenza 0-100 sull\'estrazione delle righe d\'ordine' };
  properties.overall_confidence = { type: 'number', description: 'Confidenza complessiva 0-100 sull\'estrazione' };
  return { type: 'object', properties, required: [] };
})();
const ODA_EXTRACT_INSTRUCTION =
  'Sei un assistente che legge un Ordine di Acquisto (Purchase Order). Estrai i dati di TESTATA dal documento allegato. ' +
  'Per OGNI campo fornisci anche <campo>_confidence: un intero 0-100 che indica quanto sei sicuro del valore ' +
  '(100 = trovato esplicitamente e inequivocabile nel documento, 0 = non presente / non deducibile). ' +
  'Se un campo non è nel documento lascialo vuoto e metti la sua confidenza a 0. NON inventare valori. ' +
  'Estrai ANCHE TUTTE le righe d\'ordine (posizioni) nell\'array "positions": ogni elemento ha ' +
  'description, materialCode, quantity, uom, unitPrice. Includi tutte le righe presenti, nell\'ordine. ' +
  'lines_confidence = confidenza sull\'estrazione delle righe. ' +
  'overall_confidence = affidabilità media complessiva dell\'estrazione. Gli importi sono in euro interi.';

function confChipKind(c) {
  if (c == null) return 'default';
  if (c >= 80) return 'ok';
  if (c >= 50) return 'warn';
  return 'err';
}
function ConfBadge({ c }) {
  if (c == null) return null;
  return <Chip kind={confChipKind(c)}>AI {Math.round(c)}%</Chip>;
}
function normExtractDate(v) {
  if (!v) return '';
  const s = String(v);
  const m = s.match(/\d{4}-\d{2}-\d{2}/);
  if (m) return m[0];
  const d = new Date(s);
  return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10);
}
// Normalizza un nome fornitore per la riconciliazione (toglie forme societarie + punteggiatura).
function vendorNormName(s) {
  return String(s || '')
    .toLowerCase()
    .replace(/\b(s\.?p\.?a\.?|s\.?r\.?l\.?|s\.?n\.?c\.?|s\.?a\.?s\.?|spa|srl|snc|sas|ltd|gmbh|inc|llc|co|kg)\b/g, '')
    .replace(/[^a-z0-9]/g, '')
    .trim();
}

// ============================================================
// NewOdaModal — creazione OdA diretta (testata) con UPLOAD documento + viewer
// laterale + "Estrai con AI" (pre-compila i campi con % di confidenza; l'utente
// rivede e conferma). Le posizioni si aggiungono dopo, dal detail. Usata dalla
// pagina Ordini di Acquisto e dal tab "OdA collegate" del progetto.
// ============================================================
function NewOdaModal({ projectId, onClose, onCreated }) {
  const { user, seed, extras, seedCustom, addOda, addVendor, pushToast } = useStore();
  const lockedProject = !!projectId;
  const fileInputRef = React.useRef(null);

  const projects = React.useMemo(() => {
    const ext = extras?.projects || [];
    const seen = new Set(ext.map((p) => p.id));
    return [...ext, ...(seed.PROJECTS || []).filter((p) => !seen.has(p.id))];
  }, [extras, seed]);

  const [f, setF] = React.useState({
    title: '', project: projectId || '', vendor: '', vendorCode: '', docType: '',
    status: 'draft', currency: 'EUR', orderDate: '', expectedDelivery: '',
    paymentTerms: '', incoterms: '', incotermsLocation: '', amount: '0', notes: '',
  });
  const [saving, setSaving] = React.useState(false);
  const [doc, setDoc] = React.useState(null); // { id, filename, mimeType }
  const [uploading, setUploading] = React.useState(false);
  const [extracting, setExtracting] = React.useState(false);
  const [conf, setConf] = React.useState({}); // { field: confidence }
  const [overallConf, setOverallConf] = React.useState(null);
  const [extractMeta, setExtractMeta] = React.useState(null); // { provider, model }
  // Posizioni in stato LOCALE: si costruiscono qui e si persistono al "Crea OdA".
  const [positions, setPositions] = React.useState([]);
  const [posForm, setPosForm] = React.useState(null); // { mode:'create'|'edit', pos?, lid? }
  const [addingVendor, setAddingVendor] = React.useState(false);
  const set = (k, v) => setF((s) => ({ ...s, [k]: v }));

  // ── Riconciliazione fornitore con l'anagrafica vendor ──
  const allVendors = React.useMemo(() => {
    const ext = extras?.vendors || [];
    const seen = new Set(ext.map((v) => v.id));
    return [...ext, ...((seed.VENDORS) || []).filter((v) => !seen.has(v.id))];
  }, [extras, seed]);
  const matchedVendor = React.useMemo(() => {
    const n = vendorNormName(f.vendor);
    const code = (f.vendorCode || '').trim().toLowerCase();
    if (!n && !code) return null;
    return allVendors.find((v) =>
      (n && vendorNormName(v.name) === n) ||
      (code && (String(v.taxId || '').toLowerCase() === code || String(v.id || '').toLowerCase() === code)),
    ) || null;
  }, [f.vendor, f.vendorCode, allVendors]);
  const canCreateVendor = typeof window !== 'undefined' && window.can ? window.can('vendor.create', user, seedCustom) : true;

  const sumPosNet = positions.reduce((a, p) => a + Number(p.netValueEur || 0), 0);
  const hasPositions = positions.length > 0;

  const addLocalPosition = (p) => {
    setPositions((arr) => [...arr, { _lid: 'L' + Date.now() + Math.random().toString(36).slice(2, 5), priceUnit: 1, ...p }]);
    setPosForm(null);
  };
  const updateLocalPosition = (lid, p) => {
    setPositions((arr) => arr.map((x) => (x._lid === lid ? { ...x, ...p } : x)));
    setPosForm(null);
  };
  const removeLocalPosition = (lid) => setPositions((arr) => arr.filter((x) => x._lid !== lid));

  const addVendorToAnagrafica = async () => {
    if (!f.vendor.trim() || addingVendor || !canCreateVendor) return;
    setAddingVendor(true);
    try {
      const r = await fetch('/api/vendors', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify({ name: f.vendor.trim(), taxId: f.vendorCode.trim() || null }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok || !j.data) { pushToast({ title: 'Fornitore non aggiunto', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      addVendor(j.data);
      pushToast({ title: 'Fornitore aggiunto in anagrafica', desc: j.data.name, tone: 'ok' });
    } catch (e) {
      pushToast({ title: 'Errore di rete', desc: String(e?.message || e), tone: 'err' });
    } finally { setAddingVendor(false); }
  };

  const onFileChange = async (e) => {
    const file = e.target.files && e.target.files[0];
    e.target.value = ''; // reset per permettere ri-upload dello stesso file
    if (!file) return;
    if (!f.project) { pushToast({ title: 'Seleziona prima il progetto', desc: 'Il documento si allega al progetto.', tone: 'err' }); return; }
    const fd = new FormData();
    fd.append('file', file);
    fd.append('projectId', f.project);
    fd.append('docTypeCode', 'ODA');
    fd.append('title', (f.title.trim() || file.name));
    setUploading(true);
    try {
      const r = await fetch('/api/documents/upload', {
        method: 'POST', credentials: 'same-origin',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
        body: fd,
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok || !j.data) { pushToast({ title: 'Upload fallito', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      setDoc({ id: j.data.id, filename: j.data.originalFilename || j.data.title || file.name, mimeType: j.data.mimeType || file.type || '' });
      pushToast({ title: 'Documento caricato', desc: 'Ora puoi estrarre i dati con l\'AI o compilare a mano.', tone: 'ok' });
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: String(err?.message || err), tone: 'err' });
    } finally { setUploading(false); }
  };

  const extractWithAi = async () => {
    if (!doc?.id || extracting) return;
    setExtracting(true);
    try {
      const r = await fetch('/api/ai/extract', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify({ docId: doc.id, schema: ODA_EXTRACT_SCHEMA, instruction: ODA_EXTRACT_INSTRUCTION }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok || !j.data) { pushToast({ title: 'Estrazione fallita', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      const v = j.data.values || {};
      setF((s) => ({
        ...s,
        title: v.title || s.title,
        vendor: v.vendor || s.vendor,
        vendorCode: v.vendorCode || s.vendorCode,
        docType: v.docType || s.docType,
        orderDate: normExtractDate(v.orderDate) || s.orderDate,
        expectedDelivery: normExtractDate(v.expectedDelivery) || s.expectedDelivery,
        paymentTerms: v.paymentTerms || s.paymentTerms,
        incoterms: v.incoterms || s.incoterms,
        incotermsLocation: v.incotermsLocation || s.incotermsLocation,
        currency: v.currency || s.currency,
        amount: v.netTotal != null && v.netTotal !== '' ? String(Math.round(Number(v.netTotal) || 0)) : s.amount,
      }));
      setConf({
        title: v.title_confidence, vendor: v.vendor_confidence, vendorCode: v.vendorCode_confidence,
        docType: v.docType_confidence, orderDate: v.orderDate_confidence, expectedDelivery: v.expectedDelivery_confidence,
        paymentTerms: v.paymentTerms_confidence, incoterms: v.incoterms_confidence, incotermsLocation: v.incotermsLocation_confidence,
        currency: v.currency_confidence, amount: v.netTotal_confidence,
      });
      setOverallConf(v.overall_confidence != null ? Number(v.overall_confidence) : null);
      setExtractMeta({ provider: j.data.provider, model: j.data.model });
      // Mappa le righe estratte (array `positions`, illimitate) in posizioni locali.
      const rawPositions = Array.isArray(v.positions) ? v.positions : [];
      const extractedLines = rawPositions
        .filter((p) => p && p.description != null && String(p.description).trim())
        .map((p, i) => {
          const qty = Number(p.quantity) || 0;
          const unit = Number(p.unitPrice) || 0;
          return {
            _lid: 'L' + Date.now() + '_' + i,
            description: String(p.description).trim(),
            materialCode: p.materialCode ? String(p.materialCode).trim() : null,
            quantity: qty,
            uom: (p.uom ? String(p.uom).trim() : 'PC') || 'PC',
            unitPriceEur: unit, priceUnit: 1,
            netValueEur: Math.round(qty * unit * 100) / 100,
            _ai: true,
            wbsElement: null, costCenter: null, glAccount: null, plantCode: null,
            taxCode: null, accountAssignmentCat: null, deliveryDate: null, notes: null,
          };
        });
      if (extractedLines.length > 0) setPositions(extractedLines);
      pushToast({
        title: 'Dati estratti dall\'AI',
        desc: `${j.data.provider || 'AI'} · testata + ${extractedLines.length} righe · controlla e conferma`,
        tone: 'ok',
      });
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: String(err?.message || err), tone: 'err' });
    } finally { setExtracting(false); }
  };

  const submit = async () => {
    if (!f.title.trim()) { pushToast({ title: 'Titolo obbligatorio', tone: 'err' }); return; }
    const body = {
      title: f.title.trim(),
      project: f.project || null,
      vendor: f.vendor.trim() || null,
      vendorCode: f.vendorCode.trim() || null,
      docType: f.docType.trim() || null,
      status: f.status || 'draft',
      currency: f.currency.trim() || 'EUR',
      amount: parseInt(f.amount, 10) || 0,
      orderDate: f.orderDate || null,
      expectedDelivery: f.expectedDelivery || null,
      paymentTerms: f.paymentTerms.trim() || null,
      incoterms: f.incoterms.trim() || null,
      incotermsLocation: f.incotermsLocation.trim() || null,
      notes: f.notes.trim() || null,
      source: overallConf != null ? 'ai_extracted' : 'manual',
      sourceDocumentId: doc?.id || null,
      amount: hasPositions ? Math.round(sumPosNet) : (parseInt(f.amount, 10) || 0),
      // Righe nel body di create: OdA + posizioni come UNICA operazione gated
      // `po.create` (no loop su /positions, che è gated `po.edit` e bloccherebbe
      // chi può creare ma non editare). Recompute totali server-side.
      positions: positions.map((p) => ({
        description: p.description, materialCode: p.materialCode || null,
        quantity: Number(p.quantity) || 0, uom: p.uom || 'PC',
        unitPriceEur: Number(p.unitPriceEur) || 0, priceUnit: Number(p.priceUnit) || 1,
        wbsElement: p.wbsElement || null, costCenter: p.costCenter || null,
        glAccount: p.glAccount || null, plantCode: p.plantCode || null,
        taxCode: p.taxCode || null, accountAssignmentCat: p.accountAssignmentCat || null,
        deliveryDate: p.deliveryDate || null, notes: p.notes || null,
      })),
    };
    setSaving(true);
    try {
      const r = await fetch('/api/oda', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok || !j.data) { pushToast({ title: 'OdA non creato', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      addOda(j.data);
      pushToast({
        title: 'OdA creato',
        desc: positions.length ? `${j.data.id} · ${positions.length} righe` : `${j.data.id} · aggiungi le posizioni`,
        tone: 'ok',
      });
      onCreated?.(j.data);
    } catch (e) {
      pushToast({ title: 'Errore di rete', desc: String(e?.message || e), tone: 'err' });
    } finally { setSaving(false); }
  };

  const downloadUrl = doc ? `/api/documents/${encodeURIComponent(doc.id)}/download` : null;
  const isPdf = doc && /pdf/i.test(doc.mimeType || '');
  const isImage = doc && /^image\//i.test(doc.mimeType || '');
  const labelStyle = { display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'space-between' };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          display: 'flex', background: 'var(--bg-1)', border: '1px solid var(--line)',
          borderRadius: 10, maxHeight: '92vh', overflow: 'hidden',
          width: doc ? 'min(1240px, 96vw)' : 'min(680px, 94vw)',
        }}
        data-testid="oda-new-modal"
      >
        {/* ── Pannello FORM ── */}
        <div style={{ width: doc ? 520 : '100%', flexShrink: 0, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
          <div className="modal-header" style={{ padding: '12px 16px', borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center' }}>
            <div className="title">Nuovo Ordine di Acquisto</div>
            <div style={{ flex: 1 }} />
            <button className="btn ghost icon" onClick={onClose}><Icon name="x" /></button>
          </div>
          <div style={{ overflow: 'auto', padding: 16, flex: 1 }}>
            <div className="col" style={{ gap: 10 }}>
              {/* Upload + estrazione */}
              <div style={{ border: '1px dashed var(--line)', borderRadius: 8, padding: 12, background: 'var(--bg-2)' }}>
                <input ref={fileInputRef} type="file" style={{ display: 'none' }}
                  accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp"
                  onChange={onFileChange} data-testid="oda-new-file" />
                <div className="row" style={{ gap: 8, alignItems: 'center' }}>
                  <Btn variant="ghost" size="sm" disabled={uploading || !f.project}
                    title={f.project ? 'Carica il documento d\'ordine (PDF, Word, Excel, immagini)' : 'Seleziona prima il progetto'}
                    onClick={() => { if (f.project && !uploading) fileInputRef.current && fileInputRef.current.click(); }}
                    data-testid="oda-new-upload-btn">
                    <Icon name="upload" size={12} /> {uploading ? 'Carico…' : (doc ? 'Sostituisci documento' : 'Carica documento')}
                  </Btn>
                  {doc && <span style={{ fontSize: 11, color: 'var(--text-2)' }}><Icon name="file_pdf" size={11} /> {doc.filename}</span>}
                </div>
                {!f.project && <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 6 }}>Seleziona il progetto per abilitare l'upload (il documento si allega al progetto).</div>}
                {!doc && f.project && <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 6 }}>PDF / Word / Excel / immagini. Dopo l'upload potrai estrarre i dati con l'AI o compilare a mano.</div>}
                {overallConf != null && (
                  <div className="row" style={{ gap: 8, alignItems: 'center', marginTop: 8 }}>
                    <span style={{ fontSize: 11, color: 'var(--text-2)' }}>Confidenza AI complessiva:</span>
                    <Chip kind={confChipKind(overallConf)} dot>{Math.round(overallConf)}%</Chip>
                    {extractMeta?.provider && <span style={{ fontSize: 10, color: 'var(--text-3)' }}>{extractMeta.provider}</span>}
                  </div>
                )}
              </div>

              <div className="field">
                <label style={labelStyle}><span>Titolo *</span><ConfBadge c={conf.title} /></label>
                <input value={f.title} onChange={(e) => set('title', e.target.value)} placeholder="es. Fornitura cella robotica" data-testid="oda-new-title" />
              </div>
              <div className="grid grid-2" style={{ gap: 8 }}>
                <div className="field">
                  <label>Progetto</label>
                  {lockedProject ? (
                    <input value={f.project} readOnly style={{ background: 'var(--bg-2)' }} />
                  ) : (
                    <window.Autocomplete
                      value={f.project}
                      onChange={(v) => set('project', v)}
                      options={projects.map((p) => ({ value: p.id, label: (p.name || p.id), sublabel: p.code || p.id, keywords: p.id }))}
                      placeholder="Cerca progetto… (spazio per lista)"
                      testId="oda-new-project"
                    />
                  )}
                </div>
                <div className="field"><label>Stato</label>
                  <select value={f.status} onChange={(e) => set('status', e.target.value)}>
                    {Object.keys(ODA_STATUS_LABEL).map((s) => <option key={s} value={s}>{ODA_STATUS_LABEL[s]}</option>)}
                  </select>
                </div>
              </div>
              <div className="grid grid-2" style={{ gap: 8 }}>
                <div className="field"><label style={labelStyle}><span>Vendor (nome)</span><ConfBadge c={conf.vendor} /></label><input value={f.vendor} onChange={(e) => set('vendor', e.target.value)} placeholder="es. Universal Robots" /></div>
                <div className="field"><label style={labelStyle}><span>Codice vendor (LIFNR)</span><ConfBadge c={conf.vendorCode} /></label><input value={f.vendorCode} onChange={(e) => set('vendorCode', e.target.value)} placeholder="es. V-100245" /></div>
              </div>
              {(f.vendor.trim() || f.vendorCode.trim()) && (
                <div className="row" style={{ gap: 8, alignItems: 'center', marginTop: -2 }} data-testid="oda-vendor-recon">
                  {matchedVendor ? (
                    <Chip kind="ok" dot>In anagrafica: {matchedVendor.name}</Chip>
                  ) : (
                    <>
                      <Chip kind="warn" dot>Nuovo fornitore — non in anagrafica</Chip>
                      <Btn variant="ghost" size="xs"
                        disabled={!canCreateVendor || addingVendor || !f.vendor.trim()}
                        title={canCreateVendor ? 'Aggiungi questo fornitore all\'anagrafica vendor' : window.whyDisabled('vendor.create')}
                        onClick={addVendorToAnagrafica} data-testid="oda-add-vendor">
                        <Icon name="plus" size={10} /> {addingVendor ? 'Aggiungo…' : 'Aggiungi fornitore'}
                      </Btn>
                    </>
                  )}
                </div>
              )}
              <div className="grid grid-3" style={{ gap: 8 }}>
                <div className="field"><label style={labelStyle}><span>Tipo doc.</span><ConfBadge c={conf.docType} /></label><input value={f.docType} onChange={(e) => set('docType', e.target.value)} placeholder="es. NB" /></div>
                <div className="field"><label style={labelStyle}><span>Valuta</span><ConfBadge c={conf.currency} /></label><input value={f.currency} onChange={(e) => set('currency', e.target.value)} /></div>
                <div className="field"><label style={labelStyle}><span>Importo €{hasPositions ? ' (da righe)' : ''}</span><ConfBadge c={conf.amount} /></label><input type="number" min="0" step="1" value={hasPositions ? String(Math.round(sumPosNet)) : f.amount} readOnly={hasPositions} onChange={(e) => { if (!hasPositions) set('amount', e.target.value); }} title={hasPositions ? 'Derivato dalla somma delle posizioni' : 'Verrà ricalcolato dalle posizioni dopo la creazione'} style={hasPositions ? { background: 'var(--bg-2)' } : undefined} /></div>
              </div>
              <div className="grid grid-2" style={{ gap: 8 }}>
                <div className="field"><label style={labelStyle}><span>Data ordine</span><ConfBadge c={conf.orderDate} /></label><input type="date" value={f.orderDate} onChange={(e) => set('orderDate', e.target.value)} /></div>
                <div className="field"><label style={labelStyle}><span>Consegna prevista</span><ConfBadge c={conf.expectedDelivery} /></label><input type="date" value={f.expectedDelivery} onChange={(e) => set('expectedDelivery', e.target.value)} /></div>
              </div>
              <div className="grid grid-3" style={{ gap: 8 }}>
                <div className="field"><label style={labelStyle}><span>Termini pagam.</span><ConfBadge c={conf.paymentTerms} /></label><input value={f.paymentTerms} onChange={(e) => set('paymentTerms', e.target.value)} placeholder="es. 60 gg d.f." /></div>
                <div className="field"><label style={labelStyle}><span>Incoterms</span><ConfBadge c={conf.incoterms} /></label><input value={f.incoterms} onChange={(e) => set('incoterms', e.target.value)} placeholder="es. DAP" /></div>
                <div className="field"><label style={labelStyle}><span>Luogo resa</span><ConfBadge c={conf.incotermsLocation} /></label><input value={f.incotermsLocation} onChange={(e) => set('incotermsLocation', e.target.value)} placeholder="es. Torino" /></div>
              </div>
              {/* Editor posizioni (righe) — stato locale, persistite al "Crea OdA" */}
              <div style={{ borderTop: '1px solid var(--line)', paddingTop: 10 }}>
                <div className="row" style={{ alignItems: 'center', marginBottom: 6 }}>
                  <div className="eyebrow" style={{ flex: 1 }}>Posizioni ({positions.length})</div>
                  <Btn variant="primary" size="xs" onClick={() => setPosForm({ mode: 'create' })} data-testid="oda-new-pos-add">
                    <Icon name="plus" size={11} /> Aggiungi riga
                  </Btn>
                </div>
                {positions.length === 0 ? (
                  <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>Nessuna riga. Aggiungile a mano oppure estraile dal documento con l'AI.</div>
                ) : (
                  <table className="tbl" style={{ fontSize: 11 }} data-testid="oda-new-pos-table">
                    <thead>
                      <tr><th>Descrizione</th><th className="num">Q.tà</th><th>UM</th><th className="num">Prezzo</th><th className="num">Netto</th><th></th></tr>
                    </thead>
                    <tbody>
                      {positions.map((p) => (
                        <tr key={p._lid}>
                          <td>{p.description} {p._ai && <Chip kind="ai">AI</Chip>}</td>
                          <td className="num">{Number(p.quantity).toLocaleString('it-IT', { maximumFractionDigits: 3 })}</td>
                          <td>{p.uom}</td>
                          <td className="num">{eur2(p.unitPriceEur)}</td>
                          <td className="num" style={{ fontWeight: 600 }}>{eur2(p.netValueEur)}</td>
                          <td style={{ whiteSpace: 'nowrap', textAlign: 'right' }}>
                            <button className="btn ghost icon" title="Modifica riga" onClick={() => setPosForm({ mode: 'edit', pos: p, lid: p._lid })}><Icon name="edit" size={12} /></button>
                            <button className="btn ghost icon" title="Elimina riga" onClick={() => removeLocalPosition(p._lid)}><Icon name="trash" size={12} /></button>
                          </td>
                        </tr>
                      ))}
                    </tbody>
                    <tfoot>
                      <tr style={{ borderTop: '2px solid var(--line)' }}>
                        <td colSpan={4} className="num" style={{ fontWeight: 600, color: 'var(--text-2)' }}>Totale netto</td>
                        <td className="num" style={{ fontWeight: 700 }}>{eur2(sumPosNet)}</td>
                        <td></td>
                      </tr>
                    </tfoot>
                  </table>
                )}
              </div>

              <div className="field"><label>Note</label><input value={f.notes} onChange={(e) => set('notes', e.target.value)} /></div>
              <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
                L'importo testata è la <strong>somma delle posizioni</strong>. I valori estratti dall'AI sono <strong>proposte</strong>: rivedi e correggi prima di confermare.
              </div>
            </div>
          </div>
          <div className="modal-footer" style={{ padding: '10px 16px', borderTop: '1px solid var(--line)', display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
            <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
            <Btn variant="primary" size="sm" onClick={submit} disabled={saving} data-testid="oda-new-submit">{saving ? 'Creo…' : 'Crea OdA'}</Btn>
          </div>
        </div>

        {/* ── Pannello VIEWER documento (laterale) ── */}
        {doc && (
          <div style={{ flex: 1, borderLeft: '1px solid var(--line)', display: 'flex', flexDirection: 'column', minWidth: 0 }}>
            <div className="row" style={{ gap: 8, alignItems: 'center', padding: '10px 12px', borderBottom: '1px solid var(--line)' }}>
              <Icon name="file_pdf" size={13} />
              <span style={{ fontSize: 12, fontWeight: 600, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{doc.filename}</span>
              <Btn variant="ai" size="sm" onClick={extractWithAi} disabled={extracting} data-testid="oda-extract-btn">
                <Icon name="sparkle" size={12} /> {extracting ? 'Estraggo…' : 'Estrai i dati con l\'AI'}
              </Btn>
            </div>
            <div style={{ flex: 1, background: 'var(--bg-2)', minHeight: 420, display: 'flex' }}>
              <DocViewerPane docId={doc.id} mimeType={doc.mimeType} filename={doc.filename} user={user} />
            </div>
          </div>
        )}
      </div>

      {/* Form riga in modalità LOCALE (ritorna la riga, niente API finché non si crea l'OdA) */}
      {posForm && (
        <OdaPositionForm
          oda={{ id: '', status: 'draft' }}
          mode={posForm.mode}
          pos={posForm.pos}
          user={user}
          pushToast={pushToast}
          onClose={() => setPosForm(null)}
          onSubmitLocal={(p) => { if (posForm.mode === 'edit') updateLocalPosition(posForm.lid, p); else addLocalPosition(p); }}
        />
      )}
    </div>
  );
}

// Building block condivisi (riusati da RdA.jsx per testata+posizioni+documento+AI).
Object.assign(window, {
  Oda, OdaDetailModal, OdaStatusChip, NewOdaModal,
  DocViewerPane, ConfBadge, confChipKind, vendorNormName, eur2, normExtractDate,
});
