// ============================================================
// RdA.jsx
// ============================================================
function RdA() {
  const { seed, pushToast, extras, upsertRda, user, routeParam, seedCustom, navigate, upsertOda } = useStore();
  // FASE 2 RBAC (sessione 102) — gating "+ Nuova RdA" e "Genera bozza RdA" (AI).
  const canCreateRda = window.can('rda.create', user, seedCustom);
  // FASE 2b — gating transition RdA action-aware (allineato a PATCH /api/rda/[id]).
  const canSubmitRda  = window.can('rda.submit', user, seedCustom);
  const canApproveRda = window.can('rda.approve', user, seedCustom);
  const canRejectRda  = window.can('rda.reject', user, seedCustom);
  const canWaive      = window.can('doc.waive', user, seedCustom);
  // FASE 10.5 — "Genera OdA" dalla RdA approvata (gating po.create).
  const canGenerateOda = window.can('po.create', user, seedCustom);
  const [generatingOda, setGeneratingOda] = React.useState(false);
  const [sel, setSel] = React.useState(null);
  const [showNew, setShowNew] = React.useState(false);
  const [showFullNew, setShowFullNew] = React.useState(false);
  const [viewer, setViewer] = React.useState(null);
  const [statusSaving, setStatusSaving] = React.useState(null); // 'approved' | 'rejected' | 'in_review' | null
  // Sessione 79 (B1) - Checklist Engine server-side evaluation per RdA selezionato.
  // Result strutturato: required[], optional[], missing[], present[], scoreBp, blocking.
  const [serverEval, setServerEval] = React.useState(null);
  const [evalLoading, setEvalLoading] = React.useState(false);
  // Sessione 84 (gap #9) — Drawer "Perché questi documenti?" con explain rules.
  const [whyDrawerOpen, setWhyDrawerOpen] = React.useState(false);
  const [rulesCatalog, setRulesCatalog] = React.useState(null); // cache full rules list per explain
  // Sessione 85 #10 — Waiver state
  const [waiverForm, setWaiverForm] = React.useState(null); // {docCode, reason, justification, expiresAt} | null
  const [waiverSaving, setWaiverSaving] = React.useState(false);
  // FASE 7: SLA live status per ogni RdA visibile nella pagina corrente.
  const [slaMap, setSlaMap] = React.useState({});
  // FASE 2b.5: dedup per id (extras vince su seed) — necessario perché upsertRda
  // copia il record aggiornato in extras ma il seed.RDA contiene ancora la versione
  // vecchia. Senza dedup la riga compare due volte in tabella.
  const allRda = React.useMemo(() => {
    const extra = extras?.rda || [];
    const extraIds = new Set(extra.map(r => r.id));
    const seedFiltered = (seed.RDA || []).filter(r => !extraIds.has(r.id));
    return [...extra, ...seedFiltered];
  }, [extras, seed]);
  const pg = usePaginated(allRda, 10);

  // Sessione 88 — routeParam auto-apply: quando si naviga via `navigate('rda', <id>)`
  // (es. da ProjectDetail tab "RdA collegate" click su riga), apri direttamente il
  // detail modal della RdA target invece di mostrare solo la lista. Pattern coerente
  // con Customizing (sessione 59 cumulativo 137). Trigger solo on routeParam change
  // (non on allRda update) per evitare riapertura modale dopo chiusura utente.
  React.useEffect(() => {
    if (!routeParam) return;
    const target = allRda.find((r) => r.id === routeParam);
    if (target) setSel(target);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [routeParam]);

  // FASE 7: fetch SLA per ogni RdA visibile (pg.slice). Cache per id, refresh
  // quando la slice cambia. Errori silent — semplice fallback a "—" in tabella.
  React.useEffect(() => {
    const ids = pg.slice.map(r => r.id);
    if (!ids.length) return;
    let cancelled = false;
    (async () => {
      const promises = ids.map(async (id) => {
        if (slaMap[id]) return [id, slaMap[id]];
        try {
          const r = await fetch(`/api/rda/${encodeURIComponent(id)}/sla`);
          if (!r.ok) return [id, null];
          const j = await r.json();
          return [id, j];
        } catch { return [id, null]; }
      });
      const entries = await Promise.all(promises);
      if (cancelled) return;
      setSlaMap(prev => {
        const next = { ...prev };
        for (const [id, val] of entries) if (val !== undefined) next[id] = val;
        return next;
      });
    })();
    return () => { cancelled = true; };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [pg.slice.map(r => r.id).join(',')]);

  // Sessione 79 (B1) - Fetch checklist server-side eval su apertura RdA detail
  // (dry-run = no persist quando solo si apre il modal). Refresh manuale via CTA.
  React.useEffect(() => {
    if (!sel) { setServerEval(null); return; }
    let cancelled = false;
    setEvalLoading(true);
    (async () => {
      try {
        const url = `/api/checklist/evaluate?entityType=rda&entityId=${encodeURIComponent(sel.id)}`;
        const r = await fetch(url, { headers: { 'X-Actor-Persona-Id': user?.id || '' } });
        if (!r.ok) { if (!cancelled) setServerEval(null); return; }
        const j = await r.json();
        if (cancelled) return;
        setServerEval(j.data || null);
      } catch { if (!cancelled) setServerEval(null); }
      finally { if (!cancelled) setEvalLoading(false); }
    })();
    return () => { cancelled = true; };
  }, [sel?.id, user?.id]);

  // Sessione 84 (gap #9) — Fetch rules catalog 1x per explain drawer.
  React.useEffect(() => {
    if (!whyDrawerOpen || rulesCatalog !== null) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch('/api/config/checklist-rules', { headers: { 'X-Actor-Persona-Id': user?.id || '' } });
        if (!r.ok) return;
        const j = await r.json();
        if (!cancelled) setRulesCatalog(j.data || []);
      } catch {}
    })();
    return () => { cancelled = true; };
  }, [whyDrawerOpen, user?.id]);

  // Sessione 79 (B1) - Trigger ri-valutazione persistente (POST con persist=true).
  // Aggiorna rda.missingDocs + completeness_bp lato server; refresh local sel.
  const recomputeChecklist = async () => {
    if (!sel) return;
    setEvalLoading(true);
    try {
      const res = await fetch('/api/checklist/evaluate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ entityType: 'rda', entityId: sel.id, triggerSource: 'manual', persist: true }),
      });
      const json = await res.json().catch(() => null);
      if (!res.ok) {
        pushToast({ title: 'Ricalcolo checklist fallito', desc: json?.error || `HTTP ${res.status}`, tone: 'err' });
        return;
      }
      setServerEval(json.data);
      const score = Math.round((json.data?.result?.scoreBp ?? 0) / 100);
      pushToast({
        title: 'Checklist ricalcolata',
        desc: `${sel.id} · completezza ${score}% · ${json.data.result.missingCodes.length} doc mancanti`,
        tone: json.data.result.blocking ? 'warn' : 'ok',
      });
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'Ricalcolo fallito', tone: 'err' });
    } finally { setEvalLoading(false); }
  };

  // Sessione 85 #10 — Create / revoke waiver
  const submitWaiver = async () => {
    if (!waiverForm || !sel) return;
    if (!waiverForm.justification || waiverForm.justification.length < 10) {
      pushToast({ title: 'Giustificazione troppo corta', desc: 'Min 10 caratteri richiesti.', tone: 'err' });
      return;
    }
    setWaiverSaving(true);
    try {
      const r = await fetch('/api/checklist-exceptions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({
          entityType: 'rda',
          entityId: sel.id,
          docCode: waiverForm.docCode,
          reason: waiverForm.reason,
          justification: waiverForm.justification,
          expiresAt: waiverForm.expiresAt || null,
        }),
      });
      const j = await r.json();
      if (!r.ok) {
        pushToast({ title: 'Waiver fallito', desc: j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({ title: 'Waiver creato', desc: `${waiverForm.docCode} → ${waiverForm.reason}`, tone: 'ok' });
      setWaiverForm(null);
      // Re-fetch eval per aggiornare drawer
      await recomputeChecklist();
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'POST fallito', tone: 'err' });
    } finally { setWaiverSaving(false); }
  };

  const revokeWaiver = async (exceptionId) => {
    if (!confirm('Revocare questo waiver? Il documento tornerà obbligatorio.')) return;
    try {
      const r = await fetch(`/api/checklist-exceptions/${encodeURIComponent(exceptionId)}`, {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ revokedReason: 'Manual revoke from RdA drawer' }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        pushToast({ title: 'Revoca fallita', desc: j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({ title: 'Waiver revocato', desc: exceptionId, tone: 'warn' });
      await recomputeChecklist();
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'DELETE fallito', tone: 'err' });
    }
  };

  // FASE 2b.5: PATCH status RdA con audit automatico server-side. Aggiorna sia il
  // record selezionato (per ri-render del modal) sia lo store globale via upsertRda.
  const changeStatus = async (newStatus) => {
    if (!sel || statusSaving) return;
    setStatusSaving(newStatus);
    try {
      const res = await fetch(`/api/rda/${sel.id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ status: newStatus }),
      });
      const json = await res.json().catch(() => null);
      if (!res.ok) {
        const msg = json?.error === 'validation_error'
          ? (json.issues?.map(i => `${i.path?.join('.') || 'campo'}: ${i.message}`).join(' · ') || 'Validazione fallita')
          : (json?.error || `Errore HTTP ${res.status}`);
        pushToast({ title: 'Cambio stato fallito', desc: msg, tone: 'err' });
        setStatusSaving(null);
        return;
      }
      const updated = json.data;
      upsertRda(updated);
      setSel(updated);
      const labels = { in_review: 'In review', approved: 'Approvata', rejected: 'Rifiutata' };
      pushToast({
        title: `RdA ${labels[newStatus] || newStatus}`,
        desc: `${updated.id} → status: ${updated.status}. Audit log registrato.`,
        tone: newStatus === 'rejected' ? 'warn' : 'ok',
      });
      setStatusSaving(null);
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'PATCH fallita', tone: 'err' });
      setStatusSaving(null);
    }
  };

  // FASE 10.5 — conversione RdA→OdA dal modal inline della pagina /rda.
  const generateOda = async () => {
    if (!sel || generatingOda) return;
    setGeneratingOda(true);
    try {
      const res = await fetch(`/api/rda/${encodeURIComponent(sel.id)}/generate-oda`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
      });
      const j = await res.json().catch(() => null);
      if (!res.ok) {
        pushToast({ title: 'Genera OdA fallita', desc: j?.detail || j?.error || `HTTP ${res.status}`, tone: 'err' });
        return;
      }
      if (j?.data && upsertOda) upsertOda(j.data);
      pushToast({ title: 'OdA generata', desc: `${j.data.id}${j.workflowStarted ? ' · workflow ' + j.workflowStarted + ' avviato' : ''}`, tone: 'ok' });
      if (navigate) navigate('oda', j.data.id);
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'Genera OdA fallita', tone: 'err' });
    } finally {
      setGeneratingOda(false);
    }
  };

  return (
    <div className="page fade-in">
      <div className="page-header">
        <div>
          <div className="eyebrow">Procurement</div>
          <h1 className="page-title">Richieste di Acquisto</h1>
          <div className="page-sub">8 RdA attive · 2 incomplete (l'AI ha già generato le email di follow-up). La bozza AI può essere revisionata in qualsiasi momento.</div>
        </div>
        <div className="actions">
          <Btn variant="ghost" size="sm"><Icon name="download" size={12}/> Export</Btn>
          <Btn
            variant={canCreateRda ? 'ai' : 'ghost'}
            size="sm"
            disabled={!canCreateRda}
            onClick={() => { if (canCreateRda) setShowNew(true); }}
            title={canCreateRda ? undefined : window.whyDisabled('rda.create')}
          ><Icon name="sparkle" size={12}/> Genera bozza RdA</Btn>
          <Btn
            variant={canCreateRda ? 'primary' : 'ghost'}
            size="sm"
            disabled={!canCreateRda}
            onClick={() => { if (canCreateRda) setShowFullNew(true); }}
            title={canCreateRda ? undefined : window.whyDisabled('rda.create')}
          ><Icon name="plus" size={12}/> Nuova RdA</Btn>
        </div>
      </div>

      <div className="grid grid-4" style={{ marginBottom: 14 }}>
        <div className="card"><Stat label="RdA aperte" value={seed.RDA.length} /></div>
        <div className="card"><Stat label="In review" value={seed.RDA.filter(r => r.status==='in_review').length} tone="" /></div>
        <div className="card"><Stat label="Incomplete (AI)" value={seed.RDA.filter(r => r.completeness < 1).length} delta="allegati mancanti" tone="down" /></div>
        <div className="card"><Stat label="Valore totale" value={fmtEUR(seed.RDA.reduce((a,b)=>a+b.amount,0), true)} /></div>
      </div>

      <div className="card">
        <table className="tbl">
          <thead>
            <tr>
              <th>RdA</th><th>Progetto</th><th>Oggetto</th><th>Vendor</th><th className="num">Importo</th><th style={{width:140}}>Completezza</th><th>Stato</th><th style={{width:90}}>SLA</th><th>Creata</th>
            </tr>
          </thead>
          <tbody>
            {pg.slice.map((r) => (
              <tr key={r.id} className="clickable" onClick={() => setSel(r)}>
                <td className="mono" style={{ color: 'var(--text-2)', fontSize: 11 }}>{r.id}</td>
                <td className="mono" style={{ fontSize: 11 }}>{r.project}</td>
                <td style={{ fontWeight: 500 }}>{r.title}</td>
                <td>{r.vendor}</td>
                <td className="num">{fmtEUR(r.amount, true)}</td>
                <td>
                  <div className="row" style={{ gap: 8 }}>
                    <Meter value={r.completeness*100} tone={r.completeness===1?'ok':r.completeness>0.8?'warn':'err'} />
                    <span className="num" style={{ fontSize: 11 }}>{Math.round(r.completeness*100)}%</span>
                  </div>
                </td>
                <td><Chip kind={r.status==='approved'?'ok':r.status==='rejected'?'err':r.status==='in_review'?'warn':'info'} dot>{r.status}</Chip></td>
                <td>
                  {(() => {
                    const sla = slaMap[r.id];
                    if (!sla) return <span style={{color:'var(--text-3)', fontSize:11}}>…</span>;
                    if (!sla.applicable) return <span style={{color:'var(--text-3)', fontSize:11}}>—</span>;
                    const tone = sla.status === 'breach' ? 'err' : sla.status === 'warn' ? 'warn' : 'ok';
                    const label = sla.status === 'breach'
                      ? `+${sla.daysOverdue}g`
                      : sla.status === 'warn'
                        ? `${sla.daysLeft}g`
                        : `${sla.daysLeft}g`;
                    return <Chip kind={tone} dot title={`${sla.policy?.code || ''} · ${sla.elapsedDays}g elapsed`}>{label}</Chip>;
                  })()}
                </td>
                <td className="mono" style={{ fontSize: 11, color: 'var(--text-2)' }}>{fmtDate(r.created)}</td>
              </tr>
            ))}
          </tbody>
        </table>
        <Pagination {...pg} />
      </div>

      {sel && <RdaDetailModal rda={sel} onClose={() => setSel(null)} />}

      <Modal open={showNew} onClose={() => setShowNew(false)} title="Genera bozza RdA con AI" footer={
        <>
          <Btn variant="ghost" size="sm" onClick={() => setShowNew(false)}>Annulla</Btn>
          <Btn variant="ai" size="sm" onClick={() => { setShowNew(false); pushToast({ title: 'Bozza RdA generata', desc: 'Basata su 3 RdA storiche simili · completezza stimata 91%', tone: 'ok' }); }}><Icon name="sparkle" size={12}/> Genera</Btn>
        </>
      }>
        <div className="col" style={{ gap: 12 }}>
          <div className="field"><label>Progetto</label><select><option>P-2026-013 — AI vision ispezione cablaggi</option></select></div>
          <div className="field"><label>Descrizione del bisogno</label><textarea rows={4} defaultValue="Sistema di visione AI per ispezione cablaggi con capacità 250 pz/h, integrazione con MES esistente." /></div>
          <div className="field"><label>Importo atteso</label><input defaultValue="620.000 €" /></div>
          <div className="field"><label>Vendor preferiti</label><input defaultValue="Keyence Italia, Cognex" /></div>
          <div style={{ fontSize: 11.5, color: 'var(--text-2)', padding: 10, background: 'var(--bg-2)', borderRadius: 8 }}>
            <Icon name="sparkle" size={11}/> L'AI compilerà allegati, checklist di conformità, template di approvazione e genererà una bozza di comunicazione verso i vendor.
          </div>
        </div>
      </Modal>

      <PDFViewer open={!!viewer} doc={viewer} onClose={() => setViewer(null)} />
      {showFullNew && <RdaCreateModal onClose={() => setShowFullNew(false)} />}

      {/* Sessione 84 (gap #9) — Drawer "Perché questi documenti?" */}
      {whyDrawerOpen && (
        <div onClick={() => setWhyDrawerOpen(false)} style={{
          position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 200,
        }} data-testid="rda-why-drawer-backdrop">
          <div onClick={(e) => e.stopPropagation()} style={{
            position: 'fixed', right: 0, top: 0, bottom: 0, width: 640,
            background: 'var(--bg)', borderLeft: '1px solid var(--line)',
            boxShadow: '-8px 0 24px rgba(0,0,0,0.3)', overflowY: 'auto', padding: 24,
            animation: 'slideInRight 0.2s ease-out',
          }} data-testid="rda-why-drawer">
            <div className="row" style={{ justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 20 }}>
              <div>
                <div className="eyebrow">Perché questi documenti?</div>
                <h2 style={{ fontSize: 18, marginTop: 4 }}>Explain checklist rules</h2>
              </div>
              <Btn variant="ghost" size="sm" onClick={() => setWhyDrawerOpen(false)} data-testid="rda-why-close">
                <Icon name="close" size={11}/>
              </Btn>
            </div>

            {!serverEval?.result ? (
              <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Nessuna evaluation server-side disponibile.</div>
            ) : (
              <>
                <div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 16, padding: 12, background: 'var(--bg-2)', borderRadius: 8 }}>
                  Il motore checklist server-side ha valutato l'RdA <code className="mono">{sel.id}</code> contro {serverEval.result.stats.rulesEvaluated} regole attive del tenant. <b>{serverEval.result.matchedRuleIds.length}</b> regole hanno matchato le condizioni (amount, category, status, ...) generando il set di documenti richiesti.
                </div>

                <div className="eyebrow" style={{ marginBottom: 8 }}>Regole matchate ({serverEval.result.matchedRuleIds.length})</div>
                {!rulesCatalog ? (
                  <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Caricamento regole…</div>
                ) : serverEval.result.matchedRuleIds.length === 0 ? (
                  <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Nessuna regola matchata.</div>
                ) : (
                  serverEval.result.matchedRuleIds.map((ruleId) => {
                    const rule = rulesCatalog.find(r => r.id === ruleId);
                    if (!rule) return (
                      <div key={ruleId} style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 6, marginBottom: 8, fontSize: 11.5 }}>
                        <div className="mono">{ruleId}</div>
                        <div style={{ color: 'var(--text-3)' }}>Rule non più disponibile nel catalogo corrente.</div>
                      </div>
                    );
                    return (
                      <div key={ruleId} style={{ padding: 12, border: '1px solid var(--line)', borderRadius: 8, marginBottom: 10 }} data-testid={`rda-why-rule-${rule.id}`}>
                        <div className="row" style={{ justifyContent: 'space-between', alignItems: 'flex-start' }}>
                          <div>
                            <div style={{ fontWeight: 600, fontSize: 13 }}>{rule.name}</div>
                            <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{rule.id} · priority {rule.priority}</div>
                          </div>
                          <Chip kind="info" dot>{rule.entityType}</Chip>
                        </div>

                        <div style={{ marginTop: 8 }}>
                          <div className="eyebrow" style={{ fontSize: 10 }}>Condizione</div>
                          <pre style={{ fontSize: 10.5, background: 'var(--bg-2)', padding: 8, borderRadius: 4, overflowX: 'auto', marginTop: 4, lineHeight: 1.5 }}>{JSON.stringify(rule.conditions, null, 2)}</pre>
                        </div>

                        {rule.required && rule.required.length > 0 && (
                          <div style={{ marginTop: 8 }}>
                            <div className="eyebrow" style={{ fontSize: 10 }}>Documenti obbligatori che genera</div>
                            <div className="row" style={{ gap: 4, flexWrap: 'wrap', marginTop: 4 }}>
                              {rule.required.map(c => (
                                <Chip key={c} kind={serverEval.result.presentCodes.includes(c) ? 'ok' : 'warn'} dot>
                                  {c} {serverEval.result.presentCodes.includes(c) ? '✓' : '!'}
                                </Chip>
                              ))}
                            </div>
                          </div>
                        )}
                        {rule.optional && rule.optional.length > 0 && (
                          <div style={{ marginTop: 8 }}>
                            <div className="eyebrow" style={{ fontSize: 10 }}>Documenti opzionali</div>
                            <div className="row" style={{ gap: 4, flexWrap: 'wrap', marginTop: 4 }}>
                              {rule.optional.map(c => <Chip key={c}>{c}</Chip>)}
                            </div>
                          </div>
                        )}
                      </div>
                    );
                  })
                )}

                <div className="eyebrow" style={{ marginTop: 20, marginBottom: 8 }}>Set finale documenti</div>
                <div style={{ fontSize: 11.5, color: 'var(--text-2)', marginBottom: 8 }}>
                  Dopo priority merge + forbidden remove:
                </div>
                <div className="grid grid-2" style={{ gap: 10 }}>
                  <div style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 6 }}>
                    <div className="eyebrow" style={{ fontSize: 10 }}>Obbligatori ({serverEval.result.requiredCodes.length})</div>
                    <div className="row" style={{ gap: 3, flexWrap: 'wrap', marginTop: 4 }}>
                      {serverEval.result.requiredCodes.map(c => (
                        <Chip key={c} kind={serverEval.result.presentCodes.includes(c) ? 'ok' : 'warn'} dot>{c}</Chip>
                      ))}
                    </div>
                  </div>
                  <div style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 6 }}>
                    <div className="eyebrow" style={{ fontSize: 10 }}>Mancanti ({serverEval.result.missingCodes.length})</div>
                    <div className="row" style={{ gap: 3, flexWrap: 'wrap', marginTop: 4 }}>
                      {serverEval.result.missingCodes.length === 0
                        ? <span style={{ fontSize: 11, color: 'var(--ok)' }}>✓ tutto presente</span>
                        : serverEval.result.missingCodes.map(c => (
                            <div key={c} className="row" style={{ gap: 4, alignItems: 'center' }}>
                              <Chip kind="err" dot>{c}</Chip>
                              <Btn
                                variant="ghost"
                                size="xs"
                                disabled={!canWaive}
                                onClick={() => { if (canWaive) setWaiverForm({ docCode: c, reason: 'not_applicable', justification: '', expiresAt: '' }); }}
                                title={canWaive ? undefined : window.whyDisabled('doc.waive')}
                                data-testid={`rda-waiver-btn-${c}`}
                              >
                                Waivera
                              </Btn>
                            </div>
                          ))
                      }
                    </div>
                  </div>
                </div>

                {/* Waivers attivi */}
                {serverEval.result.waivedCodes && serverEval.result.waivedCodes.length > 0 && (
                  <>
                    <div className="eyebrow" style={{ marginTop: 20, marginBottom: 8 }}>Documenti waivati ({serverEval.result.waivedCodes.length})</div>
                    <div style={{ fontSize: 11.5, color: 'var(--text-2)', marginBottom: 8 }}>
                      Eccezioni attive: questi documenti sono dichiarati non applicabili / sostituiti.
                    </div>
                    {serverEval.result.waivers.map((w) => (
                      <div key={w.exceptionId} style={{ padding: 10, background: 'rgba(220,180,80,0.08)', border: '1px solid var(--warn)', borderRadius: 6, marginBottom: 6 }} data-testid={`rda-waiver-${w.exceptionId}`}>
                        <div className="row" style={{ justifyContent: 'space-between', alignItems: 'flex-start' }}>
                          <div style={{ flex: 1 }}>
                            <div className="row" style={{ gap: 6 }}>
                              <Chip kind="warn" dot>{w.docCode}</Chip>
                              <span style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{w.reason}</span>
                            </div>
                            <div style={{ fontSize: 11, color: 'var(--text-2)', marginTop: 4 }}>{w.justification}</div>
                            <div className="mono" style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 4 }}>
                              {w.exceptionId} · {new Date(w.approvedAt).toLocaleDateString('it-IT')}
                              {w.expiresAt ? ` · scade ${new Date(w.expiresAt).toLocaleDateString('it-IT')}` : ' · no expiry'}
                            </div>
                          </div>
                          <Btn variant="ghost" size="xs" onClick={() => revokeWaiver(w.exceptionId)} data-testid={`rda-waiver-revoke-${w.exceptionId}`}>
                            Revoca
                          </Btn>
                        </div>
                      </div>
                    ))}
                  </>
                )}

                <div style={{ marginTop: 16, padding: 10, background: 'var(--bg-2)', borderRadius: 6, fontSize: 10.5, color: 'var(--text-3)' }}>
                  Configurazione regole modificabile in <b>Customizing → Checklist Rules</b>. Ogni modifica versionata via config_version + audit log.
                </div>
              </>
            )}
          </div>
        </div>
      )}

      {/* Sessione 85 #10 — Waiver creation modal */}
      {waiverForm && (
        <Modal open={!!waiverForm} onClose={() => setWaiverForm(null)} title={`Waivera documento — ${waiverForm.docCode}`} size="md" footer={
          <>
            <Btn variant="ghost" size="sm" onClick={() => setWaiverForm(null)}>Annulla</Btn>
            <Btn variant="primary" size="sm" onClick={submitWaiver} disabled={waiverSaving} data-testid="rda-waiver-submit">
              <Icon name="check" size={11}/> {waiverSaving ? 'Salvataggio…' : 'Crea waiver'}
            </Btn>
          </>
        }>
          <div className="col" style={{ gap: 10 }}>
            <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
              Stai creando un'eccezione checklist per il documento <code className="mono">{waiverForm.docCode}</code> della RdA <code className="mono">{sel?.id}</code>. Il documento verrà considerato "waivato" invece di "missing" fino a revoca o scadenza.
            </div>
            <div className="field">
              <label>Motivo *</label>
              <select value={waiverForm.reason} onChange={(e) => setWaiverForm({ ...waiverForm, reason: e.target.value })} data-testid="rda-waiver-reason">
                <option value="not_applicable">Non applicabile</option>
                <option value="replaced_by_alternative">Sostituito da alternativa</option>
                <option value="one_off">Caso una tantum</option>
                <option value="pending_external">In attesa di esterno</option>
              </select>
            </div>
            <div className="field">
              <label>Giustificazione * (≥ 10 char)</label>
              <textarea value={waiverForm.justification} onChange={(e) => setWaiverForm({ ...waiverForm, justification: e.target.value })} rows={3} placeholder="es. Documento non richiesto per progetti sotto soglia 50k€" data-testid="rda-waiver-justification"/>
            </div>
            <div className="field">
              <label>Scadenza waiver (opzionale)</label>
              <input type="datetime-local" value={waiverForm.expiresAt} onChange={(e) => setWaiverForm({ ...waiverForm, expiresAt: e.target.value ? new Date(e.target.value).toISOString() : '' })}/>
            </div>
            <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
              Audit metadata-only: solo {`{docCode, reason, justificationLen, hasExpiry}`} viene loggato. La giustificazione testuale resta in `checklist_exception` ma MAI in audit_log.
            </div>
          </div>
        </Modal>
      )}
    </div>
  );
}
// FASE 8 — Workflow Engine state machine UI.
// Mostra istanze workflow per un'entità di dominio. Permette:
// - avvio workflow (se entityType ha workflow disponibili e nessuna istanza in_progress)
// - vedere step state
// - approve/reject lo step attivo (se utente è approver — controllo soft lato UI)
function WorkflowInstancesCard({ entityType, entityId }) {
  const { user, pushToast, seedCustom } = useStore();
  const [instances, setInstances] = React.useState(null); // null = loading
  const [selInstance, setSelInstance] = React.useState(null);
  const [showStart, setShowStart] = React.useState(false);

  const reload = React.useCallback(() => {
    setInstances(null);
    fetch(`/api/workflow-instances?entityType=${encodeURIComponent(entityType)}&entityId=${encodeURIComponent(entityId)}`)
      .then(r => r.json())
      .then(json => setInstances(json.data || []))
      .catch(() => setInstances([]));
  }, [entityType, entityId]);

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

  const inProgress = (instances || []).find(i => i.status === 'in_progress');

  return (
    <div className="card flush" style={{border: '1px solid var(--line)', borderRadius: 10, padding: 14}}>
      <div className="card-header" style={{padding: 0, marginBottom: 10}}>
        <div className="title"><Icon name="workflow" size={12}/> Workflow approvazione</div>
        <div className="actions">
          {!inProgress && instances && (
            <Btn variant="primary" size="sm" onClick={() => setShowStart(true)}><Icon name="plus" size={11}/> Avvia workflow</Btn>
          )}
        </div>
      </div>
      {instances === null ? (
        <div style={{fontSize: 11, color: 'var(--text-3)', textAlign: 'center', padding: 8}}>Caricamento…</div>
      ) : instances.length === 0 ? (
        <div style={{fontSize: 11, color: 'var(--text-3)', padding: '10px 0'}}>Nessuna istanza workflow per questa entità.</div>
      ) : (
        <div className="col" style={{gap: 8}}>
          {instances.map(i => (
            <div key={i.id} className="row" style={{gap: 8, padding: '6px 8px', background: 'var(--bg-2)', borderRadius: 4, alignItems: 'center', cursor: 'pointer'}} onClick={() => setSelInstance(i.id)}>
              <Chip kind={i.status === 'in_progress' ? 'info' : i.status === 'completed' ? 'ok' : i.status === 'rejected' ? 'err' : ''} dot>{i.status}</Chip>
              <span style={{fontSize: 11.5}}>{i.workflowSnapshot?.name || i.workflowId}</span>
              <span className="spacer"/>
              <span className="mono" style={{fontSize: 10, color: 'var(--text-3)'}}>step {i.currentStepIndex !== null ? (i.currentStepIndex + 1) + '/' + (i.workflowSnapshot?.steps?.length || '?') : '—'}</span>
              <Icon name="chev_r" size={12}/>
            </div>
          ))}
        </div>
      )}

      {showStart && (
        <StartWorkflowModal
          entityType={entityType}
          entityId={entityId}
          workflows={(seedCustom?.APPROVAL_WORKFLOWS || []).filter(w => w.entityType === entityType && w.status === 'active')}
          onClose={() => setShowStart(false)}
          onStarted={() => { setShowStart(false); reload(); pushToast({ title: 'Workflow avviato', desc: 'Step iniziale attivato', tone: 'ok' }); }}
          user={user}
        />
      )}
      {selInstance && (
        <WorkflowInstanceDetailModal
          instanceId={selInstance}
          onClose={() => setSelInstance(null)}
          onTransition={() => reload()}
          user={user}
          pushToast={pushToast}
        />
      )}
    </div>
  );
}

function StartWorkflowModal({ entityType, entityId, workflows, onClose, onStarted, user }) {
  const [workflowId, setWorkflowId] = React.useState(workflows[0]?.id || '');
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);

  async function handleStart() {
    if (!workflowId) return;
    setSaving(true); setError(null);
    try {
      const res = await fetch('/api/workflow-instances', {
        method: 'POST',
        headers: { 'content-type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify({ workflowId, entityType, entityId }),
      });
      const json = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(json?.error || `HTTP ${res.status}`);
        return;
      }
      onStarted();
    } catch (e) { setError(String(e?.message || e)); }
    finally { setSaving(false); }
  }

  return (
    <Modal open onClose={onClose} title="Avvia workflow approvazione" size="md" footer={<>
      <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
      <Btn variant="primary" size="sm" disabled={!workflowId || saving} onClick={handleStart}>
        {saving ? 'Avvio…' : 'Avvia'}
      </Btn>
    </>}>
      <div className="col" style={{gap: 14}}>
        {error && <div style={{padding: '8px 10px', border: '1px solid var(--err)', borderRadius: 4, background: 'rgba(192,57,43,0.08)', color: 'var(--err)', fontSize: 12}}>Errore: {error}</div>}
        {workflows.length === 0 ? (
          <div style={{padding: 14, textAlign: 'center', fontSize: 12, color: 'var(--text-3)'}}>
            Nessun workflow attivo configurato per <code>{entityType}</code>. Vai in Customizing → Approval workflows per crearne uno.
          </div>
        ) : (
          <>
            <div className="field"><label>Workflow</label>
              <select value={workflowId} onChange={e => setWorkflowId(e.target.value)}>
                {workflows.map(w => <option key={w.id} value={w.id}>{w.code} · {w.name} ({(w.steps||[]).length} step)</option>)}
              </select>
            </div>
            <div style={{padding: '8px 10px', background: 'var(--bg-2)', borderRadius: 4, fontSize: 11, color: 'var(--text-2)'}}>
              <Icon name="info" size={10}/> Il workflow sarà istanziato con snapshot immutabile della config corrente. Ogni step state sarà tracciato in audit_log.
            </div>
          </>
        )}
      </div>
    </Modal>
  );
}

function WorkflowInstanceDetailModal({ instanceId, focusStepId, onClose, onTransition, user, pushToast }) {
  const { seed, seedCustom, navigate } = useStore();
  const [data, setData] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [acting, setActing] = React.useState(false);
  const [comment, setComment] = React.useState('');
  const [gateBlock, setGateBlock] = React.useState(null); // missing[] dal gate firma documenti

  const load = React.useCallback(() => {
    fetch(`/api/workflow-instances/${encodeURIComponent(instanceId)}`)
      .then(r => r.json())
      .then(json => { if (json.error) setError(json.error); else setData(json.data); })
      .catch(e => setError(String(e?.message || e)));
  }, [instanceId]);
  React.useEffect(() => { load(); }, [load]);

  // FASE 3b Project Cockpit (s105) — focusStepId scroll. Quando la modale
  // viene aperta da WorkflowOrgChart click su una step card, scroll smooth
  // allo step corrispondente + breve highlight per evidenza visiva.
  React.useEffect(() => {
    if (!data || !focusStepId) return;
    const t = setTimeout(() => {
      const el = document.querySelector(`[data-wf-step-id="${focusStepId}"]`);
      if (el) {
        el.scrollIntoView({ behavior: 'smooth', block: 'center' });
        el.classList.add('wf-step-focus-highlight');
        setTimeout(() => el.classList.remove('wf-step-focus-highlight'), 1800);
      }
    }, 50);
    return () => clearTimeout(t);
  }, [data, focusStepId]);

  async function transition(decision) {
    if (acting) return;
    setActing(true);
    try {
      const res = await fetch(`/api/workflow-instances/${encodeURIComponent(instanceId)}/transition`, {
        method: 'POST',
        headers: { 'content-type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify({ stepIndex: data.instance.currentStepIndex, decision, comment: comment.trim() || undefined }),
      });
      const json = await res.json().catch(() => ({}));
      if (!res.ok) {
        // Sessione 100 — blocco gate firma documenti: pannello persistente
        // cliccabile invece di un toast effimero.
        if (json?.error === 'gate_documents_unsigned' && Array.isArray(json.missing)) {
          setGateBlock(json.missing);
        } else {
          pushToast({ title: 'Transizione bloccata', desc: json?.detail || json?.error || `HTTP ${res.status}`, tone: 'err' });
        }
        return;
      }
      setGateBlock(null);
      setData(json.data);
      setComment('');
      onTransition();
      // Una transizione di step cambia i task pending (chi deve gestire lo step
      // successivo): aggiorna il badge "Le mie attività" senza attendere i 60s.
      window.dispatchEvent(new Event('my_tasks_changed'));
      pushToast({ title: `Step ${decision === 'approve' ? 'approvato' : decision === 'reject' ? 'rifiutato' : 'skipped'}`, desc: 'Audit registrato', tone: decision === 'reject' ? 'err' : 'ok' });
    } finally { setActing(false); }
  }

  if (!data && !error) {
    return <Modal open onClose={onClose} title="Caricamento istanza…" size="md" footer={<Btn variant="ghost" size="sm" onClick={onClose}>Chiudi</Btn>}>
      <div style={{padding: 20, textAlign: 'center', fontSize: 11, color: 'var(--text-3)'}}>Caricamento…</div>
    </Modal>;
  }
  if (error) {
    return <Modal open onClose={onClose} title="Errore" size="md" footer={<Btn variant="ghost" size="sm" onClick={onClose}>Chiudi</Btn>}>
      <div style={{padding: 14, color: 'var(--err)', fontSize: 12}}>{error}</div>
    </Modal>;
  }

  const inst = data.instance;
  const steps = data.steps || [];
  // Sessione 100 — per il deep-link ai documenti del gate firma serve il projectId.
  const projectIdForDocs = inst.entityType === 'project' ? inst.entityId : null;
  const activeIdx = inst.currentStepIndex;
  const canAct = inst.status === 'in_progress' && activeIdx !== null && activeIdx >= 0;
  // FASE 16 (sessione 93) — gate-ruoli: lo step si gestisce solo col ruolo
  // approver. isMyTurn=false → bottoni disabilitati (l'enforcement vero è il
  // 403 backend in transition).
  const activeStepSnap = canAct ? (steps[activeIdx]?.stepSnapshot || null) : null;
  const approverRoleId = activeStepSnap?.approver?.kind === 'role' ? activeStepSnap.approver.roleId : null;
  // FASE 16 (sessione 94) — ruoli effettivi = propri ∪ ereditati per delega attiva.
  const myRoleIds = typeof effectiveRoleIdsForUser === 'function'
    ? effectiveRoleIdsForUser(user, seedCustom?.DELEGATIONS, seed?.PERSONAS)
    : (user?.roleIds || []);
  const isMyTurn = !approverRoleId || myRoleIds.includes(approverRoleId);
  // Sessione 100 — oltre al RUOLO bloccante mostra le PERSONE che lo ricoprono.
  const approverPersonas = (seed?.PERSONAS || []).filter(
    (p) => approverRoleId && Array.isArray(p.roleIds) && p.roleIds.includes(approverRoleId),
  );
  const approverPersonaLabel = approverPersonas.map((p) => p.name).join(', ');

  return (
    <Modal open onClose={onClose} title={`Workflow · ${inst.workflowSnapshot?.name || inst.workflowId}`} size="lg" footer={<>
      <Btn variant="ghost" size="sm" onClick={onClose} disabled={acting}>Chiudi</Btn>
      {canAct && <>
        <Btn variant="ghost" size="sm" onClick={() => transition('skip')} disabled={acting || !isMyTurn} title={isMyTurn ? '' : `Riservato al ruolo ${approverRoleId}`}>Skip step</Btn>
        <Btn variant="ghost" size="sm" onClick={() => transition('reject')} disabled={acting || !isMyTurn} title={isMyTurn ? '' : `Riservato al ruolo ${approverRoleId}`}>{acting ? '…' : 'Rifiuta'}</Btn>
        <Btn variant="primary" size="sm" onClick={() => transition('approve')} disabled={acting || !isMyTurn} title={isMyTurn ? '' : `Riservato al ruolo ${approverRoleId}`}>{acting ? '…' : 'Approva step'}</Btn>
      </>}
    </>}>
      {/* FASE 3b Cockpit (s105) — CSS highlight per focusStepId */}
      <style>{`
        .wf-step-focus-highlight {
          box-shadow: 0 0 0 2px var(--accent), 0 0 12px rgba(99, 102, 241, 0.4) !important;
          border-color: var(--accent) !important;
        }
      `}</style>
      <div className="col" style={{gap: 14}}>
        <div className="grid grid-3" style={{fontSize: 11.5}}>
          <div><span style={{color: 'var(--text-3)'}}>Stato istanza</span><br/>
            <Chip kind={inst.status === 'in_progress' ? 'info' : inst.status === 'completed' ? 'ok' : inst.status === 'rejected' ? 'err' : ''} dot>{inst.status}</Chip>
          </div>
          <div><span style={{color: 'var(--text-3)'}}>Avviato</span><br/><span className="mono" style={{fontSize: 11}}>{new Date(inst.startedAt).toLocaleString('it-IT')}</span></div>
          {inst.completedAt && <div><span style={{color: 'var(--text-3)'}}>Chiuso</span><br/><span className="mono" style={{fontSize: 11}}>{new Date(inst.completedAt).toLocaleString('it-IT')}</span></div>}
        </div>

        {canAct && !isMyTurn && (
          <div style={{padding: '8px 10px', border: '1px solid var(--warn)', borderRadius: 4, background: 'var(--bg-2)', fontSize: 11.5, lineHeight: 1.5}}>
            ⚠ Step in attesa del ruolo <strong>{approverRoleId}</strong>
            {approverPersonaLabel
              ? <> — può gestirlo: <strong>{approverPersonaLabel}</strong>.</>
              : <> — <strong>nessun utente ricopre questo ruolo</strong>: lo step resta bloccato finché il ruolo non viene assegnato.</>}
            {' '}Per procedere switcha (in basso a sinistra) su un utente con quel ruolo.
          </div>
        )}
        {canAct && isMyTurn && (
          <div style={{padding: '8px 10px', border: '1px solid var(--ok)', borderRadius: 4, background: 'var(--bg-2)', fontSize: 11.5}}>
            ✓ Tocca a te — hai il ruolo <strong>{approverRoleId || '—'}</strong> per gestire questo step.
          </div>
        )}

        {gateBlock && gateBlock.length > 0 && (
          <div style={{padding: '10px 12px', border: '1px solid var(--warn)', borderRadius: 6, background: 'var(--bg-2)'}}>
            <div style={{fontSize: 11.5, fontWeight: 600, marginBottom: 6}}>✋ Step bloccato — documenti non pronti</div>
            <div className="col" style={{gap: 3}}>
              {gateBlock.map((m, i) => {
                const openable = !!m.documentId && !!projectIdForDocs;
                const label = `${m.title || m.docTypeCode} — ${m.reason === 'absent' ? 'da caricare' : 'da firmare'}`;
                if (openable) {
                  return (
                    <div key={i} className="clickable"
                      onClick={() => { navigate('project_detail', `${projectIdForDocs}|docs|${m.documentId}`); onClose(); }}
                      style={{fontSize: 11.5, color: 'var(--accent)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 6}}>
                      <Icon name="arrow-right" size={11}/> {label}
                    </div>
                  );
                }
                return <div key={i} style={{fontSize: 11.5, color: 'var(--text-2)'}}>• {label}</div>;
              })}
            </div>
            <div style={{fontSize: 10.5, color: 'var(--text-3)', marginTop: 6, lineHeight: 1.5}}>
              Clicca un documento «da firmare» per aprirlo nella scheda Documenti. Firmati i documenti richiesti, torna qui e approva lo step.
            </div>
          </div>
        )}

        <div>
          <div className="eyebrow" style={{marginBottom: 6}}>Step ({steps.length})</div>
          <div className="col" style={{gap: 6}}>
            {steps.map((s, i) => (
              <div
                key={s.id}
                data-wf-step-id={s.id}
                style={{
                padding: '8px 10px',
                border: '1px solid var(--line)',
                borderRadius: 6,
                background: s.status === 'active' ? 'rgba(8,145,178,0.06)' : s.status === 'completed' ? 'rgba(34,197,94,0.04)' : s.status === 'rejected' ? 'rgba(192,57,43,0.06)' : 'var(--bg-2)',
                transition: 'box-shadow 0.5s ease, border-color 0.5s ease',
              }}>
                <div className="row" style={{gap: 8, alignItems: 'center'}}>
                  <span className="mono" style={{fontSize: 11, color: 'var(--text-3)', minWidth: 30}}>#{s.stepOrder}</span>
                  <span style={{fontWeight: 500, fontSize: 12}}>{s.stepName}</span>
                  <span className="spacer"/>
                  <Chip kind={s.status === 'active' ? 'info' : s.status === 'completed' ? 'ok' : s.status === 'rejected' ? 'err' : s.status === 'skipped' ? 'warn' : ''} dot>{s.status}</Chip>
                </div>
                {(s.decisionBy || s.decisionComment) && (
                  <div style={{fontSize: 10.5, color: 'var(--text-3)', marginTop: 4}}>
                    {s.decisionBy && <>by <code>{s.decisionBy}</code></>}
                    {s.decisionComment && <> · «{s.decisionComment}»</>}
                    {s.completedAt && <> · {new Date(s.completedAt).toLocaleString('it-IT')}</>}
                  </div>
                )}
                <div style={{fontSize: 10, color: 'var(--text-3)', marginTop: 2}}>
                  approver: {s.stepSnapshot?.approver?.kind === 'matrix' ? 'matrix dispatch' : (s.stepSnapshot?.approver?.roleId || '—')}
                  {s.stepSnapshot?.slaDays && <> · SLA {s.stepSnapshot.slaDays}g</>}
                  {(s.stepSnapshot?.gates || []).length > 0 && <> · gates: {s.stepSnapshot.gates.join(', ')}</>}
                </div>
              </div>
            ))}
          </div>
        </div>

        {canAct && (
          <div className="field">
            <label style={{fontSize: 10}}>Commento decisione (opzionale)</label>
            <textarea rows={2} value={comment} onChange={e => setComment(e.target.value)} placeholder="Motivazione di approvazione/rifiuto…"/>
          </div>
        )}
      </div>
    </Modal>
  );
}

// FASE 10 — Card Eventi SLA + Notifiche dispatched.
// Mostra: storico eventi SLA per la RdA (warn/breach/recovered) + dispatch
// effettuati (escalation + notification rules matched). Bottone "Valuta ora"
// trigger manuale che POSTa /api/sla/evaluate { entityType, entityId } e
// reloada le liste. Idempotente (dedupKey UNIQUE per giorno).
function SlaEventsCard({ entityType, entityId }) {
  const { user, pushToast } = useStore();
  const [events, setEvents] = React.useState(null);
  const [dispatches, setDispatches] = React.useState(null);
  const [evaluating, setEvaluating] = React.useState(false);

  const reload = React.useCallback(() => {
    setEvents(null); setDispatches(null);
    Promise.all([
      fetch(`/api/sla-events?entityType=${encodeURIComponent(entityType)}&entityId=${encodeURIComponent(entityId)}&limit=20`).then(r => r.json()).catch(() => ({ data: [] })),
      fetch(`/api/notification-dispatch?entityType=${encodeURIComponent(entityType)}&entityId=${encodeURIComponent(entityId)}&limit=30`).then(r => r.json()).catch(() => ({ data: [] })),
    ]).then(([e, d]) => { setEvents(e.data || []); setDispatches(d.data || []); });
  }, [entityType, entityId]);

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

  async function evaluateNow() {
    setEvaluating(true);
    try {
      const res = await fetch('/api/sla/evaluate', {
        method: 'POST',
        headers: { 'content-type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify({ entityType, entityId }),
      });
      const json = await res.json().catch(() => ({}));
      if (!res.ok) {
        pushToast({ title: 'Errore valutazione SLA', desc: json?.error || `HTTP ${res.status}`, tone: 'err' });
        return;
      }
      const newEvents = (json.events || []).filter(e => e.inserted).length;
      const newDispatches = (json.dispatches || []).length;
      if (newEvents === 0) {
        pushToast({ title: 'Nessun crossing rilevato', desc: `Stato SLA: ${json.currStatus ?? 'n/d'}${json.prevStatus ? ' (prima: ' + json.prevStatus + ')' : ''}`, tone: 'info' });
      } else {
        pushToast({ title: 'SLA valutato', desc: `${newEvents} evento/i emessi · ${newDispatches} dispatch creati`, tone: 'ok' });
      }
      reload();
    } catch (err) {
      pushToast({ title: 'Errore valutazione SLA', desc: String(err), tone: 'err' });
    } finally {
      setEvaluating(false);
    }
  }

  function eventChipKind(eventCode) {
    if (eventCode === 'sla.breach') return 'err';
    if (eventCode === 'sla.warn') return 'warn';
    if (eventCode === 'sla.recovered') return 'ok';
    return '';
  }

  return (
    <div className="card flush" style={{border: '1px solid var(--line)', borderRadius: 10, padding: 14, marginTop: 12}}>
      <div className="card-header" style={{padding: 0, marginBottom: 10}}>
        <div className="title"><Icon name="alert" size={12}/> Eventi SLA &amp; Notifiche</div>
        <div className="actions">
          <Btn variant="ghost" size="sm" onClick={evaluateNow} disabled={evaluating}>
            {evaluating ? 'Valutazione…' : 'Valuta ora'}
          </Btn>
        </div>
      </div>

      <div className="col" style={{gap: 12}}>
        <div>
          <div className="eyebrow" style={{marginBottom: 6}}>Storico eventi SLA</div>
          {events === null ? (
            <div style={{fontSize: 11, color: 'var(--text-3)', textAlign: 'center', padding: 8}}>Caricamento…</div>
          ) : events.length === 0 ? (
            <div style={{fontSize: 11, color: 'var(--text-3)', padding: '6px 0'}}>Nessun evento SLA. Premi "Valuta ora" per la prima valutazione.</div>
          ) : (
            <div className="col" style={{gap: 6}}>
              {events.map(e => (
                <div key={e.id} className="row" style={{gap: 8, padding: '6px 8px', background: 'var(--bg-2)', borderRadius: 4, alignItems: 'center'}}>
                  <Chip kind={eventChipKind(e.eventCode)} dot>{e.eventCode}</Chip>
                  <span style={{fontSize: 11.5}}>{e.activeStepName || e.stepType}</span>
                  <span className="spacer"/>
                  <span className="mono" style={{fontSize: 10, color: 'var(--text-3)'}}>{e.elapsedDays.toFixed(1)}gg{e.daysOverdue > 0 ? ` (+${e.daysOverdue.toFixed(1)} overdue)` : ''}</span>
                  <span className="mono" style={{fontSize: 10, color: 'var(--text-3)'}}>{new Date(e.emittedAt).toLocaleString('it-IT', {day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'})}</span>
                </div>
              ))}
            </div>
          )}
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom: 6}}>Dispatch generati ({dispatches?.length ?? '…'})</div>
          {dispatches === null ? (
            <div style={{fontSize: 11, color: 'var(--text-3)', textAlign: 'center', padding: 8}}>Caricamento…</div>
          ) : dispatches.length === 0 ? (
            <div style={{fontSize: 11, color: 'var(--text-3)', padding: '6px 0'}}>Nessuna notifica dispatched.</div>
          ) : (
            <div className="col" style={{gap: 6}}>
              {dispatches.map(d => (
                <div key={d.id} className="col" style={{gap: 4, padding: '6px 8px', background: 'var(--bg-2)', borderRadius: 4}}>
                  <div className="row" style={{gap: 6, alignItems: 'center'}}>
                    <Chip kind={d.ruleKind === 'escalation' ? 'warn' : 'info'} dot>{d.ruleKind} L{d.level}</Chip>
                    <span style={{fontSize: 11.5, fontWeight: 500}}>{d.eventCode}</span>
                    <span className="spacer"/>
                    <Chip kind={d.status === 'sent' ? 'ok' : d.status === 'failed' ? 'err' : ''}>{d.status}</Chip>
                  </div>
                  <div style={{fontSize: 11, color: 'var(--text-2)'}}>
                    <strong>To:</strong> {(d.recipients || []).join(', ') || '—'}
                    {(d.channels || []).length > 0 && <> · <strong>Via:</strong> {(d.channels || []).join(', ')}</>}
                  </div>
                  {d.subject && (
                    <div style={{fontSize: 10.5, color: 'var(--text-3)'}} className="mono">{d.subject}</div>
                  )}
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ============================================================
// Sessione 88 — RdaDetailModal: componente standalone autonomo riusabile.
// Stessa UX del modal in RdA() ma riceve `rda` via prop + `onClose` callback.
// Permette di aprire il detail RdA completo (editing status, allegati,
// checklist server-side, AI bozza, workflow, SLA, waiver) DA QUALSIASI
// pagina senza navigare via. Usato da ProjectDetail tab "RdA collegate".
// ============================================================
// s127 — RDA_ANALYZER: analisi AI della singola RdA. Riusa AiInsightPanel
// (provenance/audit via /api/ai/invoke) con un contesto ricco assemblato lato
// client (testata SAP + posizioni + checklist + allegati). Output markdown →
// renderizzato da AiMarkdown (tabelle + grafici).
const RDA_ANALYZER_SYSTEM = "Sei RDA_ANALYZER, analista procurement CAPEX di una piattaforma di governance degli investimenti industriali. Analizzi UNA Richiesta di Acquisto (RdA): testata, posizioni (righe SAP), completezza checklist (documenti obbligatori/presenti/mancanti) e allegati. Rispondi in italiano, in markdown ricco, conciso e fattuale, 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 (colonne: Riga, Descrizione, Qtà, Prezzo €, Netto €, allineate a destra i numeri con ---:) e, se aiuta la lettura, un GRAFICO a barre della ripartizione 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) **Compliance**: completezza %, documenti mancanti e impatto sull'approvazione. (4) **Azioni consigliate**: lista puntata. Niente preamboli né titolo generale.";

function buildRdaAnalysisPrompt(rda, positions, evalResult, projDocs) {
  const L = [];
  L.push('RICHIESTA DI ACQUISTO da analizzare:');
  L.push(`- Codice: ${rda.id}`);
  L.push(`- Oggetto: ${rda.title || 'n/d'}`);
  L.push(`- Progetto: ${rda.project || 'n/d'}`);
  L.push(`- Vendor: ${rda.vendor || 'n/d'}${rda.vendorCode ? ' (' + rda.vendorCode + ')' : ''}`);
  L.push(`- Gruppo merci: ${rda.materialGroup || 'n/d'}`);
  L.push(`- Importo testata: ${rda.amount ?? 0} €`);
  L.push(`- Urgenza: ${rda.urgency || 'n/d'} · Stato: ${rda.status}`);
  L.push(`- Termini pagamento: ${rda.paymentTerms || 'n/d'} · Consegna richiesta: ${rda.deliveryDate || 'n/d'}`);
  const pos = Array.isArray(positions) ? positions : [];
  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 di dettaglio (RdA flat su importo testata).');
  }
  if (evalResult) {
    L.push('', `CHECKLIST COMPLETEZZA: ${Math.round((evalResult.scoreBp || 0) / 100)}% · blocking: ${evalResult.blocking ? 'sì' : 'no'}`);
    L.push(`- Obbligatori (${(evalResult.requiredCodes || []).length}): ${(evalResult.requiredCodes || []).join(', ') || '—'}`);
    L.push(`- Presenti (${(evalResult.presentCodes || []).length}): ${(evalResult.presentCodes || []).join(', ') || '—'}`);
    L.push(`- Mancanti (${(evalResult.missingCodes || []).length}): ${(evalResult.missingCodes || []).join(', ') || '—'}`);
  } else {
    L.push('', `CHECKLIST: completezza ~${Math.round((rda.completeness || 0) * 100)}%, doc mancanti: ${(rda.missing || []).join(', ') || '—'}`);
  }
  const docs = Array.isArray(projDocs) ? projDocs : [];
  if (docs.length) {
    L.push('', `ALLEGATI DEL PROGETTO (${docs.length}):`);
    docs.slice(0, 25).forEach((d) => L.push(`- ${d.title} [${d.type || 'n/d'}] firma:${d.signatureStatus || 'n/d'}`));
  }
  return L.join('\n');
}

function RdaAiAnalysis({ rda, projDocs, user }) {
  const [data, setData] = React.useState(null);
  React.useEffect(() => {
    let ab = false; setData(null);
    const h = { 'X-Actor-Persona-Id': user?.id || '' };
    Promise.all([
      fetch(`/api/rda/${encodeURIComponent(rda.id)}/positions`, { headers: h }).then((r) => r.json().catch(() => ({}))).then((j) => (Array.isArray(j?.data) ? j.data : [])).catch(() => []),
      fetch(`/api/checklist/evaluate?entityType=rda&entityId=${encodeURIComponent(rda.id)}`, { headers: h }).then((r) => r.json().catch(() => ({}))).then((j) => j?.data?.result || null).catch(() => null),
    ]).then(([positions, evalResult]) => { if (!ab) setData({ positions, evalResult }); });
    return () => { ab = true; };
  }, [rda.id, user?.id]);
  if (data === null) return <div style={{ fontSize: 11, color: 'var(--text-3)', padding: '4px 0' }}>Preparo il contesto…</div>;
  return (
    <AiInsightPanel
      system={RDA_ANALYZER_SYSTEM}
      prompt={buildRdaAnalysisPrompt(rda, data.positions, data.evalResult, projDocs)}
      idleLabel="Analizza RdA con AI"
      emptyHint="Valuta completezza, posizioni e compliance, poi propone azioni."
      runKey={rda.id}
    />
  );
}

function RdaDetailModal({ rda, onClose }) {
  const { pushToast, upsertRda, user, seedCustom, navigate, upsertOda } = useStore();
  // localRda riflette gli update post-PATCH così il modal mostra sempre lo
  // stato più recente. Sync con prop quando cambia.
  const [localRda, setLocalRda] = React.useState(rda);
  React.useEffect(() => { setLocalRda(rda); }, [rda?.id]);
  // s126 Task 1 — carica i documenti del progetto della RdA (per il viewer allegati).
  React.useEffect(() => {
    let aborted = false;
    setProjDocs(null); setViewerDoc(null);
    const pid = rda?.project;
    if (!pid) { setProjDocs([]); return; }
    fetch('/api/documents/list?hasFile=true&limit=300', { headers: { 'X-Actor-Persona-Id': user?.id || '' } })
      .then((r) => r.json().catch(() => ({})))
      .then((j) => {
        if (aborted) return;
        const all = Array.isArray(j?.data) ? j.data : [];
        setProjDocs(all.filter((d) => d.projectId === pid));
      })
      .catch(() => { if (!aborted) setProjDocs([]); });
    return () => { aborted = true; };
  }, [rda?.id, rda?.project, user?.id]);
  const sel = localRda;

  const [statusSaving, setStatusSaving] = React.useState(null);
  const [serverEval, setServerEval] = React.useState(null);
  const [evalLoading, setEvalLoading] = React.useState(false);
  const [whyDrawerOpen, setWhyDrawerOpen] = React.useState(false);
  const [rulesCatalog, setRulesCatalog] = React.useState(null);
  const [waiverForm, setWaiverForm] = React.useState(null);
  const [waiverSaving, setWaiverSaving] = React.useState(false);
  const [viewer, setViewer] = React.useState(null);
  // FASE 10.5 — "Genera OdA" dalla RdA approvata (po.create gating).
  const [generatingOda, setGeneratingOda] = React.useState(false);
  // s126 — mini-wizard "Genera OdA": prefillato dalla RdA, completa i campi OdA-specifici.
  const [showOdaWiz, setShowOdaWiz] = React.useState(false);
  const [odaWiz, setOdaWiz] = React.useState(null);
  // s126 Task 1 — allegati documentali del progetto della RdA + viewer inline.
  const [projDocs, setProjDocs] = React.useState(null);
  const [viewerDoc, setViewerDoc] = React.useState(null);
  const canGenerateOda = window.can('po.create', user, seedCustom);
  // Fix s124: questo modal standalone è riusato da ProjectDetail → RdaTab.
  // Deve dichiarare i can* RBAC dei bottoni footer/waiver, altrimenti
  // ReferenceError in render → pagina nera all'apertura. Mirror della pagina RdA.
  const canSubmitRda  = window.can('rda.submit', user, seedCustom);
  const canApproveRda = window.can('rda.approve', user, seedCustom);
  const canRejectRda  = window.can('rda.reject', user, seedCustom);
  const canWaive      = window.can('doc.waive', user, seedCustom);
  // s126 — edit testata RdA nel dettaglio (gated rda.edit + freeze stati terminali).
  const canEditRda    = window.can('rda.edit', user, seedCustom);
  const rdaFrozen     = RDA_FROZEN_STATUSES.includes(sel?.status);
  const [editingHdr, setEditingHdr] = React.useState(false);
  const [hdr, setHdr] = React.useState(null);
  const [hdrSaving, setHdrSaving] = React.useState(false);
  const startEditHdr = () => {
    setHdr({
      title: sel.title || '', vendor: sel.vendor || '', vendorCode: sel.vendorCode || '',
      materialGroup: sel.materialGroup || '', urgency: sel.urgency || 'normale',
      deliveryDate: sel.deliveryDate || '', paymentTerms: sel.paymentTerms || '',
      amount: String(sel.amount ?? ''),
    });
    setEditingHdr(true);
  };
  const setH = (k, v) => setHdr((s) => ({ ...s, [k]: v }));
  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, materialGroup: hdr.materialGroup.trim() || null,
        urgency: hdr.urgency, deliveryDate: hdr.deliveryDate || null,
        paymentTerms: hdr.paymentTerms.trim() || null, amount: Number(hdr.amount) || 0,
      };
      const r = await fetch(`/api/rda/${encodeURIComponent(sel.id)}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', '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; }
      upsertRda(j.data); setLocalRda(j.data); setEditingHdr(false);
      pushToast({ title: 'RdA aggiornata', desc: 'Testata salvata. Audit log registrato.', tone: 'ok' });
    } catch (err) { pushToast({ title: 'Errore di rete', desc: err?.message || 'PATCH fallita', tone: 'err' }); }
    finally { setHdrSaving(false); }
  };
  const setW = (k, v) => setOdaWiz((s) => ({ ...s, [k]: v }));
  const openOdaWiz = () => {
    const today = new Date().toISOString().slice(0, 10);
    setOdaWiz({
      vendorCode: sel.vendorCode || '', docType: 'NB', orderDate: today,
      expectedDelivery: sel.deliveryDate || '', paymentTerms: sel.paymentTerms || '',
      incoterms: '', incotermsLocation: '',
    });
    setShowOdaWiz(true);
  };
  const generateOda = async (overrides) => {
    if (!sel || generatingOda) return;
    setGeneratingOda(true);
    try {
      const res = await fetch(`/api/rda/${encodeURIComponent(sel.id)}/generate-oda`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify(overrides || {}),
      });
      const j = await res.json().catch(() => null);
      if (!res.ok) {
        pushToast({ title: 'Genera OdA fallita', desc: j?.detail || j?.error || `HTTP ${res.status}`, tone: 'err' });
        return;
      }
      if (j?.data && upsertOda) upsertOda(j.data);
      setShowOdaWiz(false);
      pushToast({
        title: 'OdA generata',
        desc: `${j.data.id}${j.workflowStarted ? ' · workflow ' + j.workflowStarted + ' avviato' : ''}`,
        tone: 'ok',
      });
      if (navigate) navigate('oda', j.data.id);
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'Genera OdA fallita', tone: 'err' });
    } finally {
      setGeneratingOda(false);
    }
  };

  // Fetch checklist server-side eval al mount + quando cambia rda.id
  React.useEffect(() => {
    if (!sel) { setServerEval(null); return; }
    let cancelled = false;
    setEvalLoading(true);
    (async () => {
      try {
        const url = `/api/checklist/evaluate?entityType=rda&entityId=${encodeURIComponent(sel.id)}`;
        const r = await fetch(url, { headers: { 'X-Actor-Persona-Id': user?.id || '' } });
        if (!r.ok) { if (!cancelled) setServerEval(null); return; }
        const j = await r.json();
        if (cancelled) return;
        setServerEval(j.data || null);
      } catch { if (!cancelled) setServerEval(null); }
      finally { if (!cancelled) setEvalLoading(false); }
    })();
    return () => { cancelled = true; };
  }, [sel?.id, user?.id]);

  // Catalog rules per drawer "Perché"
  React.useEffect(() => {
    if (!whyDrawerOpen || rulesCatalog) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch('/api/config/checklist-rules', { headers: { 'X-Actor-Persona-Id': user?.id || '' } });
        if (!r.ok) return;
        const j = await r.json();
        if (!cancelled) setRulesCatalog(j.data || []);
      } catch {}
    })();
    return () => { cancelled = true; };
  }, [whyDrawerOpen, user?.id]);

  const recomputeChecklist = async () => {
    if (!sel) return;
    setEvalLoading(true);
    try {
      const res = await fetch('/api/checklist/evaluate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ entityType: 'rda', entityId: sel.id, triggerSource: 'manual', persist: true }),
      });
      const json = await res.json().catch(() => null);
      if (!res.ok) {
        pushToast({ title: 'Ricalcolo checklist fallito', desc: json?.error || `HTTP ${res.status}`, tone: 'err' });
        return;
      }
      setServerEval(json.data);
      const score = Math.round((json.data?.result?.scoreBp ?? 0) / 100);
      pushToast({
        title: 'Checklist ricalcolata',
        desc: `${sel.id} · completezza ${score}% · ${json.data.result.missingCodes.length} doc mancanti`,
        tone: json.data.result.blocking ? 'warn' : 'ok',
      });
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'Ricalcolo fallito', tone: 'err' });
    } finally { setEvalLoading(false); }
  };

  const submitWaiver = async () => {
    if (!waiverForm || !sel) return;
    if (!waiverForm.justification || waiverForm.justification.length < 10) {
      pushToast({ title: 'Giustificazione troppo corta', desc: 'Min 10 caratteri richiesti.', tone: 'err' });
      return;
    }
    setWaiverSaving(true);
    try {
      const r = await fetch('/api/checklist-exceptions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({
          entityType: 'rda', entityId: sel.id, docCode: waiverForm.docCode,
          reason: waiverForm.reason, justification: waiverForm.justification,
          expiresAt: waiverForm.expiresAt || null,
        }),
      });
      const j = await r.json();
      if (!r.ok) { pushToast({ title: 'Waiver fallito', desc: j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      pushToast({ title: 'Waiver creato', desc: `${waiverForm.docCode} → ${waiverForm.reason}`, tone: 'ok' });
      setWaiverForm(null);
      await recomputeChecklist();
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'POST fallito', tone: 'err' });
    } finally { setWaiverSaving(false); }
  };

  const revokeWaiver = async (exceptionId) => {
    if (!confirm('Revocare questo waiver? Il documento tornerà obbligatorio.')) return;
    try {
      const r = await fetch(`/api/checklist-exceptions/${encodeURIComponent(exceptionId)}`, {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ revokedReason: 'Manual revoke from RdA drawer' }),
      });
      if (!r.ok) { const j = await r.json().catch(() => ({})); pushToast({ title: 'Revoca fallita', desc: j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      pushToast({ title: 'Waiver revocato', desc: exceptionId, tone: 'warn' });
      await recomputeChecklist();
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'DELETE fallito', tone: 'err' });
    }
  };

  const changeStatus = async (newStatus) => {
    if (!sel || statusSaving) return;
    setStatusSaving(newStatus);
    try {
      const res = await fetch(`/api/rda/${sel.id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ status: newStatus }),
      });
      const json = await res.json().catch(() => null);
      if (!res.ok) {
        const msg = json?.error === 'validation_error'
          ? (json.issues?.map(i => `${i.path?.join('.') || 'campo'}: ${i.message}`).join(' · ') || 'Validazione fallita')
          : (json?.error || `Errore HTTP ${res.status}`);
        pushToast({ title: 'Cambio stato fallito', desc: msg, tone: 'err' });
        setStatusSaving(null);
        return;
      }
      const updated = json.data;
      upsertRda(updated);
      setLocalRda(updated);
      const labels = { in_review: 'In review', approved: 'Approvata', rejected: 'Rifiutata' };
      pushToast({ title: `RdA ${labels[newStatus] || newStatus}`, desc: `${updated.id} → status: ${updated.status}. Audit log registrato.`, tone: newStatus === 'rejected' ? 'warn' : 'ok' });
      setStatusSaving(null);
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'PATCH fallita', tone: 'err' });
      setStatusSaving(null);
    }
  };

  if (!sel) return null;

  return (
    <>
      <Modal open={!!sel} onClose={onClose} title={sel.id + ' · ' + sel.title} size="lg" footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose} disabled={!!statusSaving}>Chiudi</Btn>
          <Btn variant="ghost" size="sm" onClick={() => setViewer({ ...sel, type: 'rda' })} disabled={!!statusSaving}><Icon name="eye" size={12}/> Apri PDF</Btn>
          <Btn variant="ghost" size="sm" onClick={() => { window.open(`/api/rda/${encodeURIComponent(sel.id)}/zip`, '_blank'); pushToast({ title: 'Generazione ZIP avviata', desc: 'Lo scaricamento parte appena pronto.', tone: 'info' }); }} disabled={!!statusSaving} title="Scarica ZIP con tutti i documenti del progetto associato">
            <Icon name="download" size={12}/> Scarica ZIP allegati
          </Btn>
          <Btn variant="ai" size="sm" onClick={() => pushToast({ title: 'Email follow-up generata', desc: 'Bozza pronta nell\'editor.', tone: 'ok' })} disabled={!!statusSaving}><Icon name="sparkle" size={12}/> Genera email fornitore</Btn>
          <span className="spacer"/>
          {sel.status === 'draft' && (
            <Btn variant="ghost" size="sm" onClick={() => { if (canSubmitRda) changeStatus('in_review'); }} disabled={!!statusSaving || !canSubmitRda} title={canSubmitRda ? undefined : window.whyDisabled('rda.submit')}>{statusSaving === 'in_review' ? 'Salvataggio…' : 'Metti in review'}</Btn>
          )}
          {sel.status !== 'rejected' && (
            <Btn variant="ghost" size="sm" onClick={() => { if (canRejectRda) changeStatus('rejected'); }} disabled={!!statusSaving || !canRejectRda} title={canRejectRda ? undefined : window.whyDisabled('rda.reject')}>{statusSaving === 'rejected' ? 'Salvataggio…' : 'Rifiuta'}</Btn>
          )}
          {sel.status !== 'approved' && (
            <Btn variant={canApproveRda ? 'primary' : 'ghost'} size="sm" onClick={() => { if (canApproveRda) changeStatus('approved'); }} disabled={!!statusSaving || !canApproveRda} title={canApproveRda ? undefined : window.whyDisabled('rda.approve')}>{statusSaving === 'approved' ? 'Salvataggio…' : 'Approva'}</Btn>
          )}
          {sel.status === 'approved' && (
            <Btn variant={canGenerateOda ? 'primary' : 'ghost'} size="sm" onClick={() => { if (canGenerateOda) openOdaWiz(); }} disabled={generatingOda || !canGenerateOda} title={canGenerateOda ? 'Genera un Ordine di Acquisto da questa RdA approvata' : window.whyDisabled('po.create')}>
              <Icon name="rda" size={12}/> {generatingOda ? 'Generazione…' : 'Genera OdA'}
            </Btn>
          )}
        </>
      }>
        <div className="col" style={{ gap: 14 }}>
          {!editingHdr ? (
            <div className="col" style={{ gap: 10 }}>
              <div className="grid grid-3">
                <div><div className="eyebrow">Progetto</div><div style={{ fontWeight: 500 }}>{sel.project}</div></div>
                <div><div className="eyebrow">Vendor</div><div style={{ fontWeight: 500 }}>{sel.vendor}{sel.vendorCode ? <span className="mono" style={{ fontSize: 10, color: 'var(--text-3)', marginLeft: 6 }}>{sel.vendorCode}</span> : null}</div></div>
                <div><div className="eyebrow">Importo</div><div style={{ fontFamily: 'var(--font-display)', fontSize: 22 }}>{fmtEUR(sel.amount, true)}</div></div>
              </div>
              <div className="grid grid-3">
                <div><div className="eyebrow">Gruppo merci</div><div>{sel.materialGroup || '—'}</div></div>
                <div><div className="eyebrow">Urgenza</div><div>{sel.urgency || 'normale'}</div></div>
                <div><div className="eyebrow">Consegna richiesta</div><div>{sel.deliveryDate || '—'}</div></div>
              </div>
              {canEditRda && (
                <div className="row" style={{ justifyContent: 'flex-end' }}>
                  <Btn variant="ghost" size="xs" onClick={startEditHdr} disabled={rdaFrozen} title={rdaFrozen ? `RdA in stato "${sel.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 RdA</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>Gruppo merci</label><input value={hdr.materialGroup} onChange={(e) => setH('materialGroup', e.target.value)} /></div>
                <div className="field"><label>Urgenza</label><select value={hdr.urgency} onChange={(e) => setH('urgency', e.target.value)}>{RDA_URGENCY.map((u) => <option key={u} value={u}>{u}</option>)}</select></div>
                <div className="field"><label>Consegna richiesta</label><input type="date" value={hdr.deliveryDate} onChange={(e) => setH('deliveryDate', 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>Importo €</label><input type="number" value={hdr.amount} onChange={(e) => setH('amount', e.target.value)} /></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>
          )}

          {/* Posizioni RdA (testata + righe) */}
          <RdaPositionsSection rda={sel} user={user} seedCustom={seedCustom} pushToast={pushToast} />
          <div>
            <div className="row" style={{ justifyContent: 'space-between', alignItems: 'center' }}>
              <div className="eyebrow">Completezza checklist {serverEval ? '(server-side)' : 'AI'}</div>
              <Btn variant="ghost" size="xs" onClick={recomputeChecklist} disabled={evalLoading} data-testid="rda-recompute-checklist">
                <Icon name="refresh" size={11}/> {evalLoading ? 'Ricalcolo…' : 'Ricalcola'}
              </Btn>
            </div>
            {(() => {
              const useServer = !!serverEval?.result;
              const sr = serverEval?.result;
              const scorePct = useServer ? Math.round((sr.scoreBp / 100)) : Math.round((sel.completeness || 0) * 100);
              const missing = useServer ? sr.missingCodes : (sel.missing || []);
              const blocking = useServer ? sr.blocking : missing.length > 0;
              return (
                <>
                  <Meter value={scorePct} tone={!blocking ? 'ok' : scorePct > 80 ? 'warn' : 'err'} thick />
                  {useServer && (
                    <div className="row" style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 4, justifyContent: 'space-between', alignItems: 'center' }} data-testid="rda-eval-source">
                      <span>{sr.matchedRuleIds.length} regole matchate · {sr.requiredCodes.length} obbligatori · {sr.optionalCodes.length} opzionali · {sr.presentCodes.length} presenti</span>
                      <Btn variant="ghost" size="xs" onClick={() => setWhyDrawerOpen(true)} data-testid="rda-why-btn"><Icon name="help" size={10}/> Perché questi doc?</Btn>
                    </div>
                  )}
                  {missing.length > 0 ? (
                    <div style={{ marginTop: 10 }}>
                      <div style={{ fontSize: 12, fontWeight: 500, marginBottom: 6 }}>Allegati mancanti</div>
                      {missing.map((m, i) => (
                        <div key={i} className="row" style={{ gap: 8, padding: '6px 0', borderBottom: '1px dashed var(--line)' }}>
                          <Icon name="warning_tri" size={13} /> <span className="mono" style={{ fontSize: 12.5 }}>{m}</span>
                        </div>
                      ))}
                    </div>
                  ) : (
                    <div style={{ marginTop: 10, fontSize: 12.5, color: 'var(--ok)' }} data-testid="rda-checklist-complete">
                      <Icon name="check" size={12}/> Tutti gli allegati presenti e validati
                    </div>
                  )}
                </>
              );
            })()}
          </div>
          <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 della RdA</div></div>
            <div className="card-body">
              <RdaAiAnalysis rda={sel} projDocs={projDocs} user={user} />
            </div>
          </div>
          <div className="card flush" style={{ border: '1px solid var(--line)', borderRadius: 10 }}>
            <div className="card-header"><div className="title"><Icon name="sparkle" size={12}/> Bozza email AI per follow-up</div></div>
            <div className="card-body" style={{ fontSize: 12.5, lineHeight: 1.6 }}>
              <div style={{ marginBottom: 6, color: 'var(--text-2)' }}>Oggetto: Completamento documentazione {sel.id}</div>
              <div>Gentile team {sel.vendor},</div>
              <div style={{ marginTop: 6 }}>in riferimento alla richiesta di acquisto {sel.id} relativa al progetto {sel.project}, vi chiediamo cortesemente di fornirci entro il {fmtDate(new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString())} i seguenti documenti:</div>
              <ul style={{ marginTop: 4 }}>{(() => {
                const m = serverEval?.result?.missingCodes ?? sel.missing ?? [];
                return (m.length ? m : ['—']).map((x,i) => <li key={i}>{x}</li>);
              })()}</ul>
              <div style={{ marginTop: 6 }}>Cordiali saluti,<br/>Procurement</div>
            </div>
          </div>
          <div>
            <div className="eyebrow" style={{ marginBottom: 8 }}>Allegati RdA</div>
            {projDocs === null ? (
              <div style={{ fontSize: 11, color: 'var(--text-3)' }}>Caricamento allegati…</div>
            ) : projDocs.length === 0 ? (
              <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 al progetto di questa RdA.
              </div>
            ) : (
              <div style={{ border: '1px solid var(--line)', borderRadius: 8, overflow: 'hidden' }}>
                {projDocs.map((dd, i) => {
                  const open = viewerDoc && viewerDoc.id === dd.id;
                  return (
                    <div key={dd.id} className="row" style={{ gap: 8, padding: '7px 10px', alignItems: 'center', borderBottom: i < projDocs.length - 1 ? '1px solid var(--line)' : 'none', background: open ? 'var(--bg-2)' : 'transparent' }}>
                      <Icon name="file_pdf" size={13}/>
                      <span style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ fontSize: 12, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{dd.originalFilename || dd.title || dd.id}</div>
                        <div style={{ fontSize: 10, color: 'var(--text-3)' }}>{dd.type}{dd.fileVersion ? ` · v${dd.fileVersion}` : ''}{dd.signatureStatus === 'signed' ? ' · firmato' : ''}</div>
                      </span>
                      <Btn variant={open ? 'primary' : 'ghost'} size="xs" onClick={() => setViewerDoc(open ? null : { id: dd.id, mimeType: dd.mimeType, filename: dd.originalFilename || dd.title })}>
                        <Icon name="eye" size={11}/> {open ? 'Chiudi' : 'Visualizza'}
                      </Btn>
                    </div>
                  );
                })}
              </div>
            )}
            {viewerDoc && window.DocViewerPane && (
              <div style={{ marginTop: 8, height: 480, border: '1px solid var(--line)', borderRadius: 8, overflow: 'hidden', display: 'flex' }}>
                <window.DocViewerPane docId={viewerDoc.id} mimeType={viewerDoc.mimeType} filename={viewerDoc.filename} user={user} />
              </div>
            )}
            <div style={{ marginTop: 10 }}>
              <Uploader compact title="Carica allegati mancanti" sub="PDF · il fornitore può anche inviarli via portale" allowedTypes={['offerta','specifica','capitolato','contratto','checklist']} />
            </div>
          </div>
          <WorkflowInstancesCard entityType="rda" entityId={sel.id}/>
          <SlaEventsCard entityType="rda" entityId={sel.id}/>
        </div>
      </Modal>

      <PDFViewer open={!!viewer} doc={viewer} onClose={() => setViewer(null)} />

      {whyDrawerOpen && (
        <div onClick={() => setWhyDrawerOpen(false)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 200 }} data-testid="rda-why-drawer-backdrop">
          <div onClick={(e) => e.stopPropagation()} style={{ position: 'fixed', right: 0, top: 0, bottom: 0, width: 640, background: 'var(--bg)', borderLeft: '1px solid var(--line)', boxShadow: '-8px 0 24px rgba(0,0,0,0.3)', overflowY: 'auto', padding: 24, animation: 'slideInRight 0.2s ease-out' }} data-testid="rda-why-drawer">
            <div className="row" style={{ justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 20 }}>
              <div><div className="eyebrow">Perché questi documenti?</div><h2 style={{ fontSize: 18, marginTop: 4 }}>Explain checklist rules</h2></div>
              <Btn variant="ghost" size="sm" onClick={() => setWhyDrawerOpen(false)} data-testid="rda-why-close"><Icon name="close" size={11}/></Btn>
            </div>
            {!serverEval?.result ? (
              <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Nessuna evaluation server-side disponibile.</div>
            ) : (
              <>
                <div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 16, padding: 12, background: 'var(--bg-2)', borderRadius: 8 }}>
                  Il motore checklist server-side ha valutato l'RdA <code className="mono">{sel.id}</code> contro {serverEval.result.stats.rulesEvaluated} regole attive. <b>{serverEval.result.matchedRuleIds.length}</b> regole hanno matchato.
                </div>
                <div className="eyebrow" style={{ marginBottom: 8 }}>Regole matchate ({serverEval.result.matchedRuleIds.length})</div>
                {!rulesCatalog ? (
                  <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Caricamento regole…</div>
                ) : serverEval.result.matchedRuleIds.length === 0 ? (
                  <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Nessuna regola matchata.</div>
                ) : (
                  serverEval.result.matchedRuleIds.map((ruleId) => {
                    const rule = rulesCatalog.find(r => r.id === ruleId);
                    if (!rule) return <div key={ruleId} style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 6, marginBottom: 8, fontSize: 11.5 }}><div className="mono">{ruleId}</div><div style={{ color: 'var(--text-3)' }}>Rule non più disponibile.</div></div>;
                    return (
                      <div key={ruleId} style={{ padding: 12, border: '1px solid var(--line)', borderRadius: 8, marginBottom: 10 }} data-testid={`rda-why-rule-${rule.id}`}>
                        <div className="row" style={{ justifyContent: 'space-between', alignItems: 'flex-start' }}>
                          <div><div style={{ fontWeight: 600, fontSize: 13 }}>{rule.name}</div><div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{rule.id} · priority {rule.priority}</div></div>
                          <Chip kind="info" dot>{rule.entityType}</Chip>
                        </div>
                        <div style={{ marginTop: 8 }}>
                          <div className="eyebrow" style={{ fontSize: 10 }}>Condizione</div>
                          <pre style={{ fontSize: 10.5, background: 'var(--bg-2)', padding: 8, borderRadius: 4, overflowX: 'auto', marginTop: 4, lineHeight: 1.5 }}>{JSON.stringify(rule.conditions, null, 2)}</pre>
                        </div>
                        {rule.required && rule.required.length > 0 && (
                          <div style={{ marginTop: 8 }}>
                            <div className="eyebrow" style={{ fontSize: 10 }}>Documenti obbligatori che genera</div>
                            <div className="row" style={{ gap: 4, flexWrap: 'wrap', marginTop: 4 }}>{rule.required.map(c => <Chip key={c} kind={serverEval.result.presentCodes.includes(c) ? 'ok' : 'warn'} dot>{c} {serverEval.result.presentCodes.includes(c) ? '✓' : '!'}</Chip>)}</div>
                          </div>
                        )}
                        {rule.optional && rule.optional.length > 0 && (
                          <div style={{ marginTop: 8 }}>
                            <div className="eyebrow" style={{ fontSize: 10 }}>Documenti opzionali</div>
                            <div className="row" style={{ gap: 4, flexWrap: 'wrap', marginTop: 4 }}>{rule.optional.map(c => <Chip key={c}>{c}</Chip>)}</div>
                          </div>
                        )}
                      </div>
                    );
                  })
                )}
                <div className="eyebrow" style={{ marginTop: 20, marginBottom: 8 }}>Set finale documenti</div>
                <div className="grid grid-2" style={{ gap: 10 }}>
                  <div style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 6 }}>
                    <div className="eyebrow" style={{ fontSize: 10 }}>Obbligatori ({serverEval.result.requiredCodes.length})</div>
                    <div className="row" style={{ gap: 3, flexWrap: 'wrap', marginTop: 4 }}>{serverEval.result.requiredCodes.map(c => <Chip key={c} kind={serverEval.result.presentCodes.includes(c) ? 'ok' : 'warn'} dot>{c}</Chip>)}</div>
                  </div>
                  <div style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 6 }}>
                    <div className="eyebrow" style={{ fontSize: 10 }}>Mancanti ({serverEval.result.missingCodes.length})</div>
                    <div className="row" style={{ gap: 3, flexWrap: 'wrap', marginTop: 4 }}>
                      {serverEval.result.missingCodes.length === 0 ? <span style={{ fontSize: 11, color: 'var(--ok)' }}>✓ tutto presente</span> : serverEval.result.missingCodes.map(c => (
                        <div key={c} className="row" style={{ gap: 4, alignItems: 'center' }}>
                          <Chip kind="err" dot>{c}</Chip>
                          <Btn variant="ghost" size="xs" disabled={!canWaive} onClick={() => { if (canWaive) setWaiverForm({ docCode: c, reason: 'not_applicable', justification: '', expiresAt: '' }); }} title={canWaive ? undefined : window.whyDisabled('doc.waive')} data-testid={`rda-waiver-btn-${c}`}>Waivera</Btn>
                        </div>
                      ))}
                    </div>
                  </div>
                </div>
                {serverEval.result.waivedCodes && serverEval.result.waivedCodes.length > 0 && (
                  <>
                    <div className="eyebrow" style={{ marginTop: 20, marginBottom: 8 }}>Documenti waivati ({serverEval.result.waivedCodes.length})</div>
                    {serverEval.result.waivers.map((w) => (
                      <div key={w.exceptionId} style={{ padding: 10, background: 'rgba(220,180,80,0.08)', border: '1px solid var(--warn)', borderRadius: 6, marginBottom: 6 }} data-testid={`rda-waiver-${w.exceptionId}`}>
                        <div className="row" style={{ justifyContent: 'space-between', alignItems: 'flex-start' }}>
                          <div style={{ flex: 1 }}>
                            <div className="row" style={{ gap: 6 }}><Chip kind="warn" dot>{w.docCode}</Chip><span style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{w.reason}</span></div>
                            <div style={{ fontSize: 11, color: 'var(--text-2)', marginTop: 4 }}>{w.justification}</div>
                            <div className="mono" style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 4 }}>{w.exceptionId} · {new Date(w.approvedAt).toLocaleDateString('it-IT')}{w.expiresAt ? ` · scade ${new Date(w.expiresAt).toLocaleDateString('it-IT')}` : ' · no expiry'}</div>
                          </div>
                          <Btn variant="ghost" size="xs" onClick={() => revokeWaiver(w.exceptionId)} data-testid={`rda-waiver-revoke-${w.exceptionId}`}>Revoca</Btn>
                        </div>
                      </div>
                    ))}
                  </>
                )}
              </>
            )}
          </div>
        </div>
      )}

      {waiverForm && (
        <Modal open={!!waiverForm} onClose={() => setWaiverForm(null)} title={`Waivera documento — ${waiverForm.docCode}`} size="md" footer={
          <>
            <Btn variant="ghost" size="sm" onClick={() => setWaiverForm(null)}>Annulla</Btn>
            <Btn variant="primary" size="sm" onClick={submitWaiver} disabled={waiverSaving} data-testid="rda-waiver-submit">
              <Icon name="check" size={11}/> {waiverSaving ? 'Salvataggio…' : 'Crea waiver'}
            </Btn>
          </>
        }>
          <div className="col" style={{ gap: 10 }}>
            <div style={{ fontSize: 12, color: 'var(--text-2)' }}>Stai creando un'eccezione checklist per <code className="mono">{waiverForm.docCode}</code> della RdA <code className="mono">{sel?.id}</code>.</div>
            <div className="field"><label>Motivo *</label>
              <select value={waiverForm.reason} onChange={(e) => setWaiverForm({ ...waiverForm, reason: e.target.value })} data-testid="rda-waiver-reason">
                <option value="not_applicable">Non applicabile</option>
                <option value="replaced_by_alternative">Sostituito da alternativa</option>
                <option value="one_off">Caso una tantum</option>
                <option value="pending_external">In attesa di esterno</option>
              </select>
            </div>
            <div className="field"><label>Giustificazione * (≥ 10 char)</label>
              <textarea value={waiverForm.justification} onChange={(e) => setWaiverForm({ ...waiverForm, justification: e.target.value })} rows={3} placeholder="es. Documento non richiesto per progetti sotto soglia 50k€" data-testid="rda-waiver-justification"/>
            </div>
            <div className="field"><label>Scadenza waiver (opzionale)</label>
              <input type="datetime-local" value={waiverForm.expiresAt} onChange={(e) => setWaiverForm({ ...waiverForm, expiresAt: e.target.value ? new Date(e.target.value).toISOString() : '' })}/>
            </div>
          </div>
        </Modal>
      )}

      {showOdaWiz && odaWiz && (
        <Modal open={showOdaWiz} onClose={() => { if (!generatingOda) setShowOdaWiz(false); }} size="md" title={`Genera OdA da ${sel.id}`} footer={
          <>
            <Btn variant="ghost" size="sm" onClick={() => setShowOdaWiz(false)} disabled={generatingOda}>Annulla</Btn>
            <span className="spacer"/>
            <Btn variant="primary" size="sm" onClick={() => generateOda(odaWiz)} disabled={generatingOda}>
              <Icon name="rda" size={12}/> {generatingOda ? 'Generazione…' : 'Crea OdA'}
            </Btn>
          </>
        }>
          <div className="col" style={{ gap: 12 }}>
            <div style={{ fontSize: 11.5, color: 'var(--text-2)', padding: 10, background: 'var(--bg-2)', borderRadius: 8 }}>
              Vendor <strong>{sel.vendor}</strong> · importo <strong>{fmtEUR(sel.amount, true)}</strong>. Le righe della RdA vengono copiate nell'OdA. Completa i dati OdA-specifici:
            </div>
            <div className="grid grid-2" style={{ gap: 10 }}>
              <div className="field"><label>Tipo documento</label><input value={odaWiz.docType} onChange={(e) => setW('docType', e.target.value)} placeholder="es. NB"/></div>
              <div className="field"><label>Codice vendor</label><input className="mono" value={odaWiz.vendorCode} onChange={(e) => setW('vendorCode', e.target.value)} placeholder="LIFNR"/></div>
              <div className="field"><label>Data ordine</label><input type="date" value={odaWiz.orderDate} onChange={(e) => setW('orderDate', e.target.value)}/></div>
              <div className="field"><label>Consegna prevista</label><input type="date" value={odaWiz.expectedDelivery} onChange={(e) => setW('expectedDelivery', e.target.value)}/></div>
              <div className="field"><label>Termini pagamento</label><input value={odaWiz.paymentTerms} onChange={(e) => setW('paymentTerms', e.target.value)}/></div>
              <div className="field"><label>Incoterms</label><input value={odaWiz.incoterms} onChange={(e) => setW('incoterms', e.target.value)} placeholder="es. DAP"/></div>
              <div className="field" style={{ gridColumn: 'span 2' }}><label>Luogo resa</label><input value={odaWiz.incotermsLocation} onChange={(e) => setW('incotermsLocation', e.target.value)} placeholder="es. Milano"/></div>
            </div>
          </div>
        </Modal>
      )}
    </>
  );
}

// ============================================================
// RdaPositionsSection — posizioni (righe) della RdA. Autonoma: fetcha le proprie
// righe da /api/rda/[id]/positions. Mirror di OdaPositionsSection (s121).
// ============================================================
const RDA_FROZEN_STATUSES = ['approved', 'rejected', 'cancelled'];
function rdaEur2(n) {
  if (typeof window !== 'undefined' && window.eur2) return window.eur2(n);
  return '€ ' + Number(n || 0).toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function RdaPositionsSection({ rda, user, seedCustom, pushToast }) {
  const [positions, setPositions] = React.useState(null); // null = loading
  const [err, setErr] = React.useState(null);
  const [form, setForm] = React.useState(null); // { mode, pos }
  const frozen = RDA_FROZEN_STATUSES.includes(rda.status);
  const canEdit = (typeof window !== 'undefined' && window.can ? window.can('rda.edit', user, seedCustom) : true) && !frozen;
  const whyDisabled = frozen
    ? `RdA in stato "${rda.status}": righe congelate`
    : (typeof window !== 'undefined' && window.whyDisabled ? window.whyDisabled('rda.edit') : 'Permesso mancante');

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

  const sumNet = (positions || []).reduce((a, p) => a + Number(p.netValueEur || 0), 0);

  const removePosition = async (pos) => {
    if (!canEdit) return;
    if (!window.confirm(`Eliminare la riga ${pos.lineNo} — ${pos.description}?`)) return;
    try {
      const r = await fetch(`/api/rda/${encodeURIComponent(rda.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' });
      reload();
    } 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' : whyDisabled}
          onClick={() => { if (canEdit) setForm({ mode: 'create' }); }} data-testid="rda-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.' : ''}</div>
      ) : (
        <table className="tbl" data-testid="rda-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>{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">{rdaEur2(p.unitPriceEur)}</td>
                <td className="num" style={{ fontWeight: 600 }}>{rdaEur2(p.netValueEur)}</td>
                <td className="mono" style={{ fontSize: 10 }}>{p.wbsElement || p.costCenter || '—'}</td>
                {canEdit && (
                  <td style={{ whiteSpace: 'nowrap', textAlign: 'right' }}>
                    <button className="btn ghost icon" title="Modifica riga" onClick={() => setForm({ mode: 'edit', pos: p })} data-testid={`rda-pos-edit-${p.lineNo}`}><Icon name="edit" size={12} /></button>
                    <button className="btn ghost icon" title="Elimina riga" onClick={() => removePosition(p)} data-testid={`rda-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 }}>{rdaEur2(sumNet)}</td><td colSpan={canEdit ? 2 : 1}></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} /> RdA in stato "{rda.status}": le righe sono congelate (sola lettura).</div>
      )}
      {form && (
        <RdaPositionForm rda={rda} mode={form.mode} pos={form.pos} user={user} pushToast={pushToast}
          onClose={() => setForm(null)} onSaved={() => { setForm(null); reload(); }} />
      )}
    </div>
  );
}

// Form riga RdA (create/edit) — API mode (POST/PATCH) oppure locale (onSubmitLocal).
function RdaPositionForm({ rda, 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,
    };
    if (onSubmitLocal) { onSubmitLocal({ ...body, netValueEur: netPreview }); return; }
    const url = mode === 'edit'
      ? `/api/rda/${encodeURIComponent(rda.id)}/positions/${encodeURIComponent(pos.id)}`
      : `/api/rda/${encodeURIComponent(rda.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'} size="md" footer={
      <>
        <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
        <Btn variant="primary" size="sm" onClick={submit} disabled={saving} data-testid="rda-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. PLC SIMATIC S7-1500" data-testid="rda-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)} /></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={rdaEur2(netPreview)} readOnly style={{ background: 'var(--bg-2)', fontWeight: 600 }} /></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)} /></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>Consegna riga</label><input type="date" value={f.deliveryDate} onChange={(e) => set('deliveryDate', e.target.value)} /></div>
        </div>
        <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>Campi allineati a SAP (EBAN) per la futura integrazione.</div>
      </div>
    </Modal>
  );
}

// ============================================================
// Estrazione AI testata RdA + righe (array nidificato). Mirror del flusso OdA.
// ============================================================
const RDA_EXTRACT_FIELDS = [
  ['title', 'string', 'Oggetto/titolo della richiesta'],
  ['vendor', 'string', 'Ragione sociale del fornitore suggerito'],
  ['vendorCode', 'string', 'Codice/partita IVA del fornitore (LIFNR) se presente'],
  ['materialGroup', 'string', 'Gruppo merci, se indicato'],
  ['deliveryDate', 'date', 'Data di consegna richiesta (YYYY-MM-DD)'],
  ['paymentTerms', 'string', 'Termini di pagamento'],
  ['currency', 'string', 'Valuta (es. EUR)'],
  ['netTotal', 'number', 'Totale netto richiesto in euro interi'],
];
const RDA_EXTRACT_SCHEMA = (() => {
  const properties = {};
  for (const [name, type, desc] of RDA_EXTRACT_FIELDS) {
    properties[name] = { type, description: desc };
    properties[`${name}_confidence`] = { type: 'number', description: `Confidenza 0-100 sul valore di ${name}` };
  }
  properties.positions = {
    type: 'array',
    description: 'Righe della richiesta (posizioni), 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)' },
        unitPrice: { type: 'number', description: 'Prezzo unitario netto' },
      },
    },
  };
  properties.overall_confidence = { type: 'number', description: 'Confidenza complessiva 0-100' };
  return { type: 'object', properties, required: [] };
})();
const RDA_EXTRACT_INSTRUCTION =
  'Sei un assistente che legge una Richiesta di Acquisto. Estrai i dati di TESTATA dal documento. ' +
  'Per OGNI campo fornisci anche <campo>_confidence (0-100). Se un campo non è nel documento lascialo ' +
  'vuoto con confidenza 0. NON inventare. Estrai TUTTE le righe nell\'array "positions" ' +
  '(description, materialCode, quantity, uom, unitPrice). overall_confidence = affidabilità media. ' +
  'Importi in euro interi.';

const RDA_URGENCY = ['bassa', 'normale', 'alta', 'critica'];

// ============================================================
// RdaCreateModal — Nuova RdA con upload documento + viewer affiancato +
// estrazione AI (testata+righe) + editor posizioni locale + riconciliazione
// fornitore. Mirror di NewOdaModal v2 (s121). Riusa i building block esposti da
// Oda.jsx su window (DocViewerPane, ConfBadge, vendorNormName, …).
// ============================================================
function RdaCreateModal({ projectId, onClose, onCreated }) {
  const { user, seed, extras, seedCustom, addRda, addVendor, pushToast } = useStore();
  const lockedProject = !!projectId;
  const fileInputRef = React.useRef(null);
  const DocViewer = (typeof window !== 'undefined' && window.DocViewerPane) || null;
  const ConfB = (typeof window !== 'undefined' && window.ConfBadge) || (() => null);
  const confKind = (typeof window !== 'undefined' && window.confChipKind) || (() => 'default');
  const normName = (typeof window !== 'undefined' && window.vendorNormName) || ((s) => String(s || '').toLowerCase().trim());
  const normDate = (typeof window !== 'undefined' && window.normExtractDate) || ((v) => (v ? String(v).slice(0, 10) : ''));

  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 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 [f, setF] = React.useState({
    title: '', project: projectId || '', vendor: '', vendorCode: '', materialGroup: '',
    urgency: 'normale', currency: 'EUR', deliveryDate: '', paymentTerms: '', amount: '0', description: '',
  });
  const [saving, setSaving] = React.useState(false);
  const [doc, setDoc] = React.useState(null);
  const [uploading, setUploading] = React.useState(false);
  const [extracting, setExtracting] = React.useState(false);
  const [conf, setConf] = React.useState({});
  const [overallConf, setOverallConf] = React.useState(null);
  const [extractMeta, setExtractMeta] = React.useState(null);
  const [positions, setPositions] = React.useState([]);
  const [posForm, setPosForm] = React.useState(null);
  const [addingVendor, setAddingVendor] = React.useState(false);
  const set = (k, v) => setF((s) => ({ ...s, [k]: v }));

  const matchedVendor = React.useMemo(() => {
    const n = normName(f.vendor);
    const code = (f.vendorCode || '').trim().toLowerCase();
    if (!n && !code) return null;
    return allVendors.find((v) =>
      (n && normName(v.name) === n) ||
      (code && (String(v.taxId || '').toLowerCase() === code || String(v.id || '').toLowerCase() === code)),
    ) || null;
  }, [f.vendor, f.vendorCode, allVendors, normName]);
  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 = '';
    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', 'RDA'); 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: RDA_EXTRACT_SCHEMA, instruction: RDA_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,
        materialGroup: v.materialGroup || s.materialGroup, deliveryDate: normDate(v.deliveryDate) || s.deliveryDate,
        paymentTerms: v.paymentTerms || s.paymentTerms, 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, materialGroup: v.materialGroup_confidence, deliveryDate: v.deliveryDate_confidence, paymentTerms: v.paymentTerms_confidence, currency: v.currency_confidence, amount: v.netTotal_confidence });
      setOverallConf(v.overall_confidence != null ? Number(v.overall_confidence) : null);
      setExtractMeta({ provider: j.data.provider });
      const raw = Array.isArray(v.positions) ? v.positions : [];
      const lines = raw.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 (lines.length > 0) setPositions(lines);
      pushToast({ title: 'Dati estratti dall\'AI', desc: `${j.data.provider || 'AI'} · testata + ${lines.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: 'Oggetto obbligatorio', tone: 'err' }); return; }
    if (!f.project) { pushToast({ title: 'Progetto obbligatorio', tone: 'err' }); return; }
    // Righe inviate nel body di create: la RdA + le sue posizioni nascono con
    // un'UNICA operazione gated `rda.create` (no loop su /positions, che è gated
    // `rda.edit` e bloccherebbe chi può creare ma non editare).
    const body = {
      title: f.title.trim(), project: f.project || null, vendor: f.vendor.trim() || null,
      vendorCode: f.vendorCode.trim() || null, materialGroup: f.materialGroup.trim() || null,
      urgency: f.urgency || 'normale', deliveryDate: f.deliveryDate || null, paymentTerms: f.paymentTerms.trim() || null,
      description: f.description.trim() || null, status: 'draft',
      amount: hasPositions ? Math.round(sumPosNet) : (parseInt(f.amount, 10) || 0),
      source: overallConf != null ? 'ai_extracted' : 'manual', sourceDocumentId: doc?.id || null,
      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,
        deliveryDate: p.deliveryDate || null, notes: p.notes || null,
      })),
    };
    setSaving(true);
    try {
      const r = await fetch('/api/rda', { 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: 'RdA non creata', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      addRda(j.data);
      pushToast({ title: 'RdA creata', desc: positions.length ? `${j.data.id} · ${positions.length} righe` : `${j.data.id} creata`, tone: 'ok' });
      onCreated ? onCreated(j.data) : onClose();
    } catch (e) { pushToast({ title: 'Errore di rete', desc: String(e?.message || e), tone: 'err' }); }
    finally { setSaving(false); }
  };

  const lbl = { 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="rda-new-modal">
        <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">Nuova Richiesta 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 }}>
              <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="rda-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 (PDF, Word, Excel, immagini)' : 'Seleziona prima il progetto'} onClick={() => { if (f.project && !uploading) fileInputRef.current && fileInputRef.current.click(); }} data-testid="rda-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.</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={confKind(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={lbl}><span>Oggetto *</span><ConfB c={conf.title} /></label><input value={f.title} onChange={(e) => set('title', e.target.value)} placeholder="es. Fornitura sistema visione AI" data-testid="rda-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="rda-new-project" />
                  )}
                </div>
                <div className="field"><label>Urgenza</label>
                  <select value={f.urgency} onChange={(e) => set('urgency', e.target.value)}>{RDA_URGENCY.map((u) => <option key={u} value={u}>{u}</option>)}</select>
                </div>
              </div>
              <div className="grid grid-2" style={{ gap: 8 }}>
                <div className="field"><label style={lbl}><span>Vendor (nome)</span><ConfB 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={lbl}><span>Codice vendor</span><ConfB 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="rda-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 all\'anagrafica vendor' : window.whyDisabled('vendor.create')} onClick={addVendorToAnagrafica} data-testid="rda-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={lbl}><span>Gruppo merci</span><ConfB c={conf.materialGroup} /></label><input value={f.materialGroup} onChange={(e) => set('materialGroup', e.target.value)} /></div>
                <div className="field"><label style={lbl}><span>Valuta</span><ConfB c={conf.currency} /></label><input value={f.currency} onChange={(e) => set('currency', e.target.value)} /></div>
                <div className="field"><label style={lbl}><span>Importo €{hasPositions ? ' (da righe)' : ''}</span><ConfB 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); }} style={hasPositions ? { background: 'var(--bg-2)' } : undefined} /></div>
              </div>
              <div className="grid grid-2" style={{ gap: 8 }}>
                <div className="field"><label style={lbl}><span>Consegna richiesta</span><ConfB c={conf.deliveryDate} /></label><input type="date" value={f.deliveryDate} onChange={(e) => set('deliveryDate', e.target.value)} /></div>
                <div className="field"><label style={lbl}><span>Termini pagam.</span><ConfB c={conf.paymentTerms} /></label><input value={f.paymentTerms} onChange={(e) => set('paymentTerms', e.target.value)} placeholder="es. 60 gg d.f." /></div>
              </div>

              {/* Editor posizioni (righe) — stato locale, persistite al "Crea RdA" */}
              <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="rda-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="rda-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">{rdaEur2(p.unitPriceEur)}</td>
                          <td className="num" style={{ fontWeight: 600 }}>{rdaEur2(p.netValueEur)}</td>
                          <td style={{ whiteSpace: 'nowrap', textAlign: 'right' }}>
                            <button className="btn ghost icon" title="Modifica" onClick={() => setPosForm({ mode: 'edit', pos: p, lid: p._lid })}><Icon name="edit" size={12} /></button>
                            <button className="btn ghost icon" title="Elimina" 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 }}>{rdaEur2(sumPosNet)}</td><td></td></tr></tfoot>
                  </table>
                )}
              </div>

              <div className="field"><label>Descrizione / giustificazione</label><input value={f.description} onChange={(e) => set('description', e.target.value)} /></div>
              <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>L'importo è la <strong>somma delle posizioni</strong>. I valori estratti dall'AI sono <strong>proposte</strong>: rivedi e correggi.</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="rda-new-submit">{saving ? 'Creo…' : 'Crea RdA'}</Btn>
          </div>
        </div>

        {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="rda-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' }}>
              {DocViewer ? <DocViewer docId={doc.id} mimeType={doc.mimeType} filename={doc.filename} user={user} /> : <div style={{ margin: 'auto', color: 'var(--text-3)' }}>Viewer non disponibile</div>}
            </div>
          </div>
        )}
      </div>

      {posForm && (
        <RdaPositionForm rda={{ 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>
  );
}

// FASE 12: esporto le card workflow + SLA così Vendors.jsx (caricato dopo RdA.jsx)
// può riusarle per il detail vendor senza duplicare codice.
// Sessione 88: esporto anche RdaDetailModal per riuso in ProjectDetail.
Object.assign(window, { RdA, RdaDetailModal, RdaPositionsSection, RdaPositionForm, RdaCreateModal, WorkflowInstancesCard, WorkflowInstanceDetailModal, SlaEventsCard });
