// ============================================================
// ProjectDetail.jsx
// ============================================================
const PD_TABS = ['overview', 'mywork', 'gantt', 'docs', 'checklist', 'rda', 'workflow', 'comm', 'budget', 'ai'];

function ProjectDetail() {
  const { navigate, routeParam, seed, pushToast, extras, user, seedCustom } = useStore();
  // FASE 16 (sessione 91) — routeParam può essere un deep-link posizionale
  // `projectId|tab|docId` (dal feed "Cosa devi fare" della pagina Alert).
  // Retro-compatibile: un routeParam senza separatore `|` resta il projectId.
  const [paramProjectId, paramTab, paramDocId] = String(routeParam || '').split('|');
  // Dedup extras + seed per evitare match-doppio (extras vince su seed).
  const allProjects = (() => {
    const ext = extras?.projects || [];
    const seenIds = new Set();
    const out = [];
    for (const x of [...ext, ...seed.PROJECTS]) {
      if (!x?.id || seenIds.has(x.id)) continue;
      seenIds.add(x.id);
      out.push(x);
    }
    return out;
  })();
  const p = allProjects.find((x) => x.id === paramProjectId) || allProjects[0];
  // Fase 4b — Smart default tab landing:
  //   - deep-link ?tab=X via paramTab → priorità massima (esplicito utente)
  //   - else 'overview' come default iniziale, poi auto-switch a 'mywork' se
  //     myWorkTodoCount > 0 (deciso nel useEffect myWorkTodoCount sotto)
  //   - flag `smartDefaultApplied` per non rifare lo switch se l'utente nel
  //     frattempo ha cliccato manualmente un altro tab
  const [tab, setTab] = React.useState(() => (PD_TABS.includes(paramTab) ? paramTab : 'overview'));
  const smartDefaultDone = React.useRef(false);
  const [exportingPdf, setExportingPdf] = React.useState(false);
  // FASE 8 (s109) — modal "Re-applica prefab" di config workflow.
  const [reloadPrefabOpen, setReloadPrefabOpen] = React.useState(false);
  const canReloadPrefab = window.can('project.update', user, seedCustom);

  // Re-sync della tab quando cambia il deep-link mentre ProjectDetail è già
  // montato (es. l'utente clicca un altro task "Vai" sullo stesso progetto).
  React.useEffect(() => {
    if (paramTab && PD_TABS.includes(paramTab)) setTab(paramTab);
  }, [routeParam]);

  const exportPdf = React.useCallback(async () => {
    if (exportingPdf || !p?.id) return;
    setExportingPdf(true);
    try {
      const res = await fetch(`/api/projects/${encodeURIComponent(p.id)}/pdf`, {
        method: 'GET',
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
        credentials: 'same-origin',
      });
      if (!res.ok) {
        const j = await res.json().catch(() => ({}));
        throw new Error(j.error || j.detail || 'HTTP ' + res.status);
      }
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `${p.code}_${new Date().toISOString().slice(0, 10)}.pdf`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      setTimeout(() => URL.revokeObjectURL(url), 1000);
      pushToast({ title: 'PDF generato', desc: `${(blob.size / 1024).toFixed(1)} KB`, tone: 'ok' });
    } catch (err) {
      pushToast({ title: 'Export PDF fallito', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setExportingPdf(false);
    }
  }, [p?.id, p?.code, exportingPdf, pushToast, user?.id]);

  // FASE 2b.4: relazioni reali invece di hash/slice fake.
  // Vendor "principale": il più frequente nelle RdA del progetto. Se nessuna RdA, null.
  // FASE 2b.5 (bug fix): dedup per id — extras vince su seed. Senza questa dedup,
  // dopo un upsertRda (es. cambio status) la stessa RdA compariva in entrambe le
  // liste e il conteggio diventava ×2.
  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 projectRda = React.useMemo(() => allRda.filter(r => r.project === p.id), [allRda, p.id]);
  // FASE 10.5 — OdA collegate al progetto (extras vince su seed, dedup per id).
  const allOda = React.useMemo(() => {
    const extra = extras?.oda || [];
    const extraIds = new Set(extra.map(o => o.id));
    const seedFiltered = (seed.ODA || []).filter(o => !extraIds.has(o.id));
    return [...extra, ...seedFiltered];
  }, [extras, seed]);
  const projectOda = React.useMemo(() => allOda.filter(o => o.project === p.id), [allOda, p.id]);
  const primaryVendorName = React.useMemo(() => {
    if (projectRda.length === 0) return null;
    const counts = projectRda.reduce((acc, r) => {
      if (r.vendor) acc[r.vendor] = (acc[r.vendor] || 0) + 1;
      return acc;
    }, {});
    const top = Object.entries(counts).sort((a, b) => b[1] - a[1])[0];
    return top?.[0] || null;
  }, [projectRda]);
  const vendor = React.useMemo(() => {
    if (!primaryVendorName) return null;
    return (seed.VENDORS || []).find(v => v.name === primaryVendorName) || null;
  }, [primaryVendorName, seed.VENDORS]);

  // Documenti d'archivio realmente correlati al progetto.
  // `project_ref` in archive_doc contiene il `code` del progetto (a volte con anno tra
  // parentesi, es. "DEMO-CMR-HVC-022 (2020)"), NON l'id. Match con `startsWith(p.code)`.
  const related = React.useMemo(
    () => (seed.ARCHIVE_DOCS || []).filter(d => d.project && p.code && d.project.startsWith(p.code)),
    [seed.ARCHIVE_DOCS, p.code],
  );

  const seedComms = (seed.COMMUNICATIONS || []).filter((c) => c.project === p.id);
  const extraComms = (extras?.comms?.[p.id] || []).map(c => ({ id: c.id, project: p.id, subject: c.subject, from: c.from, to: '—', date: c.ts.slice(0,10), excerpt: c.preview, kind: c.kind, signal: 'ok', signalScore: 0 }));
  const comms = [...extraComms, ...seedComms];
  const alerts = (seed.ALERTS || []).filter((a) => a.project === p.id);

  // Milestone: ora vengono dal DB tramite bootstrap (`seed.MILESTONES[projectId]`).
  // Le `extras.milestones` restano per i record creati nella sessione corrente
  // (inclusi quelli appena salvati dal NewProjectModal: addMilestone è chiamato dopo POST).
  const seedMilestones = (seed.MILESTONES && seed.MILESTONES[p.id]) || [];
  const extrasMilestonesRaw = extras?.milestones?.[p.id] || [];
  // Dedup: se un id è in entrambi (capita dopo creazione → bootstrap successivo), tieni quello da seed.
  const seedIds = new Set(seedMilestones.map(m => m.id));
  const extrasMilestonesUnique = extrasMilestonesRaw.filter(m => !seedIds.has(m.id));
  const milestonesForGantt = [...seedMilestones, ...extrasMilestonesUnique];

  const extraDocs = extras?.projectDocs?.[p.id] || [];
  const extraBudget = extras?.budgetRows?.[p.id] || [];
  const [docsCount, setDocsCount] = React.useState(null);

  // Conteggio documenti per il contatore nella tab. Fetch indipendente dal
  // montaggio di DocsTab (che avviene solo quando la tab Documenti e attiva):
  // senza questo, al refresh con un'altra tab attiva il contatore resta vuoto.
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const res = await fetch(`/api/projects/${encodeURIComponent(p.id)}/documents`, { cache: 'no-store' });
        if (!res.ok) return;
        const json = await res.json();
        const serverDocs = json.data || [];
        const seedDocs = (seed.DOCUMENTS || []).filter((d) => d.project === p.id);
        const ids = new Set();
        let c = 0;
        [...serverDocs, ...extraDocs, ...seedDocs].forEach((d) => {
          if (d && d.id && !ids.has(d.id)) { ids.add(d.id); c++; }
        });
        if (!cancelled) setDocsCount(c);
      } catch { /* contatore best-effort */ }
    })();
    return () => { cancelled = true; };
  }, [p.id]);

  // FASE 3 RBAC (sessione 102) — per-tab capability gating. I tab restano
  // visibili solo se il ruolo ha almeno un permesso del namespace di pertinenza.
  // Mappa allineata a ROUTE_NS in lib/permissions.jsx (namespace = chiave catalog).
  const TAB_NAMESPACE = {
    overview: 'project',
    mywork: 'project',
    gantt: 'project',
    docs: 'doc',
    checklist: 'doc',
    rda: 'rda',
    workflow: 'project',
    comm: 'communication',
    budget: 'project',
    ai: 'ai',
  };

  // Fase 4 — Conteggio "Il mio lavoro" tramite ?count_only=true (~50-100ms).
  // Lo refresh ad ogni mount/cambio progetto + ogni 60s (refresh leggero).
  // Fase 4b — Trigger smart default tab landing: dopo il primo fetch, se non
  // c'è stato deep-link esplicito (paramTab) e count > 0, switch a 'mywork'.
  const [myWorkTodoCount, setMyWorkTodoCount] = React.useState(null);
  React.useEffect(() => {
    if (!p?.id || !user?.id) return;
    let cancelled = false;
    const fetchCount = async () => {
      try {
        const res = await fetch(
          `/api/projects/${encodeURIComponent(p.id)}/my-work?count_only=true`,
          {
            cache: 'no-store',
            headers: { 'X-Actor-Persona-Id': user.id },
            credentials: 'same-origin',
          },
        );
        if (!res.ok) return;
        const j = await res.json();
        if (cancelled) return;
        const cnt = j?.data?.todoCount ?? 0;
        setMyWorkTodoCount(cnt);
        // Fase 4b — Smart default tab landing (one-shot per mount progetto).
        // Trigger SOLO se: nessun deep-link esplicito (paramTab) + non già
        // fatto + count > 0. L'utente che ha già navigato manualmente non
        // viene disturbato. Reset di smartDefaultDone avviene al cambio
        // progetto (vedi reset useEffect su [p?.id] sotto).
        if (!smartDefaultDone.current && !paramTab && cnt > 0) {
          smartDefaultDone.current = true;
          setTab('mywork');
        }
      } catch { /* best-effort */ }
    };
    fetchCount();
    const interval = setInterval(fetchCount, 60_000);
    return () => { cancelled = true; clearInterval(interval); };
  }, [p?.id, user?.id, paramTab]);

  // Reset smart default flag al cambio progetto (consente nuova auto-switch
  // se l'utente apre un altro progetto con todoCount > 0).
  React.useEffect(() => {
    smartDefaultDone.current = false;
  }, [p?.id]);

  const allTabs = [
    { id: 'overview', label: 'Panoramica' },
    { id: 'mywork', label: 'Il mio lavoro', count: myWorkTodoCount, urgent: myWorkTodoCount > 0 },
    { id: 'gantt', label: 'Gantt & forecast' },
    { id: 'docs', label: 'Documenti', count: docsCount },
    { id: 'checklist', label: 'Checklist' },
    { id: 'rda', label: 'RdA collegate', count: projectRda.length },
    { id: 'oda', label: 'OdA collegate', count: projectOda.length },
    { id: 'workflow', label: 'Workflow' },
    { id: 'comm', label: 'Comunicazioni', count: comms.length },
    { id: 'budget', label: 'Budget & EV' },
    { id: 'ai', label: 'Insight AI', badge: true },
  ];
  const tabs = allTabs.filter((t) => {
    const ns = TAB_NAMESPACE[t.id];
    if (!ns) return true;
    const perms = window.effectivePermissions(user, seedCustom);
    return window.canSeeNamespace(perms, ns);
  });
  // Se il tab corrente non è più accessibile dopo il filter, ripiega sul primo
  // tab visibile invece di mostrare una pagina bianca.
  React.useEffect(() => {
    if (tabs.length > 0 && !tabs.some((t) => t.id === tab)) {
      setTab(tabs[0].id);
    }
  }, [tabs, tab]);

  return (
    <div className="page fade-in">
      <div style={{ marginBottom: 14, fontSize: 11.5, color: 'var(--text-2)' }}>
        <span style={{ cursor: 'pointer' }} onClick={() => navigate('projects')}>← Progetti</span>
      </div>
      <div className="page-header" style={{ alignItems: 'flex-start', marginBottom: 10 }}>
        <div>
          <div className="eyebrow">{p.code} · {p.site}</div>
          <h1 className="page-title" style={{ marginTop: 6 }}>{p.name}</h1>
          <div className="row" style={{ gap: 6, marginTop: 8 }}>
            <PhaseChip phase={p.phase} />
            <Health h={p.health} />
            <Chip>{p.category}</Chip>
            <Chip kind="info">Priorità {p.priority}</Chip>
            <Chip>PM · {seed.PERSONAS.find(u=>u.id===p.pm)?.name || '—'}</Chip>
          </div>
        </div>
        <div className="actions" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          {window.ProjectHealthBadge && <window.ProjectHealthBadge projectId={p.id} />}
          <Btn variant="ghost" size="sm" onClick={exportPdf} disabled={exportingPdf} data-action="export-pdf">
            <Icon name="download" size={12}/> {exportingPdf ? 'Generazione…' : 'Export PDF'}
          </Btn>
          <Btn
            variant="ghost"
            size="sm"
            onClick={canReloadPrefab ? () => setReloadPrefabOpen(true) : undefined}
            disabled={!canReloadPrefab}
            title={canReloadPrefab ? 'Re-applica il prefab di config (workflow + checklist) della categoria/classe' : window.whyDisabled('project.update')}
            data-action="reload-prefab"
          >
            <Icon name="refresh" size={12}/> Re-applica prefab
          </Btn>
          <Btn variant="ghost" size="sm"><Icon name="copy" size={12}/> Duplica</Btn>
          <Btn variant="ai" size="sm" onClick={() => setTab('ai')}><Icon name="sparkle" size={12}/> Insight AI</Btn>
        </div>
      </div>

      <div style={{ display: 'flex', gap: 4, borderBottom: '1px solid var(--line)', marginBottom: 16 }}>
        {tabs.map((t) => (
          <button key={t.id} onClick={() => setTab(t.id)} style={{
            padding: '8px 14px', fontSize: 12, fontWeight: 500,
            color: tab === t.id ? 'var(--text-0)' : 'var(--text-2)',
            borderBottom: '2px solid ' + (tab === t.id ? 'var(--accent)' : 'transparent'),
            marginBottom: -1,
          }}>
            {t.label} {t.count != null && <span className="mono" style={{ color: t.urgent ? 'var(--accent)' : 'var(--text-3)', marginLeft: 4, fontWeight: t.urgent ? 600 : 400 }}>({t.count})</span>}
            {t.badge && <span style={{ marginLeft: 6, width: 6, height: 6, background: 'var(--accent)', display: 'inline-block', borderRadius: '50%' }} />}
          </button>
        ))}
      </div>

      {tab === 'overview' && <OverviewTab p={p} vendor={vendor} alerts={alerts} related={related} projectRda={projectRda} setTab={setTab} />}
      {tab === 'mywork' && <MyWorkTab p={p} setTab={setTab} onCountChange={setMyWorkTodoCount} />}
      {tab === 'gantt' && <GanttTab p={p} extraMilestones={milestonesForGantt} />}
      {tab === 'docs' && <DocsTab p={p} seed={seed} extraDocs={extraDocs} onDocsCount={setDocsCount} focusDocId={paramDocId} />}
      {tab === 'checklist' && <ChecklistTab p={p} setTab={setTab} />}
      {tab === 'rda' && <RdaTab projectRda={projectRda} project={p} />}
      {tab === 'oda' && <OdaTab projectOda={projectOda} project={p} />}
      {tab === 'workflow' && <WorkflowInstancesTab project={p} setTab={setTab} />}
      {tab === 'comm' && <CommTab comms={comms} projectId={p.id} />}
      {tab === 'budget' && <BudgetTab p={p} extraBudget={extraBudget} />}
      {tab === 'ai' && <AITab p={p} alerts={alerts} related={related} projectRda={projectRda} projectOda={projectOda} />}

      {reloadPrefabOpen && (
        <ConfigReloadModal project={p} onClose={() => setReloadPrefabOpen(false)} />
      )}
    </div>
  );
}

// FASE 8 Project Cockpit (s109) — Modal "Re-applica prefab" di config workflow.
// Preview (dry_run) del diff prefab vs stato attuale, poi apply gated.
const RELOAD_SOURCE_LABEL = { category: 'Categoria', capex_class: 'Classe CAPEX', none: '—' };
const RELOAD_INVALID_LABEL = {
  not_found: 'workflow di progetto non trovato',
  inactive: 'workflow disattivato',
  no_steps: 'workflow senza step',
};

function ConfigReloadModal({ project, onClose }) {
  const { user, pushToast } = useStore();
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [diff, setDiff] = React.useState(null);
  const [applying, setApplying] = React.useState(false);
  const [done, setDone] = React.useState(null);

  const callReload = React.useCallback(
    async (dryRun) => {
      const r = await fetch(
        `/api/projects/${encodeURIComponent(project.id)}/config-reload?dry_run=${dryRun}`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
          credentials: 'same-origin',
          body: '{}',
        },
      );
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.detail || j.error || 'HTTP ' + r.status);
      return j.data;
    },
    [project.id, user?.id],
  );

  const loadPreview = React.useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const data = await callReload(true);
      setDiff(data.diff);
    } catch (err) {
      setError(String(err?.message || err));
    } finally {
      setLoading(false);
    }
  }, [callReload]);

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

  const apply = async () => {
    if (applying) return;
    setApplying(true);
    try {
      const data = await callReload(false);
      setDone(data.applied);
      const parts = [];
      if (data.applied.startedWorkflow) parts.push(`workflow ${data.applied.startedWorkflow.code} avviato`);
      else if (data.applied.workflowSkipped) parts.push(`workflow non avviato (${data.applied.workflowSkipped.reason})`);
      if (data.applied.checklistReevaluated) {
        parts.push(`checklist ${Math.round(data.applied.checklistReevaluated.scoreBp / 100)}%`);
      }
      pushToast({ title: 'Prefab ri-applicato', desc: parts.join(' · ') || 'Nessuna modifica', tone: 'ok' });
    } catch (err) {
      pushToast({ title: 'Reload prefab fallito', desc: String(err?.message || err), tone: 'err' });
      setApplying(false);
    }
  };

  const wf = diff?.workflow;
  const cl = diff?.checklist;
  const aligned = diff && wf && wf.added.length === 0 && wf.removed.length === 0;

  return (
    <Modal
      open
      onClose={applying ? () => {} : onClose}
      size="md"
      title="Re-applica prefab di configurazione"
      footer={
        done ? (
          <Btn variant="primary" size="sm" onClick={onClose} data-testid="reload-close">
            Chiudi
          </Btn>
        ) : (
          <>
            <Btn variant="ghost" size="sm" onClick={onClose} disabled={applying} data-testid="reload-cancel">
              Annulla
            </Btn>
            <Btn
              variant="primary"
              size="sm"
              onClick={apply}
              disabled={loading || !!error || applying || !diff}
              data-testid="reload-apply"
            >
              {applying ? 'Applico…' : 'Applica prefab'}
            </Btn>
          </>
        )
      }
    >
      <div style={{ fontSize: 12.5, lineHeight: 1.5 }}>
        <div style={{ marginBottom: 10, color: 'var(--text-2)' }}>
          Riallinea <strong>{project.name}</strong> allo standard della categoria «{project.category}» / classe «{project.capexClass}».
        </div>

        {loading && <div style={{ color: 'var(--text-3)' }} data-testid="reload-loading">Calcolo diff…</div>}
        {error && (
          <div style={{ padding: '10px 12px', border: '1px solid var(--err)', borderRadius: 6, color: 'var(--err)' }} data-testid="reload-error">
            {error}
          </div>
        )}

        {done && (
          <div style={{ padding: '10px 12px', border: '1px solid var(--ok)', borderRadius: 6, marginBottom: 12, background: 'color-mix(in oklch, var(--ok) 8%, var(--bg-1))' }} data-testid="reload-done">
            <div style={{ fontWeight: 600, marginBottom: 4 }}>Reload applicato</div>
            <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
              {done.startedWorkflow
                ? `Workflow ${done.startedWorkflow.code} avviato.`
                : done.workflowSkipped
                  ? `Workflow non avviato: ${done.workflowSkipped.reason}.`
                  : 'Nessun workflow da avviare.'}
              {done.checklistReevaluated && ` Checklist ri-valutata: ${Math.round(done.checklistReevaluated.scoreBp / 100)}%.`}
              {done.removedFlaggedForReview.length > 0 && ` ${done.removedFlaggedForReview.length} workflow non-standard restano da rivedere manualmente.`}
            </div>
          </div>
        )}

        {!loading && !error && diff && !done && (
          <>
            {/* Workflow di governance */}
            <div style={{ marginBottom: 14 }}>
              <div className="eyebrow" style={{ marginBottom: 6 }}>Workflow di governance</div>
              {wf.prefabCode ? (
                <div className="row" style={{ marginBottom: 8, gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
                  <span className="mono" style={{ fontSize: 12 }}>{wf.prefabCode}</span>
                  {wf.prefabName && <span style={{ color: 'var(--text-2)' }}>· {wf.prefabName}</span>}
                  <Chip kind="info">{RELOAD_SOURCE_LABEL[wf.source]}</Chip>
                  {!wf.valid && (
                    <Chip kind="warn">prefab non valido: {RELOAD_INVALID_LABEL[wf.invalidReason] || wf.invalidReason}</Chip>
                  )}
                </div>
              ) : (
                <div style={{ color: 'var(--text-3)', marginBottom: 8 }} data-testid="reload-no-prefab">
                  Nessun workflow di default configurato per categoria/classe. Imposta «Workflow di default» nel Customizing della classe CAPEX.
                </div>
              )}

              {wf.added.length > 0 && (
                <div style={{ marginBottom: 4 }} data-testid="reload-added">
                  <span style={{ color: 'var(--ok)', fontWeight: 600 }}>+ Da avviare:</span>{' '}
                  {wf.added.map((a) => a.code).join(', ')}
                </div>
              )}
              {wf.unchanged.length > 0 && (
                <div style={{ marginBottom: 4, color: 'var(--text-2)' }} data-testid="reload-unchanged">
                  ✓ Già attivo: {wf.unchanged.map((a) => a.code).join(', ')}
                </div>
              )}
              {wf.removed.length > 0 && (
                <div style={{ padding: '8px 10px', border: '1px solid var(--warn)', borderRadius: 6, marginTop: 6 }} data-testid="reload-removed">
                  <div style={{ color: 'var(--warn)', fontWeight: 600, fontSize: 11.5 }}>
                    <Icon name="warning_tri" size={10} /> Workflow non-standard ancora attivi
                  </div>
                  <div style={{ fontSize: 11.5, color: 'var(--text-2)', marginTop: 2 }}>
                    {wf.removed.map((a) => a.code).join(', ')} — non vengono annullati automaticamente (potrebbero avere approvazioni in corso). Rivedili e annullali manualmente se necessario.
                  </div>
                </div>
              )}
              {aligned && wf.valid && (
                <div style={{ color: 'var(--ok)', fontSize: 12 }} data-testid="reload-aligned">
                  ✓ Workflow già allineato allo standard.
                </div>
              )}
            </div>

            {/* Checklist */}
            <div>
              <div className="eyebrow" style={{ marginBottom: 6 }}>Checklist documentale</div>
              <div style={{ color: 'var(--text-2)' }}>
                {cl.prefabRuleName
                  ? <>Default: <strong>{cl.prefabRuleName}</strong>. </>
                  : <>Nessuna checklist di default sulla categoria. </>}
                Verrà ri-valutata con la config corrente
                {cl.lastScoreBp != null && <> (ultimo score {Math.round(cl.lastScoreBp / 100)}%)</>}.
              </div>
            </div>

            <div style={{ marginTop: 12, fontSize: 11, color: 'var(--text-3)' }}>
              <Icon name="info" size={10} /> L'operazione avvia il workflow prefab mancante e ri-valuta la checklist. Registrata in audit.
            </div>
          </>
        )}
      </div>
    </Modal>
  );
}

function OverviewTab({ p, vendor, alerts, related, projectRda = [], setTab }) {
  const { user, pushToast, seed, seedCustom } = useStore();
  const utilization = p.budget > 0 ? Math.round((p.spent / p.budget) * 100) : 0;

  // Sessione 84 (gap #11) — Checklist eval server-side anche su ProjectDetail.
  const [checklistEval, setChecklistEval] = React.useState(null);
  const [checklistLoading, setChecklistLoading] = React.useState(false);
  const [whyOpen, setWhyOpen] = React.useState(false);
  const [rulesCatalog, setRulesCatalog] = React.useState(null);
  // Sessione 85 #10 — Waiver state per project
  const [waiverForm, setWaiverForm] = React.useState(null);
  const [waiverSaving, setWaiverSaving] = React.useState(false);

  // FASE 1 Cockpit (s104) — team + catalogo ruoli del progetto.
  const [teamMembers, setTeamMembers] = React.useState([]);
  const [teamLoading, setTeamLoading] = React.useState(false);
  const [teamRoles, setTeamRoles] = React.useState([]);

  const reloadTeam = React.useCallback(async () => {
    if (!p?.id) return;
    setTeamLoading(true);
    try {
      const r = await fetch(`/api/projects/${p.id}/members`, {
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
      });
      const j = await r.json().catch(() => ({}));
      if (r.ok && Array.isArray(j?.data)) setTeamMembers(j.data);
    } catch { /* noop */ }
    finally { setTeamLoading(false); }
  }, [p?.id, user?.id]);

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

  React.useEffect(() => {
    let aborted = false;
    fetch('/api/config/project-roles', {
      headers: { 'X-Actor-Persona-Id': user?.id || '' },
    })
      .then((r) => r.json().catch(() => ({})))
      .then((j) => { if (!aborted && Array.isArray(j?.data)) setTeamRoles(j.data); })
      .catch(() => { /* noop */ });
    return () => { aborted = true; };
  }, [user?.id]);

  const canUpdateProject = typeof window !== 'undefined' && window.can
    ? window.can('project.update', user, seedCustom)
    : true;

  const onAddTeamMember = async (m) => {
    if (!p?.id || !canUpdateProject) return;
    try {
      const r = await fetch(`/api/projects/${p.id}/members`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ personaId: m.personaId, roleInProject: m.roleInProject }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        pushToast({ title: 'Errore', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({ title: 'Membro aggiunto', desc: j?.changed === false ? 'Era già nel team con quel ruolo' : 'Audit log registrato', tone: 'ok' });
      reloadTeam();
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'fail', tone: 'err' });
    }
  };

  const onRemoveTeamMember = async (m) => {
    if (!p?.id || !canUpdateProject) return;
    const memberId = m.id;
    if (!memberId) return;
    if (!window.confirm(`Rimuovere ${m.personaName || m.personaId} dal team?`)) return;
    try {
      const r = await fetch(`/api/projects/${p.id}/members/${memberId}`, {
        method: 'DELETE',
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        pushToast({ title: 'Errore', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({ title: 'Membro rimosso', desc: 'Soft-delete · audit log registrato', tone: 'ok' });
      reloadTeam();
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'fail', tone: 'err' });
    }
  };

  // s130 — cambio ruolo di un membro esistente inline (dropdown sul chip).
  // Backend già pronto (PATCH `/api/projects/[id]/members/[memberId]` da s104
  // FASE 1 Cockpit: RBAC project.update, validazione ruolo, anti-duplicate
  // 23505, audit diff, notifica role_changed). Era dead-path FE: il prop
  // onRoleChange esisteva ma nessun consumer lo passava → chip read-only.
  const onChangeTeamMemberRole = async (m, newRoleCode) => {
    if (!p?.id || !canUpdateProject) return;
    const memberId = m.id;
    if (!memberId || !newRoleCode || newRoleCode === m.roleInProject) return;
    try {
      const r = await fetch(`/api/projects/${p.id}/members/${memberId}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ roleInProject: newRoleCode }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        if (j?.error === 'duplicate_active_role') {
          pushToast({ title: 'Ruolo già presente', desc: 'La persona è già membro attivo con quel ruolo', tone: 'warn' });
        } else if (j?.error === 'invalid_role_in_project') {
          pushToast({ title: 'Ruolo non valido', desc: j?.detail || 'roleInProject sconosciuto', tone: 'err' });
        } else {
          pushToast({ title: 'Errore', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' });
        }
        return;
      }
      pushToast({ title: 'Ruolo aggiornato', desc: j?.changed === false ? 'Nessuna modifica' : 'Audit log registrato', tone: 'ok' });
      reloadTeam();
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'fail', tone: 'err' });
    }
  };

  React.useEffect(() => {
    if (!p?.id) return;
    let cancelled = false;
    setChecklistLoading(true);
    (async () => {
      try {
        const r = await fetch(`/api/checklist/evaluate?entityType=project&entityId=${encodeURIComponent(p.id)}`, {
          headers: { 'X-Actor-Persona-Id': user?.id || '' },
        });
        if (!r.ok) { if (!cancelled) setChecklistEval(null); return; }
        const j = await r.json();
        if (!cancelled) setChecklistEval(j.data || null);
      } catch { if (!cancelled) setChecklistEval(null); }
      finally { if (!cancelled) setChecklistLoading(false); }
    })();
    return () => { cancelled = true; };
  }, [p?.id, user?.id]);

  React.useEffect(() => {
    if (!whyOpen || 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; };
  }, [whyOpen, user?.id]);

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

  // Sessione 85 #10 — Create / revoke waiver per project
  const submitWaiver = async () => {
    if (!waiverForm || !p?.id) return;
    if (!waiverForm.justification || waiverForm.justification.length < 10) {
      pushToast({ title: 'Giustificazione troppo corta', desc: 'Min 10 caratteri.', 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: 'project', entityId: p.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}`, tone: 'ok' });
      setWaiverForm(null);
      await recomputeChecklist();
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'fail', tone: 'err' });
    } finally { setWaiverSaving(false); }
  };

  const revokeWaiver = async (exceptionId) => {
    if (!confirm('Revocare questo waiver?')) 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 ProjectDetail' }),
      });
      if (!r.ok) { pushToast({ title: 'Revoca fallita', tone: 'err' }); return; }
      pushToast({ title: 'Waiver revocato', tone: 'warn' });
      await recomputeChecklist();
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'fail', tone: 'err' });
    }
  };

  return (
    <>
      <div className="grid grid-4" style={{ marginBottom: 14 }}>
        <div className="card"><Stat label="Budget" value={fmtEUR(p.budget, true)} delta={'scadenza ' + fmtDate(p.end)} /></div>
        <div className="card"><Stat label="Impegnato" value={fmtEUR(p.committed, true)} delta={p.budget > 0 ? Math.round(p.committed/p.budget*100) + '% del budget' : '—'} /></div>
        <div className="card"><Stat label="Speso · utilization" value={fmtEUR(p.spent, true)} delta={utilization + '% del budget'} tone={utilization > p.progress ? 'down' : 'up'} /></div>
        <div className="card"><Stat label="Avanzamento fisico" value={p.progress + '%'} delta={'health: ' + (p.health || 'n/d')} tone={p.health === 'err' ? 'down' : p.health === 'warn' ? '' : 'up'} /></div>
      </div>

      {/* FASE 1 Cockpit (s104) — Sezione Team: chi è coinvolto, chi presiede cosa. */}
      <div className="card" style={{ marginBottom: 14 }}>
        <div className="card-header">
          <div className="title"><Icon name="users" size={12}/> Team di progetto</div>
          <div className="desc">
            {teamMembers.length === 0
              ? (p.pm ? `Solo PM (${(seed.PERSONAS||[]).find(x=>x.id===p.pm)?.name || p.pm}) — team non ancora costituito` : '🟡 Team non costituito: assegna almeno un PM al progetto per iniziare a tracciare le responsabilità')
              : `${teamMembers.length} membri attivi`}
          </div>
        </div>
        <div className="card-body">
          {teamLoading ? (
            <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Caricamento team…</div>
          ) : (
            <ProjectTeamEditor
              members={teamMembers}
              roles={teamRoles}
              personasFallback={seed.PERSONAS || []}
              onAdd={onAddTeamMember}
              onRemove={onRemoveTeamMember}
              onRoleChange={onChangeTeamMemberRole}
              disabled={!canUpdateProject}
              emptyHint={canUpdateProject
                ? 'Nessun membro nel team. Aggiungi sotto.'
                : 'Nessun membro nel team. Servono permessi project.update per modificare.'}
            />
          )}
        </div>
      </div>

      <div className="grid" style={{ gridTemplateColumns: '2fr 1fr', marginBottom: 14 }}>
        <div className="card">
          <div className="card-header">
            <div className="title"><Icon name="sparkle" size={12}/> Raccomandazione AI per questo progetto</div>
            <div className="desc">Agent PROJECT_ANALYZER · context-aware su DB live</div>
          </div>
          <div className="card-body">
            <ProjectAnalyzerCard project={p} />
          </div>
        </div>

        <div className="card">
          <div className="card-header">
            <div className="title">Fornitore principale</div>
            <div className="desc">
              {projectRda.length === 0
                ? 'Non ancora assegnato'
                : `Più frequente nelle ${projectRda.length} RdA del progetto`}
            </div>
          </div>
          <div className="card-body">
            {!vendor ? (
              <EmptyState
                icon={<Icon name="vendors" size={20} />}
                title="Nessun vendor associato"
                desc={projectRda.length === 0
                  ? 'Il progetto non ha RdA emesse. Crea una RdA per associare un fornitore principale.'
                  : 'Le RdA del progetto non specificano un vendor riconosciuto.'}
              />
            ) : (
              <>
                <div style={{ fontSize: 15, fontWeight: 600 }}>{vendor.name}</div>
                <div className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{vendor.category || '—'} · {vendor.country}</div>
                <div style={{ marginTop: 12, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
                  <div><div className="eyebrow">Rating</div><div style={{ fontSize: 18, fontWeight: 600, fontFamily: 'var(--font-display)' }}>{vendor.rating.toFixed(1)}<span style={{ fontSize: 11, color: 'var(--text-3)' }}>/5</span></div></div>
                  <div><div className="eyebrow">On-time</div><div style={{ fontSize: 18, fontWeight: 600, fontFamily: 'var(--font-display)' }}>{vendor.onTime}%</div></div>
                  <div><div className="eyebrow">Progetti</div><div style={{ fontSize: 18, fontWeight: 600, fontFamily: 'var(--font-display)' }}>{vendor.projects}</div></div>
                  <div><div className="eyebrow">Rischio</div><Chip kind={vendor.risk === 'high' ? 'err' : vendor.risk === 'medium' ? 'warn' : 'ok'} dot>{vendor.risk}</Chip></div>
                </div>
              </>
            )}
          </div>
        </div>
      </div>

      {/* Sessione 84 (gap #11) — Checklist documentale server-side */}
      <div className="card" style={{ marginBottom: 14 }}>
        <div className="card-header">
          <div className="title"><Icon name="check" size={12}/> Checklist documentale {checklistEval?.result && <span style={{ fontSize: 10, color: 'var(--ok)', marginLeft: 6 }}>· server-side</span>}</div>
          <div className="actions">
            {checklistEval?.result && (
              <Btn variant="ghost" size="sm" onClick={() => setWhyOpen(true)} data-testid="pd-checklist-why-btn">
                <Icon name="help" size={11}/> Perché questi doc?
              </Btn>
            )}
            <Btn variant="ghost" size="sm" onClick={recomputeChecklist} disabled={checklistLoading} data-testid="pd-checklist-recompute">
              <Icon name="refresh" size={11}/> {checklistLoading ? 'Ricalcolo…' : 'Ricalcola'}
            </Btn>
          </div>
        </div>
        <div className="card-body">
          {checklistLoading && !checklistEval ? (
            <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Caricamento checklist…</div>
          ) : !checklistEval?.result ? (
            <EmptyState icon={<Icon name="warning_tri" size={20}/>} title="Nessuna evaluation server-side" desc="Clicca 'Ricalcola' per generare una valutazione checklist per questo progetto."/>
          ) : (
            <>
              <div className="row" style={{ justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
                <div className="row" style={{ gap: 16, fontSize: 12.5 }}>
                  <span><b>{checklistEval.result.stats.rulesMatched}</b>/{checklistEval.result.stats.rulesEvaluated} regole matchate</span>
                  <span><b>{checklistEval.result.requiredCodes.length}</b> obbligatori</span>
                  <span style={{ color: 'var(--ok)' }}><b>{checklistEval.result.presentCodes.length}</b> presenti</span>
                  <span style={{ color: checklistEval.result.missingCodes.length > 0 ? 'var(--warn)' : 'var(--text-3)' }}><b>{checklistEval.result.missingCodes.length}</b> mancanti</span>
                </div>
                <div className="mono" style={{ fontSize: 12 }} data-testid="pd-checklist-score">
                  {Math.round(checklistEval.result.scoreBp / 100)}%
                </div>
              </div>
              <Meter value={checklistEval.result.scoreBp / 100} tone={!checklistEval.result.blocking ? 'ok' : checklistEval.result.scoreBp > 8000 ? 'warn' : 'err'} thick />
              {checklistEval.result.missingCodes.length > 0 ? (
                <div style={{ marginTop: 10 }}>
                  <div style={{ fontSize: 11.5, fontWeight: 500, marginBottom: 6 }}>Documenti mancanti</div>
                  <div className="row" style={{ gap: 4, flexWrap: 'wrap' }} data-testid="pd-checklist-missing">
                    {checklistEval.result.missingCodes.map(c => (
                      <div key={c} className="row" style={{ gap: 4, alignItems: 'center' }}>
                        <Chip kind="warn" dot>{c}</Chip>
                        <Btn variant="ghost" size="xs" onClick={() => setWaiverForm({ docCode: c, reason: 'not_applicable', justification: '', expiresAt: '' })} data-testid={`pd-waiver-btn-${c}`}>
                          Waivera
                        </Btn>
                      </div>
                    ))}
                  </div>
                </div>
              ) : (
                <div style={{ marginTop: 10, fontSize: 12.5, color: 'var(--ok)' }} data-testid="pd-checklist-complete">
                  <Icon name="check" size={12}/> Checklist documentale completa
                </div>
              )}
              {checklistEval.result.waivedCodes && checklistEval.result.waivedCodes.length > 0 && (
                <div style={{ marginTop: 10 }}>
                  <div style={{ fontSize: 11.5, fontWeight: 500, marginBottom: 6 }}>Documenti waivati ({checklistEval.result.waivedCodes.length})</div>
                  {checklistEval.result.waivers.map((w) => (
                    <div key={w.exceptionId} className="row" style={{ gap: 8, padding: '6px 0', borderBottom: '1px dashed var(--line)', alignItems: 'center' }} data-testid={`pd-waiver-${w.exceptionId}`}>
                      <Chip kind="warn" dot>{w.docCode}</Chip>
                      <span style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{w.reason}</span>
                      <span style={{ fontSize: 11, color: 'var(--text-2)', flex: 1 }}>{w.justification.slice(0, 60)}{w.justification.length > 60 ? '…' : ''}</span>
                      <Btn variant="ghost" size="xs" onClick={() => revokeWaiver(w.exceptionId)}>Revoca</Btn>
                    </div>
                  ))}
                </div>
              )}
            </>
          )}
        </div>
      </div>

      {/* Sessione 84 — Drawer "Perché questi documenti?" condiviso pattern RdA */}
      {whyOpen && checklistEval?.result && (
        <div onClick={() => setWhyOpen(false)} style={{
          position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 200,
        }} data-testid="pd-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,
          }} data-testid="pd-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={() => setWhyOpen(false)} data-testid="pd-why-close">
                <Icon name="close" size={11}/>
              </Btn>
            </div>
            <div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 16, padding: 12, background: 'var(--bg-2)', borderRadius: 8 }}>
              Motore checklist server-side ha valutato il progetto <code className="mono">{p.id}</code> contro {checklistEval.result.stats.rulesEvaluated} regole attive. <b>{checklistEval.result.matchedRuleIds.length}</b> hanno matchato.
            </div>
            <div className="eyebrow" style={{ marginBottom: 8 }}>Regole matchate ({checklistEval.result.matchedRuleIds.length})</div>
            {!rulesCatalog ? (
              <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Caricamento regole…</div>
            ) : checklistEval.result.matchedRuleIds.length === 0 ? (
              <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Nessuna regola matchata.</div>
            ) : (
              checklistEval.result.matchedRuleIds.map((ruleId) => {
                const rule = rulesCatalog.find(r => r.id === ruleId);
                if (!rule) return null;
                return (
                  <div key={ruleId} style={{ padding: 12, border: '1px solid var(--line)', borderRadius: 8, marginBottom: 10 }} data-testid={`pd-why-rule-${rule.id}`}>
                    <div className="row" style={{ justifyContent: 'space-between' }}>
                      <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 }}>{JSON.stringify(rule.conditions, null, 2)}</pre>
                    </div>
                    {rule.required && rule.required.length > 0 && (
                      <div style={{ marginTop: 8 }}>
                        <div className="eyebrow" style={{ fontSize: 10 }}>Doc obbligatori</div>
                        <div className="row" style={{ gap: 4, flexWrap: 'wrap', marginTop: 4 }}>
                          {rule.required.map(c => (
                            <Chip key={c} kind={checklistEval.result.presentCodes.includes(c) ? 'ok' : 'warn'} dot>
                              {c} {checklistEval.result.presentCodes.includes(c) ? '✓' : '!'}
                            </Chip>
                          ))}
                        </div>
                      </div>
                    )}
                  </div>
                );
              })
            )}
            <div style={{ marginTop: 16, padding: 10, background: 'var(--bg-2)', borderRadius: 6, fontSize: 10.5, color: 'var(--text-3)' }}>
              Modificabile in <b>Customizing → Checklist Rules</b>.
            </div>
          </div>
        </div>
      )}

      {/* Sessione 85 #10 — Waiver modal per ProjectDetail */}
      {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="pd-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)' }}>
              Eccezione checklist su <code className="mono">{waiverForm.docCode}</code> per project <code className="mono">{p.id}</code>.
            </div>
            <div className="field">
              <label>Motivo *</label>
              <select value={waiverForm.reason} onChange={(e) => setWaiverForm({ ...waiverForm, reason: e.target.value })} data-testid="pd-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} data-testid="pd-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>
      )}

      <div className="grid grid-2" style={{ marginBottom: 14 }}>
        <div className="card">
          <div className="card-header"><div className="title">Alert attivi</div></div>
          <div className="card-body" style={{ padding: 0 }}>
            {alerts.length === 0 ? <EmptyState icon={<Icon name="check" size={24}/>} title="Nessun alert" desc="Il progetto non mostra segnali di deviazione negli ultimi 30 giorni." /> : alerts.map((a) => (
              <div key={a.id} className="alert-card" style={{ borderRadius: 0, borderLeft: 'none', borderRight: 'none', borderTop: 'none' }}>
                <div className="bar" style={{ background: a.severity === 'err' ? 'var(--err)' : a.severity === 'warn' ? 'var(--warn)' : 'var(--info)' }} />
                <div>
                  <div className="ttl">{a.title}</div>
                  <div className="meta">{a.source} · confidenza {(a.confidence*100).toFixed(0)}% · {a.detected}</div>
                  <div className="reason">{a.reason}</div>
                </div>
              </div>
            ))}
          </div>
        </div>

        <div className="card">
          <div className="card-header">
            <div className="title">Documenti d'archivio del progetto</div>
            <div className="desc">{related.length === 0 ? 'Nessun documento d\'archivio collegato' : `${related.length} documenti correlati a ${p.code}`}</div>
          </div>
          <div className="card-body" style={{ padding: 0 }}>
            {related.length === 0 ? (
              <EmptyState
                icon={<Icon name="archive" size={20} />}
                title="Archivio vuoto per questo progetto"
                desc="Quando documenti d'archivio storici verranno collegati a questo progetto (project_ref), compariranno qui. La ricerca semantica AI sui progetti simili sarà disponibile in FASE 14 (pgvector + embeddings)."
              />
            ) : (
              related.map((d) => (
                <div key={d.id} className="doc-row">
                  <div className="ico">{d.type.slice(0, 3).toUpperCase()}</div>
                  <div>
                    <div className="name">{d.title}</div>
                    <div className="path">{d.project} · {d.vendor || '—'}{d.year ? ` · ${d.year}` : ''}</div>
                  </div>
                  <div><Chip kind="ai">{(d.match*100).toFixed(0)}%</Chip></div>
                </div>
              ))
            )}
          </div>
        </div>
      </div>
    </>
  );
}

function GanttTab({ p, extraMilestones = [] }) {
  const { user, pushToast, addMilestone, seedCustom } = useStore();
  const [addOpen, setAddOpen] = React.useState(false);
  // FASE 2c RBAC (sessione 103) — gating "Aggiungi milestone": milestone è
  // sub-entità progetto → project.update.
  const canEditProject = window.can('project.update', user, seedCustom);

  // FASE 19 — milestone reali da DB con drag-n-drop reorder + edit/delete.
  const [serverMilestones, setServerMilestones] = React.useState(null);
  const [loadingMs, setLoadingMs] = React.useState(true);
  const [savingOrder, setSavingOrder] = React.useState(false);
  const [draggedId, setDraggedId] = React.useState(null);
  const [dragOverId, setDragOverId] = React.useState(null);

  // FASE 15.E (sessione 91) — proposte AI di shift milestone (mail → Gantt).
  const [shiftProposals, setShiftProposals] = React.useState([]);
  const [shiftModalTarget, setShiftModalTarget] = React.useState(null);

  const reload = React.useCallback(async () => {
    if (!p?.id) return;
    setLoadingMs(true);
    try {
      const r = await fetch(`/api/projects/${encodeURIComponent(p.id)}/milestones`, { cache: 'no-store' });
      if (!r.ok) throw new Error('HTTP ' + r.status);
      const j = await r.json();
      setServerMilestones(j.data || []);
    } catch (err) {
      console.warn('[GanttTab] fetch milestones failed:', err);
      setServerMilestones([]);
    } finally {
      setLoadingMs(false);
    }
  }, [p?.id]);

  const reloadProposals = React.useCallback(async () => {
    if (!p?.id) return;
    try {
      const r = await fetch(
        `/api/projects/${encodeURIComponent(p.id)}/milestone-shift-proposals`,
        { cache: 'no-store' },
      );
      if (!r.ok) throw new Error('HTTP ' + r.status);
      const j = await r.json();
      setShiftProposals(j.data || []);
    } catch (err) {
      console.warn('[GanttTab] fetch shift proposals failed:', err);
      setShiftProposals([]);
    }
  }, [p?.id]);

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

  // Merge: server (canonico) + extras locali in sessione (creati ora).
  const milestones = React.useMemo(() => {
    const seedIds = new Set((serverMilestones || []).map((m) => m.id));
    const extras = (extraMilestones || []).filter((m) => !seedIds.has(m.id));
    return [...(serverMilestones || []), ...extras].sort(
      (a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0),
    );
  }, [serverMilestones, extraMilestones]);

  // Mappa milestoneId → proposta pending (confidence più alta se più di una).
  const proposalsByMilestone = React.useMemo(() => {
    const map = {};
    for (const pr of shiftProposals) {
      const cur = map[pr.milestoneId];
      if (!cur || (pr.confidenceBp ?? 0) > (cur.confidenceBp ?? 0)) map[pr.milestoneId] = pr;
    }
    return map;
  }, [shiftProposals]);

  // Sliding-window Gantt: fasi calcolate dal progress (no hardcoded health-ternary).
  const start = new Date(p.start);
  const end = new Date(p.end);
  const totalDays = daysBetween(start, end);
  const today = new Date();
  const phaseTone = (from, to) => {
    const prog = (p.progress || 0) / 100;
    if (prog >= to) return 'done';
    if (prog >= from) return 'risk';
    return 'planned';
  };
  const phases = [
    { n: 'Progettazione', from: 0, to: 0.15, tone: phaseTone(0, 0.15) },
    { n: 'Approvvigionamento', from: 0.12, to: 0.35, tone: phaseTone(0.12, 0.35) },
    { n: 'Installazione', from: 0.30, to: 0.70, tone: phaseTone(0.30, 0.70) },
    { n: 'Collaudo', from: 0.65, to: 0.90, tone: phaseTone(0.65, 0.90) },
    { n: 'Chiusura & SAL finale', from: 0.88, to: 1.0, tone: phaseTone(0.88, 1.0) },
  ];

  // Drag-n-drop reorder: sposta `draggedId` nella posizione di `targetId`.
  const handleReorder = async (draggedIdLocal, targetIdLocal) => {
    if (!draggedIdLocal || !targetIdLocal || draggedIdLocal === targetIdLocal) return;
    const sorted = [...milestones];
    const fromIdx = sorted.findIndex((m) => m.id === draggedIdLocal);
    const toIdx = sorted.findIndex((m) => m.id === targetIdLocal);
    if (fromIdx === -1 || toIdx === -1) return;
    const [moved] = sorted.splice(fromIdx, 1);
    sorted.splice(toIdx, 0, moved);
    // Riassegna orderIndex 0..N
    const items = sorted.map((m, i) => ({ id: m.id, orderIndex: i }));
    setSavingOrder(true);
    try {
      const r = await fetch(`/api/projects/${encodeURIComponent(p.id)}/milestones`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        credentials: 'same-origin',
        body: JSON.stringify({ items }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || 'HTTP ' + r.status);
      if (j.changed) {
        setServerMilestones(j.data);
        pushToast({ title: `Riordinate ${j.reordered} milestone`, tone: 'ok' });
      }
    } catch (err) {
      pushToast({ title: 'Riordino fallito', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setSavingOrder(false);
      setDraggedId(null);
      setDragOverId(null);
    }
  };

  const updateMilestoneStatus = async (id, status) => {
    try {
      const r = await fetch(
        `/api/projects/${encodeURIComponent(p.id)}/milestones/${encodeURIComponent(id)}`,
        {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
          credentials: 'same-origin',
          body: JSON.stringify({ status }),
        },
      );
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || 'HTTP ' + r.status);
      if (j.changed) {
        await reload();
        pushToast({ title: `Status → ${status}`, tone: 'ok' });
      }
    } catch (err) {
      pushToast({ title: 'Update status fallito', desc: String(err?.message || err), tone: 'err' });
    }
  };

  const deleteMilestone = async (id) => {
    if (!window.confirm('Eliminare questa milestone?')) return;
    try {
      const r = await fetch(
        `/api/projects/${encodeURIComponent(p.id)}/milestones/${encodeURIComponent(id)}`,
        {
          method: 'DELETE',
          headers: { 'X-Actor-Persona-Id': user?.id || '' },
          credentials: 'same-origin',
        },
      );
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.error || 'HTTP ' + r.status);
      }
      await reload();
      pushToast({ title: 'Milestone eliminata', tone: 'ok' });
    } catch (err) {
      pushToast({ title: 'Delete fallito', desc: String(err?.message || err), tone: 'err' });
    }
  };

  const statusKind = (s) =>
    s === 'done' ? 'ok' : s === 'at_risk' ? 'err' : s === 'in_progress' ? 'warn' : 'info';
  const statusLabel = (s) =>
    ({ planned: 'pianificata', in_progress: 'in corso', done: 'completata', at_risk: 'a rischio' }[s] || s);

  return (
    <div className="card">
      <div className="card-header">
        <div className="title">Gantt & milestone</div>
        <div className="desc">Fasi auto-calcolate da progress · milestone live drag-n-drop</div>
        <div className="actions">
          <Btn
            variant="ghost"
            size="sm"
            disabled={!canEditProject}
            onClick={() => { if (canEditProject) setAddOpen(true); }}
            title={canEditProject ? undefined : window.whyDisabled('project.update')}
          ><Icon name="plus" size={12}/> Aggiungi milestone</Btn>
          <Btn variant="ghost" size="sm" onClick={reload} disabled={loadingMs || savingOrder}>
            <Icon name="refresh" size={12}/> Aggiorna
          </Btn>
        </div>
      </div>
      <div className="gantt">
        <div className="gantt-row header">
          <div className="label">Fase · milestone</div>
          <div style={{ position: 'relative' }}>
            <div style={{ position: 'absolute', left: 0, top: 10, fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-3)' }}>{fmtDate(p.start)}</div>
            <div style={{ position: 'absolute', right: 10, top: 10, fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-3)' }}>{fmtDate(p.end)}</div>
          </div>
        </div>
        {phases.map((ph, i) => (
          <div key={i} className="gantt-row">
            <div className="label"><span className="phase-dot" />{ph.n}</div>
            <div className="bars">
              <div className="today" style={{ left: Math.min(100, Math.max(0, daysBetween(start, today) / totalDays * 100)) + '%' }} />
              <div className={`bar ${ph.tone}`} style={{ left: (ph.from * 100) + '%', width: ((ph.to - ph.from) * 100) + '%' }}>{ph.n}</div>
            </div>
          </div>
        ))}
        {/* Milestone reali sull'asse temporale */}
        {milestones.length > 0 && totalDays > 0 && (
          <div className="gantt-row">
            <div className="label"><span className="phase-dot" style={{ background: 'var(--accent-2)' }}/>Milestone</div>
            <div className="bars">
              <div className="today" style={{ left: Math.min(100, Math.max(0, daysBetween(start, today) / totalDays * 100)) + '%' }} />
              {milestones.filter((m) => m.due).map((m) => {
                const dueDate = new Date(m.due);
                const dueDays = daysBetween(start, dueDate);
                const at = Math.min(1, Math.max(0, dueDays / totalDays));
                return (
                  <React.Fragment key={m.id}>
                    <div
                      className="milestone"
                      style={{
                        left: (at * 100) + '%',
                        ...(proposalsByMilestone[m.id]
                          ? { boxShadow: '0 0 0 3px color-mix(in oklch, var(--accent) 38%, transparent)', cursor: 'pointer' }
                          : {}),
                      }}
                      title={proposalsByMilestone[m.id]
                        ? `${m.title} · proposta AI → ${proposalsByMilestone[m.id].proposedDue}`
                        : `${m.title} · ${m.due}`}
                      data-milestone-id={m.id}
                      onClick={proposalsByMilestone[m.id]
                        ? () => setShiftModalTarget(proposalsByMilestone[m.id])
                        : undefined}
                    />
                    <div style={{ position: 'absolute', left: (at * 100) + '%', top: 22, fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-2)', transform: 'translateX(-50%)', whiteSpace: 'nowrap', maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis' }}>{m.title}</div>
                  </React.Fragment>
                );
              })}
            </div>
          </div>
        )}
      </div>

      {/* Lista milestone con drag-n-drop reorder */}
      <div style={{ padding: '10px 14px', borderTop: '1px solid var(--line)' }}>
        <div className="row" style={{ marginBottom: 8 }}>
          <div className="eyebrow">
            Milestones live ({milestones.length})
            {shiftProposals.length > 0 && (
              <span style={{ color: 'var(--accent)', marginLeft: 8 }} data-shift-proposals-count>
                · {shiftProposals.length} proposta/e AI in attesa
              </span>
            )}
            {savingOrder && <span style={{ color: 'var(--accent)', marginLeft: 8 }}>· salvataggio…</span>}
          </div>
          <span className="spacer"/>
          <span style={{ fontSize: 10, color: 'var(--text-3)' }}>
            <Icon name="info" size={10}/> Trascina ⠿ per riordinare
          </span>
        </div>
        {loadingMs ? (
          <div style={{ fontSize: 11, color: 'var(--text-3)', padding: 8 }}>Caricamento…</div>
        ) : milestones.length === 0 ? (
          <div style={{ fontSize: 11.5, color: 'var(--text-3)', padding: 8 }}>
            Nessuna milestone. Click su "Aggiungi milestone" per crearne una.
          </div>
        ) : (
          milestones.map((m) => (
            <div
              key={m.id}
              draggable
              onDragStart={(e) => { setDraggedId(m.id); e.dataTransfer.effectAllowed = 'move'; }}
              onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverId(m.id); }}
              onDragLeave={() => setDragOverId((cur) => cur === m.id ? null : cur)}
              onDrop={(e) => { e.preventDefault(); handleReorder(draggedId, m.id); }}
              onDragEnd={() => { setDraggedId(null); setDragOverId(null); }}
              className="row"
              data-milestone-row={m.id}
              style={{
                gap: 10, padding: '8px 6px', borderBottom: '1px dashed var(--line)', fontSize: 11.5,
                cursor: 'grab', userSelect: 'none',
                background: dragOverId === m.id ? 'color-mix(in oklch, var(--accent) 8%, transparent)' : 'transparent',
                opacity: draggedId === m.id ? 0.5 : 1,
                transition: 'background .15s, opacity .15s',
              }}
            >
              <span style={{ color: 'var(--text-3)', cursor: 'grab', fontSize: 14 }} title="Trascina per riordinare">⠿</span>
              <span style={{ minWidth: 24, textAlign: 'center', color: 'var(--text-3)', fontFamily: 'var(--font-mono)', fontSize: 10.5 }}>#{m.orderIndex ?? 0}</span>
              <span style={{ fontWeight: 500, flex: '0 1 auto' }}>{m.title}</span>
              {m.owner && <span style={{ color: 'var(--text-2)' }}>· {m.owner}</span>}
              {proposalsByMilestone[m.id] && (
                <span
                  onClick={(e) => { e.stopPropagation(); setShiftModalTarget(proposalsByMilestone[m.id]); }}
                  onMouseDown={(e) => e.stopPropagation()}
                  title={`Proposta AI: sposta la data al ${proposalsByMilestone[m.id].proposedDue}`}
                  style={{ cursor: 'pointer' }}
                  data-shift-proposal-chip={m.id}
                >
                  <Chip kind="ai">proposta AI → {proposalsByMilestone[m.id].proposedDue}</Chip>
                </span>
              )}
              <span className="spacer"/>
              <select
                value={m.status}
                onChange={(e) => updateMilestoneStatus(m.id, e.target.value)}
                onClick={(e) => e.stopPropagation()}
                onMouseDown={(e) => e.stopPropagation()}
                style={{ fontSize: 11 }}
              >
                <option value="planned">pianificata</option>
                <option value="in_progress">in corso</option>
                <option value="done">completata</option>
                <option value="at_risk">a rischio</option>
              </select>
              <Chip kind={statusKind(m.status)} dot>{statusLabel(m.status)}</Chip>
              <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', minWidth: 80 }}>{m.due || '—'}</span>
              <button
                onClick={(e) => { e.stopPropagation(); deleteMilestone(m.id); }}
                onMouseDown={(e) => e.stopPropagation()}
                title="Elimina"
                style={{ fontSize: 11, color: 'var(--text-3)', background: 'transparent', border: 'none', cursor: 'pointer', padding: 4 }}
              >
                <Icon name="trash" size={11}/>
              </button>
            </div>
          ))
        )}
      </div>

      <AddMilestoneModal open={addOpen} onClose={() => { setAddOpen(false); reload(); }} projectId={p.id} />

      {shiftModalTarget && (
        <MilestoneShiftModal
          proposal={shiftModalTarget}
          milestone={milestones.find((m) => m.id === shiftModalTarget.milestoneId)}
          projectId={p.id}
          onClose={() => setShiftModalTarget(null)}
          onDecided={() => { reload(); reloadProposals(); }}
        />
      )}
    </div>
  );
}

/**
 * FASE 15.E (sessione 91) — Modale di revisione di una proposta AI di shift milestone.
 * Mostra before/after data + rationale + confidence. Approva → applica la nuova data
 * (decisione irreversibile, human-in-the-loop). Rifiuta → archivia la proposta.
 */
function MilestoneShiftModal({ proposal, milestone, projectId, onClose, onDecided }) {
  const { user, pushToast } = useStore();
  const [note, setNote] = React.useState('');
  const [busy, setBusy] = React.useState(null); // 'approve' | 'reject' | null

  const decide = async (decision) => {
    if (busy) return;
    setBusy(decision);
    try {
      const r = await fetch(
        `/api/projects/${encodeURIComponent(projectId)}/milestones/${encodeURIComponent(proposal.milestoneId)}/decide-shift`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
          credentials: 'same-origin',
          body: JSON.stringify({ proposalId: proposal.id, decision, note: note || undefined }),
        },
      );
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || 'HTTP ' + r.status);
      pushToast({
        title: decision === 'approve' ? 'Shift approvato' : 'Proposta rifiutata',
        desc: decision === 'approve'
          ? `Milestone spostata al ${proposal.proposedDue}${j.anomalyResolved ? ' · anomalia collegata risolta' : ''}`
          : 'Nessuna modifica alla milestone',
        tone: 'ok',
      });
      onDecided();
      onClose();
    } catch (err) {
      pushToast({ title: 'Decisione fallita', desc: String(err?.message || err), tone: 'err' });
      setBusy(null);
    }
  };

  const confidencePct = Math.round((proposal.confidenceBp || 0) / 100);

  return (
    <Modal
      open
      onClose={busy ? () => {} : onClose}
      size="md"
      title="Proposta AI · spostamento milestone"
      footer={<>
        <Btn variant="ghost" size="sm" onClick={() => decide('reject')} disabled={!!busy} data-testid="shift-reject">
          {busy === 'reject' ? 'Rifiuto…' : 'Rifiuta'}
        </Btn>
        <Btn variant="primary" size="sm" onClick={() => decide('approve')} disabled={!!busy} data-testid="shift-approve">
          {busy === 'approve' ? 'Applico…' : 'Approva shift'}
        </Btn>
      </>}
    >
      <div style={{ fontSize: 12.5, lineHeight: 1.5 }}>
        <div style={{ marginBottom: 4 }}>
          <div className="eyebrow">Milestone</div>
          <div style={{ fontWeight: 600, fontSize: 13 }}>{milestone?.title || proposal.milestoneId}</div>
        </div>

        <div
          className="row"
          style={{ gap: 14, alignItems: 'center', margin: '14px 0', padding: 12, border: '1px solid var(--line)', borderRadius: 8 }}
        >
          <div style={{ textAlign: 'center', flex: 1 }}>
            <div className="eyebrow">Data attuale</div>
            <div className="mono" style={{ fontSize: 14, color: 'var(--text-2)' }} data-testid="shift-current">
              {proposal.currentDue || '— non impostata'}
            </div>
          </div>
          <span style={{ fontSize: 20, color: 'var(--text-3)' }}>→</span>
          <div style={{ textAlign: 'center', flex: 1 }}>
            <div className="eyebrow">Data proposta</div>
            <div className="mono" style={{ fontSize: 14, fontWeight: 700, color: 'var(--accent)' }} data-testid="shift-proposed">
              {proposal.proposedDue}
            </div>
          </div>
        </div>

        <div className="row" style={{ gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
          <Chip kind="ai">confidenza {confidencePct}%</Chip>
          {proposal.matchedRef && <Chip kind="info">ref «{proposal.matchedRef}»</Chip>}
        </div>

        {proposal.rationale && (
          <div style={{ padding: '10px 12px', border: '1px solid var(--line)', borderRadius: 6, fontSize: 12, color: 'var(--text-2)' }}>
            {proposal.rationale}
          </div>
        )}

        <div style={{ marginTop: 12 }}>
          <div className="eyebrow">Nota decisione (opzionale)</div>
          <textarea
            value={note}
            onChange={(e) => setNote(e.target.value)}
            rows={2}
            maxLength={500}
            placeholder="Motivazione della scelta…"
            style={{ width: '100%', fontSize: 12, marginTop: 4 }}
          />
        </div>

        <div style={{ marginTop: 10, fontSize: 11, color: 'var(--text-3)' }}>
          <Icon name="info" size={10} /> L'approvazione modifica la data della milestone e risolve
          l'eventuale anomalia collegata. Decisione registrata in audit.
        </div>
      </div>
    </Modal>
  );
}

function DocsTab({ p, seed, extraDocs = [], onDocsCount, focusDocId }) {
  const { user, pushToast, seedCustom, navigate } = useStore();
  const [addOpen, setAddOpen] = React.useState(false);
  // FASE 2c RBAC (sessione 103) — gating "Nuovo documento DTO" → doc.upload.
  const canUploadDoc = window.can('doc.upload', user, seedCustom);
  // FASE 16 (sessione 91) — riga evidenziata dal deep-link "Cosa devi fare".
  const [highlightId, setHighlightId] = React.useState(null);

  // FASE 2c.4 Sprint 3: AI extraction modal per doc con file
  const [extractTarget, setExtractTarget] = React.useState(null);

  // FASE 2c.6 Sprint 4: signature request modal
  const [signTarget, setSignTarget] = React.useState(null);

  // Modale di lettura documento (testo + ricerca + delucidazioni AI)
  const [readerTarget, setReaderTarget] = React.useState(null);

  // Documenti reali con file caricato dal DB (FASE 2c.2 Sprint 2a)
  const [serverDocs, setServerDocs] = React.useState([]);
  const [serverLoading, setServerLoading] = React.useState(true);

  // Upload state
  const [uploadDocType, setUploadDocType] = React.useState('');
  const [classifying, setClassifying] = React.useState(false);
  const [uploadFile, setUploadFile] = React.useState(null);
  const [uploading, setUploading] = React.useState(false);
  const fileInputRef = React.useRef(null);

  const docTypes = seedCustom?.DOC_TYPES || [];

  const refreshServerDocs = React.useCallback(async () => {
    if (!p?.id) return;
    setServerLoading(true);
    try {
      const res = await fetch(`/api/projects/${encodeURIComponent(p.id)}/documents`, { cache: 'no-store' });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const json = await res.json();
      setServerDocs(json.data || []);
    } catch (err) {
      console.warn('[DocsTab] fetch documents failed:', err);
      setServerDocs([]);
    } finally {
      setServerLoading(false);
    }
  }, [p?.id]);

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

  const handleUpload = async () => {
    if (!uploadFile) {
      pushToast({ title: 'Nessun file selezionato', tone: 'warn' });
      return;
    }
    if (!uploadDocType) {
      pushToast({ title: 'Seleziona il tipo documento', tone: 'warn' });
      return;
    }
    setUploading(true);
    try {
      const fd = new FormData();
      fd.append('file', uploadFile);
      fd.append('projectId', p.id);
      fd.append('docTypeCode', uploadDocType);
      fd.append('title', uploadFile.name);
      const res = await fetch('/api/documents/upload', {
        method: 'POST',
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
        body: fd,
      });
      const json = await res.json().catch(() => null);
      if (!res.ok) {
        const msg = json?.detail || json?.error || `HTTP ${res.status}`;
        throw new Error(msg);
      }
      pushToast({ title: 'Upload OK', desc: `${uploadFile.name} · v${json.data.fileVersion}`, tone: 'ok' });
      setUploadFile(null);
      setUploadDocType('');
      if (fileInputRef.current) fileInputRef.current.value = '';
      await refreshServerDocs();
    } catch (err) {
      pushToast({ title: 'Upload fallito', desc: err?.message?.slice(0, 200) || 'Errore', tone: 'err' });
    } finally {
      setUploading(false);
    }
  };

  /**
   * FASE 2c.6 (Sprint 4 cliente): auto-classify upload con AI.
   * Legge il file localmente, lo manda a /api/ai/classify (chain provider),
   * pre-fila il select DocType se confidence > 0.7. Sotto threshold solo toast.
   */
  const handleClassifyAi = async () => {
    if (!uploadFile) return;
    setClassifying(true);
    try {
      // base64 encode lato client
      const arrayBuffer = await uploadFile.arrayBuffer();
      const bytes = new Uint8Array(arrayBuffer);
      let binary = '';
      for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
      const base64 = btoa(binary);

      const res = await fetch('/api/ai/classify', {
        method: 'POST',
        headers: { 'content-type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({
          fileBase64: base64,
          mimeType: uploadFile.type || 'application/octet-stream',
          filename: uploadFile.name,
        }),
      });
      const j = await res.json().catch(() => ({}));
      if (!res.ok) {
        const msg = mapAiErrorToUser(j?.error, j?.detail);
        throw new Error(msg);
      }
      const { docTypeCode, confidence, reasoning } = j.data || {};
      if (docTypeCode && confidence >= 0.7) {
        setUploadDocType(docTypeCode);
        pushToast({
          title: 'Classificazione AI',
          desc: `Tipo suggerito: ${docTypeCode} (${Math.round(confidence * 100)}% di confidenza)`,
          tone: 'ok',
        });
      } else if (docTypeCode) {
        pushToast({
          title: 'Suggerimento AI',
          desc: `Tipo possibile: ${docTypeCode} (${Math.round(confidence * 100)}%) — confidenza bassa, verifica manualmente`,
          tone: 'warn',
        });
      } else {
        pushToast({
          title: 'Classificazione AI',
          desc: reasoning ? reasoning.slice(0, 160) : 'Tipo non determinabile — seleziona manualmente',
          tone: 'warn',
        });
      }
    } catch (err) {
      pushToast({ title: 'Classificazione AI non riuscita', desc: err?.message?.slice(0, 240) || 'Riprova', tone: 'err' });
    } finally {
      setClassifying(false);
    }
  };

  const handleDelete = async (docId, name) => {
    if (!window.confirm(`Eliminare "${name}"?`)) return;
    try {
      const res = await fetch(`/api/documents/${encodeURIComponent(docId)}`, {
        method: 'DELETE',
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      pushToast({ title: 'Documento eliminato', tone: 'warn' });
      await refreshServerDocs();
    } catch (err) {
      pushToast({ title: 'Eliminazione fallita', desc: err?.message || 'Errore', tone: 'err' });
    }
  };

  const seedDocs = seed.DOCUMENTS.filter((d) => d.project === p.id);
  // Documenti server (con o senza file) — già filtrati per project
  const serverDocsRows = serverDocs.map(d => ({
    id: d.id,
    type: d.type,
    title: d.title,
    updated: d.uploadedAt || d.updatedAtText,
    completeness: d.completeness ?? 1,
    status: d.status || 'draft',
    aiGenerated: d.aiGenerated,
    _server: true,
    hasFile: d.hasFile,
    fileSize: d.fileSize,
    fileVersion: d.fileVersion,
    originalFilename: d.originalFilename,
    signatureStatus: d.signatureStatus,
    signedBy: d.signedBy,
    signedAt: d.signedAt,
  }));
  const docs = [
    ...serverDocsRows,
    ...extraDocs.map(d => ({ id: d.id, type: d.kind, title: d.title, updated: d.uploaded, completeness: 1, status: d.status || 'nuovo', aiGenerated: false, _extra: true })),
    ...seedDocs,
  ];

  // Dedup per id (server vince)
  const seenIds = new Set();
  const dedupedDocs = docs.filter(d => {
    if (seenIds.has(d.id)) return false;
    seenIds.add(d.id);
    return true;
  });

  // Comunica il conteggio documenti al ProjectDetail (per il contatore nella tab)
  React.useEffect(() => {
    if (onDocsCount) onDocsCount(dedupedDocs.length);
  }, [dedupedDocs.length, onDocsCount]);

  // FASE 16 (sessione 92) — Deep-link dal feed "Cosa devi fare": evidenzia il
  // documento target (bordo rosso) e scrolla la riga in vista. NESSUNA
  // apertura automatica della firma — è l'utente che decide se aprire il
  // documento e se/quando firmarlo. L'evidenziazione RESTA finché l'utente
  // non cambia tab (DocsTab smonta) o ricarica la pagina: appena consumato il
  // focus si toglie il docId dal routeParam, così un refresh non ri-evidenzia.
  const didFocusRef = React.useRef(false);
  React.useEffect(() => {
    if (!focusDocId || didFocusRef.current || serverLoading) return;
    const target = dedupedDocs.find((d) => d.id === focusDocId);
    if (!target) return;
    didFocusRef.current = true;
    setHighlightId(focusDocId);
    setTimeout(() => {
      const el = document.getElementById(`docrow-${focusDocId}`);
      if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }, 80);
    if (p?.id) navigate('project_detail', `${p.id}|docs`);
  }, [focusDocId, serverLoading, dedupedDocs, p?.id, navigate]);

  // FASE 16 — Riepilogo firme del progetto: quante richieste, quante firmate,
  // quante assegnate al ruolo dell'utente loggato ("chi deve fare cosa").
  const myRoleIds = user?.roleIds || [];
  const sigDocs = dedupedDocs.filter((d) => d._server && docTypeRequiresSignature(d.type, seedCustom));
  const sigSigned = sigDocs.filter((d) => d.signatureStatus === 'signed');
  const sigMine = sigDocs.filter((d) => {
    if (d.signatureStatus === 'signed') return false;
    const roles = docTypeSignerRoles(d.type, seedCustom);
    return roles.length > 0 && canSignDocTypeClient(myRoleIds, roles);
  });

  return (
    <div className="card">
      <div className="card-header">
        <div className="title">Documenti di progetto</div>
        <div className="actions">
          <Btn
            variant="ghost"
            size="sm"
            disabled={!canUploadDoc}
            onClick={() => { if (canUploadDoc) setAddOpen(true); }}
            title={canUploadDoc ? undefined : window.whyDisabled('doc.upload')}
          ><Icon name="plus" size={12}/> Nuovo documento DTO</Btn>
          <Btn variant="ai" size="sm"><Icon name="sparkle" size={12}/> Genera bozza</Btn>
        </div>
      </div>

      {/* Upload widget — FASE 2c.2 Sprint 2a */}
      <div style={{ padding: '12px 14px', borderBottom: '1px solid var(--line)', background: 'var(--bg-2)' }}>
        <div className="eyebrow" style={{ marginBottom: 8 }}>📎 Upload file</div>
        <div className="row" style={{ gap: 8, flexWrap: 'wrap', alignItems: 'flex-end' }}>
          <div className="field" style={{ flex: '1 1 200px', minWidth: 180 }}>
            <label style={{ fontSize: 10.5 }}>Tipo documento</label>
            <window.Autocomplete
              value={uploadDocType}
              onChange={(v) => setUploadDocType(v)}
              options={docTypes.map(dt => ({ value: dt.code, label: (dt.name || dt.code), sublabel: dt.code }))}
              placeholder="Cerca tipo… (spazio per lista)"
              testId="pd-upload-doctype-ac"
            />
          </div>
          <div className="field" style={{ flex: '2 1 280px', minWidth: 240 }}>
            <label style={{ fontSize: 10.5 }}>File (max 50MB · pdf, doc, xls, png, jpg, ...)</label>
            <input
              ref={fileInputRef}
              type="file"
              className="input sm"
              accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp,.txt,.csv,.zip,.json"
              onChange={(e) => setUploadFile(e.target.files?.[0] || null)}
              style={{ width: '100%' }}
            />
          </div>
          <Btn
            variant="ai"
            size="sm"
            onClick={handleClassifyAi}
            disabled={!uploadFile || classifying}
            title="L'AI legge il file e suggerisce il tipo documento"
          >
            <Icon name="sparkle" size={11}/> {classifying ? 'Classifica…' : 'Classifica con AI'}
          </Btn>
          <Btn
            variant="primary"
            size="sm"
            onClick={handleUpload}
            disabled={uploading || !uploadFile || !uploadDocType}
          >
            <Icon name="upload" size={12}/> {uploading ? 'Upload…' : 'Carica'}
          </Btn>
        </div>
      </div>

      {/* FASE 16 (sessione 91) — Riepilogo firme: chi deve fare cosa. */}
      {sigDocs.length > 0 && (
        <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--line)', display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
          <Icon name="signature" size={13} />
          <span style={{ fontSize: 12, fontWeight: 600 }}>Firme</span>
          <Chip kind="info">{sigDocs.length} richiedono firma</Chip>
          <Chip kind="ok">{sigSigned.length} firmati</Chip>
          {sigMine.length > 0 && <Chip kind="warn" dot>{sigMine.length} da firmare per il tuo ruolo</Chip>}
        </div>
      )}

      <div>
        {serverLoading ? (
          <div style={{ padding: 16, color: 'var(--text-2)', fontSize: 12 }}>Caricamento…</div>
        ) : dedupedDocs.length === 0 ? (
          <EmptyState title="Nessun documento" desc="Carica un file qui sopra; l'AI lo indicizzerà appena disponibile la pipeline FASE 11." />
        ) : dedupedDocs.map((d) => (
          <div
            key={d.id}
            id={`docrow-${d.id}`}
            className="doc-row"
            style={highlightId === d.id ? { outline: '2px solid var(--err)', outlineOffset: '-2px', borderRadius: 6 } : undefined}
          >
            <div className="ico">{(d.type || 'DOC').slice(0, 3).toUpperCase()}</div>
            <div style={{ flex: 1 }}>
              <div className="name">
                {d.title}
                {d.aiGenerated && <Chip kind="ai">AI draft</Chip>}
                {d._extra && <Chip kind="info">nuovo</Chip>}
                {d.hasFile && <Chip kind="ok">file v{d.fileVersion}</Chip>}
              </div>
              <div className="path">
                {d.type} · {d.updated ? `aggiornato ${fmtDate(d.updated)}` : 'no data'}
                {d.fileSize ? ` · ${(d.fileSize / 1024).toFixed(1)} KB` : ''}
                {d.originalFilename ? ` · ${d.originalFilename}` : ''}
                {d._server && docTypeRequiresSignature(d.type, seedCustom) && d.signatureStatus !== 'signed' && docTypeSignerRoles(d.type, seedCustom).length > 0 && (
                  <span style={{ color: 'var(--text-2)' }}> · firma: {docTypeSignerRoles(d.type, seedCustom).join(', ')}</span>
                )}
              </div>
            </div>
            <div className="row" style={{ gap: 6 }}>
              <Chip kind={d.status === 'approved' ? 'ok' : d.status === 'in_review' ? 'warn' : 'info'} dot>{d.status}</Chip>
              {/* Signature status chip — solo se la firma è rilevante (server doc) */}
              {d._server && d.signatureStatus && d.signatureStatus !== 'unsigned' && (
                <Chip
                  kind={d.signatureStatus === 'signed' ? 'ok' : d.signatureStatus === 'pending' ? 'warn' : 'err'}
                  dot
                  title={
                    d.signatureStatus === 'signed' && d.signedBy
                      ? `Firmato da ${d.signedBy}${d.signedAt ? ` · ${fmtDate(d.signedAt)}` : ''}`
                      : `Firma: ${d.signatureStatus}`
                  }
                >
                  {d.signatureStatus === 'signed' ? 'firmato' : d.signatureStatus === 'pending' ? 'firma in attesa' : 'firma rifiutata'}
                </Chip>
              )}
              {d.hasFile && (
                <Btn
                  variant="ghost"
                  size="sm"
                  onClick={() => setReaderTarget(d)}
                  title="Leggi il documento (ricerca testo + delucidazioni AI)"
                >
                  Leggi
                </Btn>
              )}
              {d.hasFile && (
                <a
                  className="btn ghost sm"
                  href={`/api/documents/${encodeURIComponent(d.id)}/download`}
                  download
                  title="Scarica file"
                >
                  <Icon name="download" size={11}/>
                </a>
              )}
              {d.hasFile && (
                <Btn
                  variant="ai"
                  size="sm"
                  onClick={() => setExtractTarget(d)}
                  title="Estrai dati AI dal documento"
                >
                  <Icon name="sparkle" size={11}/>
                </Btn>
              )}
              {/* FASE 16 (sessione 91) — Bottone firma con gate-ruoli: abilitato
                  solo se il ruolo dell'utente è tra i signerRoles del doc_type.
                  Non abilitato → disabled + tooltip coi ruoli richiesti. */}
              {d.hasFile && d._server && docTypeRequiresSignature(d.type, seedCustom) && d.signatureStatus !== 'signed' && (() => {
                const signerRoles = docTypeSignerRoles(d.type, seedCustom);
                const canSign = canSignDocTypeClient(myRoleIds, signerRoles);
                return (
                  <Btn
                    variant={canSign ? 'primary' : 'ghost'}
                    size="sm"
                    disabled={!canSign}
                    onClick={() => { if (canSign) setSignTarget(d); }}
                    title={canSign
                      ? (d.signatureStatus === 'pending' ? 'Firma in attesa: clicca per gestire' : 'Firma il documento')
                      : `Firma riservata ai ruoli: ${signerRoles.join(', ') || '—'}`}
                  >
                    <Icon name="signature" size={11}/> Firma
                  </Btn>
                );
              })()}
              {d._server && (
                <Btn variant="ghost" size="sm" onClick={() => handleDelete(d.id, d.title)} title="Elimina">
                  <Icon name="trash" size={11}/>
                </Btn>
              )}
            </div>
          </div>
        ))}
      </div>
      <AddDocumentModal open={addOpen} onClose={() => setAddOpen(false)} projectId={p.id} />
      {extractTarget && <AiExtractModal doc={extractTarget} onClose={() => setExtractTarget(null)} />}
      {signTarget && (
        <SignatureRequestModal
          doc={signTarget}
          prefill={{
            email: user?.email || '',
            name: user?.name || '',
            role: docTypeSignerRoles(signTarget.type, seedCustom).find((r) => (user?.roleIds || []).includes(r)) || '',
          }}
          onClose={() => setSignTarget(null)}
          onComplete={() => { refreshServerDocs(); window.dispatchEvent(new Event('my_tasks_changed')); }}
        />
      )}
      {readerTarget && (
        <DocReaderModal
          doc={readerTarget}
          onClose={() => setReaderTarget(null)}
          onRequestSign={(d) => { setReaderTarget(null); setSignTarget(d); }}
        />
      )}
    </div>
  );
}

/**
 * Modale di lettura documento MULTI-FORMATO. Renderizza il documento in-app
 * cosi come appare aprendolo:
 *  - PDF        → pdf.js (impaginato fedele, canvas + text layer selezionabile)
 *  - Word .docx → mammoth (conversione in HTML)
 *  - Excel/csv  → SheetJS (tabelle HTML)
 *  - immagini   → visualizzazione diretta
 *  - txt/json   → testo formattato
 * Per i formati con testo: ricerca di stringhe + evidenzia una porzione e poni
 * una domanda libera all'AI. I formati non gestiti hanno un fallback download.
 */
function DocReaderModal({ doc, onClose, onRequestSign }) {
  const { user, pushToast, seedCustom } = useStore();
  const [loading, setLoading] = React.useState(true);
  const [err, setErr] = React.useState(null);
  const [unsupported, setUnsupported] = React.useState(false);
  const [numPages, setNumPages] = React.useState(0);
  const [bodyHtml, setBodyHtml] = React.useState('');
  const [query, setQuery] = React.useState('');
  const [matchInfo, setMatchInfo] = React.useState(0);
  const [selText, setSelText] = React.useState('');
  const [question, setQuestion] = React.useState('');
  const [aiLoading, setAiLoading] = React.useState(false);
  const [aiAnswer, setAiAnswer] = React.useState(null);
  const viewerRef = React.useRef(null);

  const fileUrl = `/api/documents/${encodeURIComponent(doc.id)}/download`;

  // Tipo di rendering, dedotto dall'estensione del file.
  const kind = React.useMemo(() => {
    const name = (doc.originalFilename || doc.title || '').toLowerCase();
    const ext = name.includes('.') ? name.split('.').pop() : '';
    if (ext === 'pdf') return 'pdf';
    if (['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp'].includes(ext)) return 'image';
    if (['doc', 'docx'].includes(ext)) return 'docx';
    if (['xls', 'xlsx'].includes(ext)) return 'xlsx';
    if (ext === 'csv') return 'csv';
    if (ext === 'json') return 'json';
    if (ext === 'txt') return 'text';
    return 'pdf'; // default: i documenti generati dal sistema sono PDF
  }, [doc.originalFilename, doc.title]);

  const hasText = kind !== 'image';

  const escapeHtml = (s) =>
    String(s).replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));

  // Carica e renderizza il documento secondo il formato.
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true); setErr(null); setUnsupported(false); setNumPages(0); setBodyHtml('');
      try {
        if (kind === 'pdf') {
          const pdfjsLib = window.pdfjsLib;
          if (!pdfjsLib) throw new Error('pdf.js non caricato');
          pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
          const pdf = await pdfjsLib.getDocument({ url: fileUrl, withCredentials: true }).promise;
          if (cancelled) return;
          const container = viewerRef.current;
          if (!container) return;
          container.innerHTML = '';
          setNumPages(pdf.numPages);
          const measureCtx = document.createElement('canvas').getContext('2d');
          for (let n = 1; n <= pdf.numPages; n++) {
            if (cancelled) return;
            const page = await pdf.getPage(n);
            const viewport = page.getViewport({ scale: 1.5 });
            const pageDiv = document.createElement('div');
            pageDiv.style.cssText = `position:relative;margin:0 auto 14px;width:${viewport.width}px;height:${viewport.height}px;box-shadow:0 1px 8px rgba(0,0,0,.28);background:#fff;`;
            const canvas = document.createElement('canvas');
            canvas.width = viewport.width; canvas.height = viewport.height;
            canvas.style.display = 'block';
            pageDiv.appendChild(canvas);
            const textDiv = document.createElement('div');
            textDiv.className = 'textLayer';
            textDiv.style.cssText = `position:absolute;left:0;top:0;width:${viewport.width}px;height:${viewport.height}px;`;
            pageDiv.appendChild(textDiv);
            container.appendChild(pageDiv);
            await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
            // Text layer: span trasparenti posizionati sopra il canvas. Ogni span
            // riceve uno scaleX (larghezza reale dal PDF / larghezza naturale del
            // font misurata via canvas) → la selezione resta allineata al testo.
            const tc = await page.getTextContent();
            for (const item of tc.items) {
              if (!item.str) continue;
              const tx = pdfjsLib.Util.transform(viewport.transform, item.transform);
              const fh = Math.hypot(tx[2], tx[3]);
              if (fh <= 0) continue;
              const span = document.createElement('span');
              span.textContent = item.str;
              span.style.left = `${tx[4]}px`;
              span.style.top = `${tx[5] - fh}px`;
              span.style.fontSize = `${fh}px`;
              span.style.fontFamily = 'sans-serif';
              textDiv.appendChild(span);
              const targetW = (item.width || 0) * viewport.scale;
              measureCtx.font = `${fh}px sans-serif`;
              const naturalW = measureCtx.measureText(item.str).width;
              if (targetW > 0 && naturalW > 0) {
                span.style.transform = `scaleX(${targetW / naturalW})`;
              }
            }
          }
        } else if (kind === 'image') {
          // nessun pre-caricamento: <img> punta direttamente all'url
        } else if (kind === 'docx') {
          if (!window.mammoth) throw new Error('mammoth non caricato');
          const res = await fetch(fileUrl, { credentials: 'same-origin' });
          if (!res.ok) throw new Error(`HTTP ${res.status}`);
          const out = await window.mammoth.convertToHtml({ arrayBuffer: await res.arrayBuffer() });
          if (!cancelled) setBodyHtml(out?.value || '<p>(documento vuoto)</p>');
        } else if (kind === 'xlsx' || kind === 'csv') {
          if (!window.XLSX) throw new Error('SheetJS non caricato');
          const res = await fetch(fileUrl, { credentials: 'same-origin' });
          if (!res.ok) throw new Error(`HTTP ${res.status}`);
          const wb = window.XLSX.read(await res.arrayBuffer(), { type: 'array' });
          const parts = wb.SheetNames.map((sn) =>
            `<h3 style="margin:12px 0 4px;color:#1f3a5f;font-size:12px;">${escapeHtml(sn)}</h3>` +
            window.XLSX.utils.sheet_to_html(wb.Sheets[sn]));
          if (!cancelled) setBodyHtml(parts.join('') || '<p>(foglio vuoto)</p>');
        } else if (kind === 'json' || kind === 'text') {
          const res = await fetch(fileUrl, { credentials: 'same-origin' });
          if (!res.ok) throw new Error(`HTTP ${res.status}`);
          let txt = await res.text();
          if (kind === 'json') { try { txt = JSON.stringify(JSON.parse(txt), null, 2); } catch { /* raw */ } }
          if (!cancelled) setBodyHtml(`<pre style="white-space:pre-wrap;font-size:12px;margin:0;">${escapeHtml(txt)}</pre>`);
        }
        if (!cancelled) setLoading(false);
      } catch (e) {
        if (cancelled) return;
        console.warn('[DocReader] load failed:', e);
        setUnsupported(true);
        setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [doc.id, kind]);

  // Inietta l'HTML dei formati non-PDF/non-image nel viewer.
  React.useEffect(() => {
    if (kind === 'pdf' || kind === 'image') return;
    const c = viewerRef.current;
    if (c && !loading && !unsupported) c.innerHTML = bodyHtml;
  }, [bodyHtml, loading, unsupported, kind]);

  // Ricerca: evidenzia le occorrenze (text layer per PDF, text node walk per HTML).
  React.useEffect(() => {
    const container = viewerRef.current;
    if (!container || !hasText) return;
    const q = query.trim().toLowerCase();
    if (kind === 'pdf') {
      let count = 0, first = null;
      container.querySelectorAll('.textLayer span').forEach((sp) => {
        const hit = q.length > 0 && (sp.textContent || '').toLowerCase().includes(q);
        sp.style.background = hit ? 'rgba(255,210,0,.55)' : 'transparent';
        if (hit) { count++; if (!first) first = sp; }
      });
      setMatchInfo(count);
      if (first) first.scrollIntoView({ block: 'center', behavior: 'smooth' });
    } else {
      container.querySelectorAll('mark[data-find]').forEach((m) => {
        m.replaceWith(document.createTextNode(m.textContent));
      });
      container.normalize();
      if (!q) { setMatchInfo(0); return; }
      let count = 0, first = null;
      const walk = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
      const targets = [];
      let node;
      while ((node = walk.nextNode())) {
        if (node.nodeValue && node.nodeValue.toLowerCase().includes(q)) targets.push(node);
      }
      targets.forEach((tn) => {
        const text = tn.nodeValue;
        const lc = text.toLowerCase();
        const frag = document.createDocumentFragment();
        let i = 0;
        while (i < text.length) {
          const idx = lc.indexOf(q, i);
          if (idx < 0) { frag.appendChild(document.createTextNode(text.slice(i))); break; }
          if (idx > i) frag.appendChild(document.createTextNode(text.slice(i, idx)));
          const mk = document.createElement('mark');
          mk.setAttribute('data-find', '1');
          mk.style.background = 'rgba(255,210,0,.6)';
          mk.textContent = text.slice(idx, idx + q.length);
          frag.appendChild(mk);
          if (!first) first = mk;
          count++;
          i = idx + q.length;
        }
        if (tn.parentNode) tn.parentNode.replaceChild(frag, tn);
      });
      setMatchInfo(count);
      if (first) first.scrollIntoView({ block: 'center', behavior: 'smooth' });
    }
  }, [query, numPages, bodyHtml, loading, kind, hasText]);

  // Cattura ESPLICITA della selezione (chiamata dal tasto "Cattura selezione").
  // Il tasto usa onMouseDown+preventDefault per non rubare il focus → la
  // selezione del documento resta viva al momento del click e viene fissata
  // in state, cosi NON si perde quando poi si clicca nell'input della domanda.
  const captureSelection = () => {
    try {
      const s = String(window.getSelection() || '').trim();
      if (!s) {
        pushToast({ title: 'Evidenzia prima col mouse una parte del documento', tone: 'warn' });
        return;
      }
      setSelText(s);
    } catch { /* noop */ }
  };

  const askAi = async () => {
    const sel = selText.trim();
    const dom = question.trim();
    if (!sel) { pushToast({ title: 'Evidenzia prima una parte del documento', tone: 'warn' }); return; }
    if (!dom) { pushToast({ title: 'Scrivi la tua domanda', tone: 'warn' }); return; }
    setAiLoading(true); setAiAnswer(null);
    try {
      const res = await fetch('/api/ai/complete', {
        method: 'POST',
        headers: { 'content-type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({
          system: "Sei un assistente esperto di documentazione CAPEX e governance degli investimenti industriali, sulla piattaforma Veridanto. L'utente sta leggendo un documento di progetto, evidenzia una porzione e ti pone una domanda specifica su di essa. Rispondi in italiano, in modo chiaro e professionale, basandoti sul testo evidenziato. Non inventare dati non presenti.",
          messages: [{
            role: 'user',
            content: `Documento: "${doc.title}" (tipo ${doc.type}).\n\nPorzione evidenziata dall'utente:\n"""\n${sel.slice(0, 4000)}\n"""\n\nDomanda dell'utente: ${dom}`,
          }],
        }),
      });
      const j = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(j?.detail || j?.error || `HTTP ${res.status}`);
      setAiAnswer(j.data?.text || '(nessuna risposta dall\'AI)');
    } catch (e) {
      pushToast({ title: 'AI non disponibile', desc: String(e?.message || e).slice(0, 200), tone: 'err' });
    } finally {
      setAiLoading(false);
    }
  };

  // FASE 16 (sessione 91) — Firma dalla modale di lettura, con gate-ruoli.
  const docReqSig = docTypeRequiresSignature(doc.type, seedCustom);
  const docSignerRoles = docTypeSignerRoles(doc.type, seedCustom);
  const docCanSign = canSignDocTypeClient(user?.roleIds || [], docSignerRoles);
  const docSigned = doc.signatureStatus === 'signed';

  const footer = (
    <div className="row" style={{ gap: 8, width: '100%', alignItems: 'center' }}>
      {docReqSig && (
        docSigned ? (
          <Chip kind="ok" dot>Documento firmato</Chip>
        ) : docCanSign ? (
          <Btn variant="primary" size="sm" onClick={() => onRequestSign && onRequestSign(doc)}>
            <Icon name="signature" size={12}/> Firma documento
          </Btn>
        ) : (
          <span style={{ fontSize: 11, color: 'var(--text-3)' }}>
            ✍ Firma riservata ai ruoli: {docSignerRoles.join(', ') || '—'}
          </span>
        )
      )}
      <div style={{ flex: 1 }} />
      <a className="btn ghost sm" href={fileUrl} download>
        <Icon name="download" size={11}/> Scarica file
      </a>
      <Btn variant="ghost" size="sm" onClick={onClose}>Chiudi</Btn>
    </div>
  );

  return (
    <Modal open={true} onClose={onClose} title={`Lettura — ${doc.title}`} size="lg" footer={footer}>
      <style>{`
        .textLayer{position:absolute;left:0;top:0;overflow:hidden;line-height:1;opacity:1;text-size-adjust:none;forced-color-adjust:none;transform-origin:0 0;}
        .textLayer span,.textLayer br{color:transparent;position:absolute;white-space:pre;cursor:text;transform-origin:0% 0%;}
        .textLayer span.markedContent{top:0;height:0;}
        .textLayer ::selection{background:rgba(0,120,255,.45);}
        #doc-viewer table{border-collapse:collapse;font-size:11px;margin:2px 0;}
        #doc-viewer td,#doc-viewer th{border:1px solid #ccc;padding:2px 6px;}
        #doc-viewer img{max-width:100%;}
      `}</style>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        <div className="row" style={{ gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
          {hasText && (
            <input
              className="input sm"
              placeholder="Cerca nel documento…"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              disabled={unsupported}
              style={{ flex: '1 1 200px', minWidth: 160 }}
            />
          )}
          {query.trim() && hasText && !unsupported && (
            <span className="mono" style={{ fontSize: 11, color: 'var(--text-2)' }}>
              {matchInfo} {matchInfo === 1 ? 'risultato' : 'risultati'}
            </span>
          )}
          {numPages > 0 && (
            <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{numPages} pag.</span>
          )}
        </div>

        {loading && <div style={{ padding: 20, color: 'var(--text-2)', fontSize: 12 }}>Caricamento documento…</div>}
        {!loading && err && <div style={{ padding: 16, color: 'var(--danger, #c0392b)', fontSize: 12 }}>{err}</div>}
        {!loading && unsupported && (
          <EmptyState
            title="Anteprima non disponibile per questo formato"
            desc="Il documento non è visualizzabile in-app — usa Scarica file per aprirlo."
          />
        )}
        {!loading && !unsupported && kind === 'image' && (
          <div style={{ textAlign: 'center', maxHeight: '55vh', overflow: 'auto', background: 'var(--bg-2)', borderRadius: 6, padding: 10 }}>
            <img src={fileUrl} alt={doc.title} style={{ maxWidth: '100%' }} />
          </div>
        )}
        <div
          id="doc-viewer"
          ref={viewerRef}
          style={{
            maxHeight: '50vh', overflowY: 'auto',
            display: loading || err || unsupported || kind === 'image' ? 'none' : 'block',
            padding: kind === 'pdf' ? '12px 6px' : '12px 14px',
            background: 'var(--bg-2)', borderRadius: 6, userSelect: 'text', fontSize: 12, lineHeight: 1.6,
          }}
        />

        {hasText && !unsupported && !err && (
          <div style={{ border: '1px solid var(--line)', borderRadius: 6, padding: '10px 12px' }}>
            <div className="eyebrow" style={{ marginBottom: 6 }}>✨ Chiedi all'AI su una parte del documento</div>
            <div className="row" style={{ gap: 8, alignItems: 'center', marginBottom: 6, flexWrap: 'wrap' }}>
              {/* onMouseDown+preventDefault: il tasto non ruba la selezione → al
                  click la selezione del documento è ancora viva e viene fissata. */}
              <Btn
                variant="ghost"
                size="sm"
                onMouseDown={(e) => e.preventDefault()}
                onClick={captureSelection}
                title="Evidenzia col mouse una parte del documento, poi premi qui per catturarla"
              >
                Cattura selezione
              </Btn>
              {selText.trim() ? (
                <React.Fragment>
                  <span style={{ fontSize: 10.5, color: 'var(--text-0)', flex: '1 1 200px' }}>
                    Catturato: «{selText.trim().slice(0, 100)}{selText.trim().length > 100 ? '…' : ''}»
                  </span>
                  <Btn variant="ghost" size="sm" onClick={() => { setSelText(''); setAiAnswer(null); }} title="Pulisci selezione catturata">✕</Btn>
                </React.Fragment>
              ) : (
                <span style={{ fontSize: 10.5, color: 'var(--text-2)' }}>
                  Evidenzia il testo nel documento, poi premi "Cattura selezione".
                </span>
              )}
            </div>
            <div className="row" style={{ gap: 8, alignItems: 'center' }}>
              <input
                className="input sm"
                placeholder="La tua domanda sulla parte catturata…"
                value={question}
                onChange={(e) => setQuestion(e.target.value)}
                onKeyDown={(e) => { if (e.key === 'Enter') askAi(); }}
                style={{ flex: 1 }}
              />
              <Btn variant="ai" size="sm" onClick={askAi} disabled={aiLoading || !selText.trim() || !question.trim()}>
                <Icon name="sparkle" size={11}/> {aiLoading ? 'AI…' : 'Chiedi'}
              </Btn>
            </div>
            {(aiLoading || aiAnswer) && (
              <div style={{ marginTop: 8, background: 'var(--bg-2)', borderRadius: 6, padding: '8px 10px' }}>
                {aiLoading
                  ? <div style={{ fontSize: 12, color: 'var(--text-2)' }}>Generazione risposta…</div>
                  : <div style={{ fontSize: 12, lineHeight: 1.55, whiteSpace: 'pre-wrap' }}>{aiAnswer}</div>}
              </div>
            )}
          </div>
        )}
      </div>
    </Modal>
  );
}

/**
 * Helper user-friendly error messages: mappa codici tecnici a messaggi business.
 * Sprint 3.5: nascondiamo dettagli tecnici provider/model dall'utente CFO/PM.
 */
function mapAiErrorToUser(errorCode, detail) {
  if (errorCode === 'no_ai_key') {
    return 'Nessuna chiave AI configurata. Vai in Impostazioni → Provider AI per impostarne una.';
  }
  if (errorCode === 'primary_provider_no_key') {
    return 'Il provider AI selezionato non ha una chiave configurata. Verifica le Impostazioni.';
  }
  if (errorCode === 'extract_failed') {
    return 'Non è stato possibile leggere il documento. Verifica che il file sia un PDF testuale o riprova più tardi.';
  }
  if (errorCode === 'doc_no_file' || errorCode === 'source_doc_no_file') {
    return 'Il documento selezionato non ha un file allegato.';
  }
  if (errorCode === 'file_gone') {
    return 'Il file non è più disponibile nello storage. Ricaricalo per riprovare.';
  }
  if (errorCode === 'no_variables') {
    return 'Questo template non ha variabili da compilare con AI.';
  }
  if (errorCode === 'validation_error') {
    return 'Dati non validi. Verifica i campi del form.';
  }
  // Fallback: detail server (potrebbe contenere termini tecnici, ma è meglio di nulla)
  return detail || 'Operazione AI non riuscita. Riprova più tardi.';
}

/**
 * FASE 2c.4 (Sprint 3 cliente): modal per estrazione AI dati strutturati.
 * Usa lo schema dichiarato nel DocType (`aiMetadataSchema`) come target. Se il
 * DocType non ha schema, l'utente può aggiungere fields ad-hoc.
 */
function AiExtractModal({ doc, onClose }) {
  const { user, pushToast, seedCustom } = useStore();

  // Cerca docType.aiMetadataSchema dal seed Customizing
  const docType = React.useMemo(() => {
    return (seedCustom?.DOC_TYPES || []).find(dt => dt.code === doc.type);
  }, [doc.type, seedCustom]);

  // Schema target: priorità a docType.aiMetadataSchema, altrimenti fields ad-hoc
  const initialFields = React.useMemo(() => {
    const schema = docType?.aiMetadataSchema;
    if (schema && typeof schema === 'object') {
      return Object.entries(schema).map(([name, type]) => ({ name, type }));
    }
    // Default per Nomina RL del flusso CAPEX cliente
    if (/NOMINA_RL/.test(doc.type)) {
      return [
        { name: 'nome', type: 'string' },
        { name: 'cognome', type: 'string' },
        { name: 'ruolo', type: 'string' },
        { name: 'ente', type: 'string' },
      ];
    }
    if (/OFFERTA/.test(doc.type)) {
      return [
        { name: 'vendor', type: 'string' },
        { name: 'importo', type: 'number' },
        { name: 'valuta', type: 'string' },
        { name: 'data_offerta', type: 'date' },
        { name: 'oggetto', type: 'string' },
      ];
    }
    return [{ name: 'campo1', type: 'string' }];
  }, [docType, doc.type]);

  const [fields, setFields] = React.useState(initialFields);
  const [instruction, setInstruction] = React.useState('');
  const [extracting, setExtracting] = React.useState(false);
  const [result, setResult] = React.useState(null);

  const updateField = (i, key, val) => {
    setFields(prev => prev.map((f, idx) => (idx === i ? { ...f, [key]: val } : f)));
  };
  const addField = () => setFields(prev => [...prev, { name: `campo${prev.length + 1}`, type: 'string' }]);
  const removeField = (i) => setFields(prev => prev.filter((_, idx) => idx !== i));

  async function handleExtract() {
    if (fields.length === 0) {
      pushToast({ title: 'Definisci almeno un campo', tone: 'warn' });
      return;
    }
    setExtracting(true);
    setResult(null);
    try {
      const properties = {};
      for (const f of fields) {
        if (!f.name.trim()) continue;
        properties[f.name.trim()] = { type: f.type };
      }
      const body = {
        docId: doc.id,
        schema: { type: 'object', properties, required: [] },
      };
      if (instruction.trim()) body.instruction = instruction.trim();
      const res = await fetch('/api/ai/extract', {
        method: 'POST',
        headers: { 'content-type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify(body),
      });
      const j = await res.json().catch(() => ({}));
      if (!res.ok) {
        // Errori user-friendly: niente detail tecnico, ma messaggio chiaro
        const userMsg = mapAiErrorToUser(j?.error, j?.detail);
        throw new Error(userMsg);
      }
      setResult(j.data);
      pushToast({
        title: 'Estrazione AI completata',
        desc: `${Object.keys(j.data?.values || {}).length} campi · ${(j.data?.latencyMs / 1000).toFixed(1)}s`,
        tone: 'ok',
      });
    } catch (err) {
      pushToast({ title: 'Estrazione AI non riuscita', desc: err?.message?.slice(0, 240) || 'Riprova più tardi.', tone: 'err' });
    } finally {
      setExtracting(false);
    }
  }

  return (
    <Modal
      open
      onClose={onClose}
      title={`Estrai dati AI · ${doc.type}`}
      size="lg"
      footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose}>Chiudi</Btn>
          <Btn variant="ai" size="sm" onClick={handleExtract} disabled={extracting}>
            {extracting ? 'Estrazione…' : <><Icon name="sparkle" size={12}/> Esegui estrazione</>}
          </Btn>
        </>
      }
    >
      <div className="col" style={{ gap: 14 }}>
        <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
          Documento: <code>{doc.title || doc.id}</code> · {doc.mimeType} · {(doc.fileSize / 1024).toFixed(1)} KB
        </div>

        <div>
          <div className="row" style={{ alignItems: 'center', marginBottom: 8 }}>
            <div className="eyebrow">Schema target ({fields.length} campi)</div>
            <span className="spacer"/>
            <Btn variant="ghost" size="sm" onClick={addField}><Icon name="plus" size={11}/> campo</Btn>
          </div>
          <div className="col" style={{ gap: 6 }}>
            {fields.map((f, i) => (
              <div key={i} className="row" style={{ gap: 6 }}>
                <input
                  value={f.name}
                  onChange={e => updateField(i, 'name', e.target.value)}
                  placeholder="nome_campo"
                  style={{ flex: 2 }}
                />
                <select value={f.type} onChange={e => updateField(i, 'type', e.target.value)} style={{ flex: 1 }}>
                  <option value="string">string</option>
                  <option value="number">number</option>
                  <option value="boolean">boolean</option>
                  <option value="date">date</option>
                </select>
                <Btn variant="ghost" size="sm" onClick={() => removeField(i)}>
                  <Icon name="trash" size={11}/>
                </Btn>
              </div>
            ))}
          </div>
        </div>

        <div className="field">
          <label>Istruzione opzionale</label>
          <textarea
            rows={2}
            value={instruction}
            onChange={e => setInstruction(e.target.value)}
            placeholder="Es: 'Cerca i dati nella firma a fondo email'"
            style={{ width: '100%', resize: 'vertical' }}
          />
        </div>

        {result && (
          <div className="card flush" style={{ border: '1px solid var(--line)', borderRadius: 6, padding: 12 }}>
            <div className="row" style={{ alignItems: 'center', marginBottom: 8 }}>
              <Chip kind="ai"><Icon name="sparkle" size={11}/> AI Copilot</Chip>
              {result.attempts > 1 && (
                <Chip kind="info" title={`Estrazione riuscita al tentativo n.${result.attempts}`}>
                  retry × {result.attempts}
                </Chip>
              )}
              <span className="spacer"/>
              <span style={{ fontSize: 11, color: 'var(--text-3)' }}>
                {(result.latencyMs / 1000).toFixed(1)}s · {Object.keys(result.values || {}).length} campi
              </span>
            </div>
            <div className="eyebrow" style={{ marginBottom: 6 }}>Valori estratti</div>
            <table className="tbl dense">
              <thead><tr><th>Campo</th><th>Valore</th></tr></thead>
              <tbody>
                {Object.entries(result.values || {}).map(([k, v]) => (
                  <tr key={k}>
                    <td className="mono" style={{ fontSize: 11 }}>{k}</td>
                    <td>{typeof v === 'object' ? JSON.stringify(v) : String(v)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </Modal>
  );
}

/**
 * Helper: il DocType richiede firma? Cerca in seedCustom.DOC_TYPES per `code === type`.
 */
function docTypeRequiresSignature(typeCode, seedCustom) {
  if (!typeCode || !seedCustom) return false;
  const dt = (seedCustom.DOC_TYPES || []).find(d => d.code === typeCode);
  return !!(dt && (dt.requiresSignature || dt.digitalSignature));
}

/**
 * FASE 16 (sessione 91) — Ruoli abilitati a firmare un doc_type (gate-ruoli).
 * Sorgente: doc_type.signerRoles, configurato in Customizing → Tipi documento.
 * [] = firma libera (qualunque persona attiva).
 */
function docTypeSignerRoles(typeCode, seedCustom) {
  if (!typeCode || !seedCustom) return [];
  const dt = (seedCustom.DOC_TYPES || []).find(d => d.code === typeCode);
  const roles = dt && dt.signerRoles;
  return Array.isArray(roles) ? roles.filter(Boolean) : [];
}

/**
 * Gate-ruoli firma lato client (solo UX: abilita/disabilita il bottone).
 * L'enforcement reale è il 403 backend (lib/signature-authz.ts).
 *  - signerRoles vuoto → firma libera.
 *  - ruoli utente ignoti (myRoleIds vuoto) → NON disabilitare: non si blocca
 *    un firmatario legittimo per dati client incompleti; decide il backend.
 */
function canSignDocTypeClient(myRoleIds, signerRoles) {
  const required = Array.isArray(signerRoles) ? signerRoles.filter(Boolean) : [];
  if (required.length === 0) return true;
  const mine = Array.isArray(myRoleIds) ? myRoleIds.filter(Boolean) : [];
  if (mine.length === 0) return true;
  return required.some(r => mine.includes(r));
}

/**
 * FASE 2c.6 (Sprint 4): modal di richiesta firma elettronica.
 * Workflow:
 *  - Se non c'è una richiesta attiva: form per creare nuova richiesta (provider
 *    manual o mock) + recipient email/nome/ruolo + messaggio
 *  - Se richiesta pending: mostra status, link signatureUrl, polling automatico,
 *    bottoni "Carica PDF firmato" (manual) / "Annulla richiesta"
 *  - Se signed: mostra cert chain summary + signedBy/signedAt
 */
function SignatureRequestModal({ doc, onClose, onComplete, prefill }) {
  const { user, pushToast } = useStore();
  const [activeRequest, setActiveRequest] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  // Form fields — FASE 16 (sessione 91): se `prefill` è passato la firma è
  // avviata dall'utente stesso (gate-ruoli soddisfatto), quindi pre-compila
  // destinatario = utente loggato e default provider 'mock' (firma demo
  // immediata, auto-completata dopo ~5s).
  const [provider, setProvider] = React.useState(prefill ? 'mock' : 'manual');
  const [recipientEmail, setRecipientEmail] = React.useState(prefill?.email || '');
  const [recipientName, setRecipientName] = React.useState(prefill?.name || '');
  const [recipientRole, setRecipientRole] = React.useState(prefill?.role || '');
  const [message, setMessage] = React.useState('');
  const [submitting, setSubmitting] = React.useState(false);

  // Polling per status mock
  const [polling, setPolling] = React.useState(false);

  // Confirm signed upload (manual workflow)
  const [signedFile, setSignedFile] = React.useState(null);
  const [uploading, setUploading] = React.useState(false);
  const signedFileRef = React.useRef(null);

  // Carica eventuale richiesta attiva al mount
  const refreshActiveRequest = React.useCallback(async () => {
    setLoading(true);
    try {
      // /api/projects/[id]/documents non è strettamente necessario; basta provare a creare
      // la richiesta e ricevere 409 already_pending con existingRequestId, oppure
      // listing dedicato. Usiamo un fetch del singolo project_document via /api/documents/[id]
      // (esiste GET) per controllare signature_status; se = pending dobbiamo trovare
      // la richiesta attiva via listing. Per Sprint 4 MVP teniamo il check semplice.
      const res = await fetch(`/api/documents/${encodeURIComponent(doc.id)}`, { cache: 'no-store' });
      if (!res.ok) {
        setActiveRequest(null);
        return;
      }
      const j = await res.json();
      const sigStatus = j?.data?.signatureStatus;
      if (sigStatus !== 'pending' && sigStatus !== 'signed') {
        setActiveRequest(null);
        return;
      }
      // Per recuperare l'id signature_request: prova POST sign-request che ritorna 409
      // con existingRequestId. Approccio pragmatico Sprint 4 MVP.
      const probe = await fetch(`/api/documents/${encodeURIComponent(doc.id)}/sign-request`, {
        method: 'POST',
        headers: { 'content-type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ recipientEmail: 'probe@veridanto.local', recipientName: 'probe' }),
      });
      const pj = await probe.json().catch(() => ({}));
      if (probe.status === 409 && pj?.existingRequestId) {
        const sigRes = await fetch(`/api/signature-requests/${encodeURIComponent(pj.existingRequestId)}`, { cache: 'no-store' });
        const sigJ = await sigRes.json().catch(() => ({}));
        if (sigJ?.data) setActiveRequest(sigJ.data);
      } else if (probe.status === 201) {
        // Era stata creata per sbaglio dalla probe — la cancello (rare race condition Sprint 4)
        setActiveRequest(pj.data);
        if (pj?.data?.id) {
          await fetch(`/api/signature-requests/${encodeURIComponent(pj.data.id)}`, {
            method: 'DELETE',
            headers: { 'X-Actor-Persona-Id': user?.id || '' },
          });
          setActiveRequest(null);
        }
      } else {
        setActiveRequest(null);
      }
    } catch (e) {
      setActiveRequest(null);
    } finally {
      setLoading(false);
    }
  }, [doc.id, user?.id]);

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

  // Polling auto per richieste pending (per mock auto-complete dopo 5s)
  React.useEffect(() => {
    if (!activeRequest || activeRequest.status !== 'pending') return;
    setPolling(true);
    const interval = setInterval(async () => {
      try {
        const res = await fetch(`/api/signature-requests/${encodeURIComponent(activeRequest.id)}`, { cache: 'no-store' });
        const j = await res.json().catch(() => ({}));
        if (j?.data && j.data.status !== activeRequest.status) {
          setActiveRequest(j.data);
          if (j.data.status === 'signed') {
            pushToast({ title: 'Firma completata', desc: `Firmato da ${j.data.recipientName}`, tone: 'ok' });
            onComplete?.();
            clearInterval(interval);
          }
        }
      } catch {}
    }, 2000);
    return () => { clearInterval(interval); setPolling(false); };
  }, [activeRequest, pushToast, onComplete]);

  async function handleCreate() {
    if (!recipientEmail.trim() || !recipientName.trim()) {
      pushToast({ title: 'Dati mancanti', desc: 'Email e nome destinatario sono obbligatori', tone: 'warn' });
      return;
    }
    setSubmitting(true);
    try {
      const body = {
        provider,
        recipientEmail: recipientEmail.trim(),
        recipientName: recipientName.trim(),
      };
      if (recipientRole.trim()) body.recipientRole = recipientRole.trim();
      if (message.trim()) body.message = message.trim();
      const res = await fetch(`/api/documents/${encodeURIComponent(doc.id)}/sign-request`, {
        method: 'POST',
        headers: { 'content-type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify(body),
      });
      const j = await res.json().catch(() => ({}));
      if (!res.ok) {
        const msg = j?.detail || j?.error || `HTTP ${res.status}`;
        throw new Error(msg);
      }
      setActiveRequest(j.data);
      pushToast({ title: 'Richiesta firma creata', desc: `Inviata a ${j.data.recipientName}`, tone: 'ok' });
    } catch (err) {
      pushToast({ title: 'Errore creazione richiesta', desc: err?.message?.slice(0, 200) || 'errore', tone: 'err' });
    } finally {
      setSubmitting(false);
    }
  }

  async function handleCancel() {
    if (!activeRequest) return;
    if (!window.confirm('Annullare la richiesta di firma?')) return;
    try {
      const res = await fetch(`/api/signature-requests/${encodeURIComponent(activeRequest.id)}`, {
        method: 'DELETE',
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      pushToast({ title: 'Richiesta annullata', tone: 'warn' });
      setActiveRequest(null);
      onComplete?.();
    } catch (err) {
      pushToast({ title: 'Errore annullamento', desc: err?.message || 'errore', tone: 'err' });
    }
  }

  async function handleConfirmSignedUpload() {
    if (!activeRequest || !signedFile) return;
    setUploading(true);
    try {
      const fd = new FormData();
      fd.append('file', signedFile);
      fd.append('signatureRequestId', activeRequest.id);
      const res = await fetch(`/api/documents/${encodeURIComponent(doc.id)}/confirm-signed-upload`, {
        method: 'POST',
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
        body: fd,
      });
      const j = await res.json().catch(() => ({}));
      if (!res.ok) {
        const msg = j?.detail || j?.error || `HTTP ${res.status}`;
        throw new Error(msg);
      }
      pushToast({ title: 'Documento firmato caricato', desc: `v${j.data?.document?.fileVersion}`, tone: 'ok' });
      setActiveRequest({ ...activeRequest, status: 'signed' });
      onComplete?.();
      onClose();
    } catch (err) {
      pushToast({ title: 'Upload firma fallito', desc: err?.message?.slice(0, 200) || 'errore', tone: 'err' });
    } finally {
      setUploading(false);
    }
  }

  return (
    <Modal
      open
      onClose={onClose}
      title={`Firma elettronica · ${doc.type}`}
      size="md"
      footer={
        <>
          <Btn variant="ghost" size="sm" onClick={onClose}>Chiudi</Btn>
          {!activeRequest && (
            <Btn variant="primary" size="sm" onClick={handleCreate} disabled={submitting || !recipientEmail || !recipientName}>
              {submitting ? 'Invio…' : <><Icon name="signature" size={12}/> Richiedi firma</>}
            </Btn>
          )}
          {activeRequest && activeRequest.status === 'pending' && activeRequest.provider === 'manual' && (
            <Btn variant="primary" size="sm" onClick={handleConfirmSignedUpload} disabled={uploading || !signedFile}>
              {uploading ? 'Upload…' : <><Icon name="upload" size={12}/> Carica PDF firmato</>}
            </Btn>
          )}
          {activeRequest && (activeRequest.status === 'pending' || activeRequest.status === 'sent') && (
            <Btn variant="ghost" size="sm" onClick={handleCancel}>Annulla richiesta</Btn>
          )}
        </>
      }
    >
      <div className="col" style={{ gap: 14 }}>
        <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
          Documento: <code>{doc.title || doc.id}</code> · v{doc.fileVersion}
        </div>

        {loading ? (
          <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Caricamento…</div>
        ) : !activeRequest ? (
          <>
            <div className="field">
              <label>Tipo firma</label>
              <div className="row" style={{ gap: 4, flexWrap: 'wrap' }}>
                <button className={`btn sm ${provider==='manual'?'primary':'ghost'}`} onClick={() => setProvider('manual')}>Manuale</button>
                <button className={`btn sm ${provider==='mock'?'primary':'ghost'}`} onClick={() => setProvider('mock')} title="Provider demo: auto-firma dopo 5s">Demo / mock</button>
                <button
                  className="btn sm ghost"
                  disabled
                  title="Richiede contratto business e configurazione IT"
                  style={{ opacity: 0.5, cursor: 'not-allowed' }}
                >GoSign</button>
                <button
                  className="btn sm ghost"
                  disabled
                  title="Richiede contratto business e configurazione IT"
                  style={{ opacity: 0.5, cursor: 'not-allowed' }}
                >InfoCert</button>
                <button
                  className="btn sm ghost"
                  disabled
                  title="Richiede contratto business e configurazione IT"
                  style={{ opacity: 0.5, cursor: 'not-allowed' }}
                >Aruba</button>
              </div>
              <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 4 }}>
                {provider === 'manual'
                  ? "Manuale: scarichi il PDF, lo firmi (anche con altro tool), poi ricarichi qui il PDF firmato."
                  : "Demo: simulazione di un provider esterno con auto-completion dopo 5 secondi (utile per test). I provider reali (GoSign/InfoCert/Aruba) richiedono contratto business: contattare amministratore IT."}
              </div>
            </div>
            <div className="grid grid-2" style={{ gap: 10 }}>
              <div className="field">
                <label>Email destinatario *</label>
                <input value={recipientEmail} onChange={e => setRecipientEmail(e.target.value)} placeholder="firmatario@example.com" />
              </div>
              <div className="field">
                <label>Nome destinatario *</label>
                <input value={recipientName} onChange={e => setRecipientName(e.target.value)} placeholder="Nome Cognome" />
              </div>
            </div>
            <div className="field">
              <label>Ruolo (opzionale)</label>
              <input value={recipientRole} onChange={e => setRecipientRole(e.target.value)} placeholder="Es. Direttore Lavori, CFO, Procurement Manager…" />
            </div>
            <div className="field">
              <label>Messaggio (opzionale)</label>
              <textarea rows={3} value={message} onChange={e => setMessage(e.target.value)} placeholder="Note per il firmatario" style={{ width: '100%', resize: 'vertical' }} />
            </div>
          </>
        ) : (
          <>
            <div className="card flush" style={{ border: '1px solid var(--line)', borderRadius: 6, padding: 12 }}>
              <div className="row" style={{ alignItems: 'center', marginBottom: 8 }}>
                <Chip kind={activeRequest.status === 'signed' ? 'ok' : activeRequest.status === 'pending' ? 'warn' : 'err'} dot>
                  {activeRequest.status}
                </Chip>
                {activeRequest.provider === 'mock' && polling && (
                  <span style={{ fontSize: 11, color: 'var(--text-3)' }}>polling in corso…</span>
                )}
              </div>
              <div style={{ fontSize: 12, lineHeight: 1.6 }}>
                <div><strong>Destinatario:</strong> {activeRequest.recipientName} &lt;{activeRequest.recipientEmail}&gt;</div>
                {activeRequest.recipientRole && <div><strong>Ruolo:</strong> {activeRequest.recipientRole}</div>}
                {activeRequest.message && <div style={{ marginTop: 6, padding: 6, background: 'var(--bg-2)', borderRadius: 4, fontSize: 11.5 }}>{activeRequest.message}</div>}
                <div style={{ marginTop: 6 }}><strong>Creata:</strong> {new Date(activeRequest.createdAt).toLocaleString('it-IT')}</div>
                {activeRequest.completedAt && (
                  <div><strong>Completata:</strong> {new Date(activeRequest.completedAt).toLocaleString('it-IT')}</div>
                )}
              </div>
            </div>

            {activeRequest.status === 'pending' && activeRequest.provider === 'manual' && (
              <div className="card flush" style={{ border: '1px solid var(--line)', borderRadius: 6, padding: 12, background: 'var(--bg-2)' }}>
                <div className="eyebrow" style={{ marginBottom: 6 }}>Workflow manuale</div>
                <ol style={{ fontSize: 12, paddingLeft: 18, margin: 0, lineHeight: 1.6 }}>
                  <li>Scarica il PDF originale dal documento</li>
                  <li>Firma il PDF (cartaceo + scan, oppure tool digitale esterno)</li>
                  <li>Ricarica qui sotto il PDF firmato</li>
                </ol>
                <div className="field" style={{ marginTop: 10 }}>
                  <label style={{ fontSize: 10.5 }}>PDF firmato</label>
                  <input
                    ref={signedFileRef}
                    type="file"
                    accept=".pdf,application/pdf"
                    onChange={e => setSignedFile(e.target.files?.[0] || null)}
                    style={{ width: '100%' }}
                  />
                </div>
              </div>
            )}

            {activeRequest.status === 'pending' && activeRequest.provider === 'mock' && (
              <div style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 4, fontSize: 11.5 }}>
                <Icon name="info" size={11}/> Simulazione provider esterno: la richiesta sarà
                automaticamente firmata dopo {Math.round(5)}s dalla creazione. Questa modale
                aggiorna lo stato in tempo reale.
              </div>
            )}

            {activeRequest.status === 'signed' && (
              <div style={{ padding: 10, background: 'color-mix(in oklch, var(--ok) 12%, var(--bg-1))', border: '1px solid var(--ok)', borderRadius: 4, fontSize: 12 }}>
                ✓ Documento firmato con successo. Una nuova versione del documento è stata
                creata con lo stato firmato.
              </div>
            )}
          </>
        )}
      </div>
    </Modal>
  );
}

// Sessione 88 (FASE 15.A) — AI signal classification human-in-the-loop.
// Const SSoT per signal + label/color.
const COMM_SIGNAL_VALUES = ['delay', 'risk', 'action', 'decision', 'offer', 'ok'];
const COMM_SIGNAL_LABEL = { delay: 'ritardo', risk: 'rischio', action: 'azione richiesta', decision: 'decisione', offer: 'offerta', ok: 'informativa' };
const COMM_SIGNAL_CHIP_KIND = { delay: 'err', risk: 'warn', action: 'info', decision: 'info', offer: 'info', ok: 'ok' };

function CommTab({ comms: seedComms, projectId }) {
  const { pushToast, user } = useStore();
  const [addOpen, setAddOpen] = React.useState(false);
  const [liveComms, setLiveComms] = React.useState(null); // null=loading, []=empty, [...]=loaded
  const [reclassifyBusy, setReclassifyBusy] = React.useState(false);
  const [confirmingId, setConfirmingId] = React.useState(null); // commId loading state per riga
  const [suggestingId, setSuggestingId] = React.useState(null);
  const [signalPicker, setSignalPicker] = React.useState(null); // { comm } per modale cambio signal

  const loadLive = React.useCallback(async () => {
    try {
      const r = await fetch(`/api/communications?projectId=${encodeURIComponent(projectId)}`, {
        credentials: 'same-origin',
        cache: 'no-store',
        headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
      });
      if (!r.ok) { setLiveComms([]); return; }
      const j = await r.json();
      setLiveComms(Array.isArray(j.data) ? j.data : []);
    } catch {
      setLiveComms([]);
    }
  }, [projectId, user?.id]);

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

  // Comms da mostrare: live se caricato non vuoto, altrimenti seed fallback.
  const comms = (liveComms && liveComms.length > 0) ? liveComms : seedComms;
  const isLive = liveComms !== null;

  async function handleSuggestSignal(commId) {
    if (suggestingId || confirmingId) return;
    setSuggestingId(commId);
    try {
      const r = await fetch(`/api/communications/${encodeURIComponent(commId)}/suggest-signal`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        credentials: 'same-origin',
        body: '{}',
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        pushToast({ title: 'AI scoring fallito', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({ title: `AI suggerisce: ${COMM_SIGNAL_LABEL[j.classification?.signal] || j.classification?.signal}`, desc: `${j.classification?.confidence}% · ${j.classification?.provider}/${j.classification?.model}`, tone: 'info' });
      await loadLive();
    } catch (err) {
      pushToast({ title: 'Errore rete', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setSuggestingId(null);
    }
  }

  // Sessione 90 — Riapri triage: user_confirmed → ai_suggested
  async function handleReopenTriage(commId) {
    if (confirmingId) return;
    setConfirmingId(commId);
    try {
      const r = await fetch(`/api/communications/${encodeURIComponent(commId)}/reopen`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        credentials: 'same-origin',
        body: JSON.stringify({ mode: 'reset_to_ai' }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { pushToast({ title: 'Riapertura fallita', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' }); return; }
      pushToast({ title: 'Riaperta nel triage', desc: `${commId} torna in /communications come AI suggested`, tone: 'info' });
      await loadLive();
    } catch (err) {
      pushToast({ title: 'Errore rete', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setConfirmingId(null);
    }
  }

  async function handleConfirmSignal(commId, newSignal /* undefined=accept */) {
    if (confirmingId || suggestingId) return;
    setConfirmingId(commId);
    try {
      const r = await fetch(`/api/communications/${encodeURIComponent(commId)}/confirm-signal`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        credentials: 'same-origin',
        body: JSON.stringify(newSignal ? { signal: newSignal } : {}),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        pushToast({ title: 'Conferma fallita', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      // Sessione 90 — toast con workflow actions
      const actions = j?.workflow?.actions || [];
      const summary = actions.length === 0
        ? `Comm ${commId} confermato.`
        : actions.map((a) => {
            if (a.type === 'anomaly_flag') return `🚩 anomaly ${a.severity}`;
            if (a.type === 'audit_log') return `📝 ${a.reason}`;
            return a.type;
          }).join(' · ');
      pushToast({ title: newSignal ? `Evasa → ${COMM_SIGNAL_LABEL[newSignal]}` : 'Evasa (AI accettato)', desc: summary, tone: 'ok' });
      setSignalPicker(null);
      await loadLive();
    } catch (err) {
      pushToast({ title: 'Errore rete', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setConfirmingId(null);
    }
  }

  async function handleBulkReclassify() {
    if (reclassifyBusy) return;
    setReclassifyBusy(true);
    try {
      const r = await fetch('/api/communications/reclassify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        credentials: 'same-origin',
        body: JSON.stringify({ mode: 'project', projectId, force: false, limit: 50 }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        pushToast({ title: 'Ri-classifica fallita', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({
        title: `Ri-classifica completata`,
        desc: `${j.summary?.classified} classificate · ${j.summary?.skipped} saltate (già confermate) · ${j.summary?.failed} errori`,
        tone: j.summary?.failed > 0 ? 'warn' : 'ok',
      });
      await loadLive();
    } catch (err) {
      pushToast({ title: 'Errore rete', desc: String(err?.message || err), tone: 'err' });
    } finally {
      setReclassifyBusy(false);
    }
  }

  return (
    <div className="card">
      <div className="card-header">
        <div>
          <div className="title">Comunicazioni del progetto</div>
          {isLive && (
            <div className="desc">
              {comms.length === 0 ? 'Nessuna comunicazione' : `${comms.length} comunicazioni · ${comms.filter((c) => c.signalSource === 'ai_suggested').length} in attesa di conferma`}
            </div>
          )}
        </div>
        <div className="actions">
          <Btn variant="ghost" size="sm" onClick={() => setAddOpen(true)}><Icon name="plus" size={12}/> Nuova comunicazione</Btn>
          <Btn variant="ai" size="sm" onClick={handleBulkReclassify} disabled={reclassifyBusy || !isLive} data-action="comm-bulk-reclassify">
            <Icon name="sparkle" size={12}/> {reclassifyBusy ? 'Ri-classifica…' : 'Ri-classifica segnali'}
          </Btn>
        </div>
      </div>
      <div>
        {comms.length === 0 ? <EmptyState title="Nessuna comunicazione indicizzata" /> : comms.map((c) => {
          const sigSource = c.signalSource || 'user';
          const isAiSuggested = sigSource === 'ai_suggested';
          const isUserConfirmed = sigSource === 'user_confirmed';
          const chipKind = COMM_SIGNAL_CHIP_KIND[c.signal] || 'info';
          const chipLabel = COMM_SIGNAL_LABEL[c.signal] || c.signal;
          return (
            <div key={c.id} style={{ padding: '12px 14px', borderBottom: '1px solid var(--line)' }} data-comm-row={c.id} data-signal-source={sigSource}>
              <div className="row" style={{ gap: 6 }}>
                <Icon name={c.kind === 'email' ? 'mail' : 'users'} size={12} />
                <span style={{ fontWeight: 500 }}>{c.subject}</span>
                <div className="spacer" />
                <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{typeof c.date === 'string' && c.date.length > 10 ? c.date.slice(0, 10) : c.date}</span>
              </div>
              <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 2 }}>{c.from} → {c.to}</div>
              <div style={{ fontSize: 12, color: 'var(--text-1)', marginTop: 6 }}>{c.excerpt}</div>
              <div className="row" style={{ marginTop: 6, gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
                {/* Chip signal — sempre visibile */}
                <Chip kind={chipKind} dot>
                  {isAiSuggested ? '✨ ' : ''}{isUserConfirmed ? '✓ ' : ''}{chipLabel}
                  {(isAiSuggested || isUserConfirmed) && c.signalConfidence > 0 ? ` · ${c.signalConfidence}%` : ''}
                </Chip>

                {/* AI suggested → chip dorato + bottoni Accetta/Cambia */}
                {isAiSuggested && isLive && (
                  <>
                    <Chip kind="warn">AI in attesa conferma</Chip>
                    <Btn variant="ghost" size="xs" onClick={() => handleConfirmSignal(c.id)} disabled={confirmingId === c.id} data-action="comm-accept-signal" data-comm-id={c.id}>
                      <Icon name="check" size={10}/> {confirmingId === c.id ? '…' : 'Accetta'}
                    </Btn>
                    <Btn variant="ghost" size="xs" onClick={() => setSignalPicker({ comm: c })} disabled={confirmingId === c.id} data-action="comm-change-signal" data-comm-id={c.id}>
                      <Icon name="edit" size={10}/> Cambia
                    </Btn>
                  </>
                )}

                {/* User-provided (no AI ancora) → bottone "Suggerisci AI" */}
                {sigSource === 'user' && !isUserConfirmed && isLive && (
                  <Btn variant="ghost" size="xs" onClick={() => handleSuggestSignal(c.id)} disabled={suggestingId === c.id} data-action="comm-suggest-signal" data-comm-id={c.id}>
                    <Icon name="sparkle" size={10}/> {suggestingId === c.id ? 'AI…' : 'Suggerisci AI'}
                  </Btn>
                )}

                {/* Sessione 90 — User confirmed → bottone "Riapri triage" */}
                {isUserConfirmed && isLive && (
                  <Btn variant="ghost" size="xs" onClick={() => handleReopenTriage(c.id)} disabled={confirmingId === c.id} data-action="comm-reopen-triage" data-comm-id={c.id} title="Rimanda nel triage queue di /communications (signal AI mantenuto)">
                    <Icon name="refresh" size={10}/> {confirmingId === c.id ? 'Riapertura…' : 'Riapri triage'}
                  </Btn>
                )}

                {/* Rationale AI (se disponibile) */}
                {(isAiSuggested || isUserConfirmed) && c.signalRationale && (
                  <span style={{ fontSize: 10.5, color: 'var(--text-3)', fontStyle: 'italic', marginLeft: 8 }} title={c.signalRationale}>
                    {c.signalRationale.length > 80 ? c.signalRationale.slice(0, 80) + '…' : c.signalRationale}
                  </span>
                )}
              </div>
            </div>
          );
        })}
      </div>
      <AddCommModal open={addOpen} onClose={() => { setAddOpen(false); loadLive(); }} projectId={projectId} />

      {/* Signal picker modal */}
      {signalPicker && typeof window !== 'undefined' && window.Modal && (
        <window.Modal
          open
          onClose={() => setSignalPicker(null)}
          title={`Cambia signal — ${signalPicker.comm.id}`}
          size="sm"
          footer={<Btn variant="ghost" size="sm" onClick={() => setSignalPicker(null)}>Annulla</Btn>}
        >
          <div className="col" style={{ gap: 10 }}>
            <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
              AI ha suggerito <strong>{COMM_SIGNAL_LABEL[signalPicker.comm.signal]}</strong>. Seleziona il signal corretto:
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              {COMM_SIGNAL_VALUES.map((s) => (
                <button
                  key={s}
                  className={`btn ${s === signalPicker.comm.signal ? 'primary' : 'ghost'} sm`}
                  onClick={() => handleConfirmSignal(signalPicker.comm.id, s)}
                  disabled={confirmingId === signalPicker.comm.id}
                  data-signal-option={s}
                  style={{ justifyContent: 'flex-start', textAlign: 'left' }}
                >
                  <Chip kind={COMM_SIGNAL_CHIP_KIND[s]} dot>{COMM_SIGNAL_LABEL[s]}</Chip>
                  <span style={{ fontSize: 10.5, color: 'var(--text-3)', marginLeft: 8 }}>{s}</span>
                </button>
              ))}
            </div>
          </div>
        </window.Modal>
      )}
    </div>
  );
}

// ============================================================
// Baseline EVM — builder guidato della Planned Value curve (NO JSON).
// La PV curve si DERIVA da: BAC (budget) + periodo (date progetto) + forma
// (curva a S realistica / lineare) oppure dalle milestone del progetto.
// Anteprima grafica live + tabella punti leggibile. Output = stesso formato
// dell'API: [{date, plannedValueEur}] cumulativo non-decrescente.
// ============================================================
function buildPvCurve({ method, bac, startDate, endDate, granularity, milestones }) {
  const B = Math.max(0, Math.round(Number(bac) || 0));
  if (!B || !startDate || !endDate) return [];
  const startMs = new Date(startDate).getTime();
  const endMs = new Date(endDate).getTime();
  if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) return [];
  const fmt = (ms) => new Date(ms).toISOString().slice(0, 10);

  let pts = [];
  if (method === 'milestones') {
    const ms = (milestones || [])
      .filter((m) => m && m.due)
      .map((m) => ({ date: String(m.due).slice(0, 10), ms: new Date(m.due).getTime() }))
      .filter((m) => Number.isFinite(m.ms) && m.ms >= startMs && m.ms <= endMs)
      .sort((a, b) => a.ms - b.ms);
    if (ms.length === 0) return [];
    pts.push({ date: fmt(startMs), plannedValueEur: 0 });
    const n = ms.length;
    ms.forEach((m, i) => {
      if (m.ms > startMs) pts.push({ date: m.date, plannedValueEur: Math.round(B * ((i + 1) / n)) });
    });
    const last = pts[pts.length - 1];
    if (new Date(last.date).getTime() < endMs) pts.push({ date: fmt(endMs), plannedValueEur: B });
  } else {
    const stepMonths = granularity === 'quarterly' ? 3 : 1;
    const dates = [];
    let cur = new Date(startMs);
    let guard = 0;
    while (cur.getTime() < endMs && guard < 600) {
      dates.push(cur.getTime());
      const nx = new Date(cur); nx.setMonth(nx.getMonth() + stepMonths); cur = nx; guard += 1;
    }
    dates.push(endMs);
    // Cap a 120 punti (limite API): downsample mantenendo gli estremi.
    if (dates.length > 120) {
      const k = Math.ceil(dates.length / 119);
      const capped = dates.filter((_, i) => i % k === 0);
      if (capped[capped.length - 1] !== endMs) capped.push(endMs);
      dates.splice(0, dates.length, ...capped);
    }
    const span = endMs - startMs;
    // Curva a S: raised-cosine (avvio lento → picco centrale → chiusura lenta).
    const ease = (t) => (method === 'scurve' ? 0.5 - 0.5 * Math.cos(Math.PI * t) : t);
    pts = dates.map((m) => {
      const t = Math.min(1, Math.max(0, (m - startMs) / span));
      return { date: fmt(m), plannedValueEur: Math.round(B * ease(t)) };
    });
  }
  // dedup per data + estremi 0..B + enforce non-decrescente
  const out = [];
  for (const pt of pts) {
    if (out.length && out[out.length - 1].date === pt.date) out[out.length - 1] = { ...pt };
    else out.push({ ...pt });
  }
  if (out.length) out[0].plannedValueEur = 0;
  let maxv = 0;
  for (const pt of out) { if (pt.plannedValueEur < maxv) pt.plannedValueEur = maxv; else maxv = pt.plannedValueEur; }
  if (out.length) out[out.length - 1].plannedValueEur = B;
  return out;
}

function PvPreviewChart({ curve, bac }) {
  if (!Array.isArray(curve) || curve.length < 2) {
    return <div style={{ fontSize: 11.5, color: 'var(--text-3)', padding: '24px 0', textAlign: 'center' }}>Anteprima non disponibile — completa BAC e periodo qui sopra.</div>;
  }
  const W = 480, H = 150, padL = 8, padR = 8, padT = 10, padB = 16;
  const innerW = W - padL - padR, innerH = H - padT - padB;
  const B = bac || curve[curve.length - 1].plannedValueEur || 1;
  const startMs = new Date(curve[0].date).getTime();
  const endMs = new Date(curve[curve.length - 1].date).getTime();
  const xs = (ms) => padL + ((ms - startMs) / (endMs - startMs || 1)) * innerW;
  const ys = (v) => padT + innerH - (v / B) * innerH;
  const line = curve.map((pt, i) => `${i === 0 ? 'M' : 'L'}${xs(new Date(pt.date).getTime()).toFixed(1)},${ys(pt.plannedValueEur).toFixed(1)}`).join(' ');
  const area = `${line} L${xs(endMs).toFixed(1)},${ys(0).toFixed(1)} L${xs(startMs).toFixed(1)},${ys(0).toFixed(1)} Z`;
  return (
    <svg width={W} height={H} style={{ maxWidth: '100%', display: 'block' }}>
      <path d={area} fill="color-mix(in oklch, var(--accent) 14%, transparent)" />
      <path d={line} fill="none" stroke="var(--accent)" strokeWidth="2" />
      {curve.map((pt, i) => <circle key={i} cx={xs(new Date(pt.date).getTime())} cy={ys(pt.plannedValueEur)} r="2.5" fill="var(--accent)" />)}
      <line x1={padL} y1={ys(0)} x2={W - padR} y2={ys(0)} stroke="var(--line)" strokeWidth="1" />
    </svg>
  );
}

function BaselineBuilderModal({ project, existing, saving, onClose, onSubmit }) {
  const { user } = useStore();
  const today = new Date().toISOString().slice(0, 10);
  const defEnd = (project?.end || new Date(Date.now() + 365 * 24 * 3600 * 1000).toISOString().slice(0, 10)).slice(0, 10);
  const [bacEur, setBacEur] = React.useState(String(existing?.bacEur ?? project?.budget ?? ''));
  const [baselineDate, setBaselineDate] = React.useState(today);
  const [startDate, setStartDate] = React.useState((project?.start || today).slice(0, 10));
  const [endDate, setEndDate] = React.useState(defEnd);
  const [method, setMethod] = React.useState('scurve');
  const [granularity, setGranularity] = React.useState('monthly');
  const [notes, setNotes] = React.useState('');
  const [milestones, setMilestones] = React.useState(null); // null = loading

  React.useEffect(() => {
    if (!project?.id) return;
    let cancelled = false;
    fetch(`/api/projects/${encodeURIComponent(project.id)}/milestones`, {
      cache: 'no-store',
      headers: user?.id ? { 'X-Actor-Persona-Id': user.id } : {},
    })
      .then((r) => r.json())
      .then((j) => { if (!cancelled) setMilestones(Array.isArray(j.data) ? j.data : []); })
      .catch(() => { if (!cancelled) setMilestones([]); });
    return () => { cancelled = true; };
  }, [project?.id, user?.id]);

  const msInRange = React.useMemo(() => {
    const s = new Date(startDate).getTime(), e = new Date(endDate).getTime();
    return (milestones || []).filter((m) => m?.due && new Date(m.due).getTime() >= s && new Date(m.due).getTime() <= e);
  }, [milestones, startDate, endDate]);

  // Se "da milestone" non ha checkpoint nel periodo, ripiega su curva a S.
  React.useEffect(() => {
    if (method === 'milestones' && milestones !== null && msInRange.length === 0) setMethod('scurve');
  }, [method, milestones, msInRange.length]);

  const curve = React.useMemo(
    () => buildPvCurve({ method, bac: bacEur, startDate, endDate, granularity, milestones }),
    [method, bacEur, startDate, endDate, granularity, milestones],
  );

  const B = Math.round(Number(bacEur) || 0);
  const valid = B > 0 && !!baselineDate && curve.length >= 2;

  return (
    <Modal open onClose={onClose} title={existing ? 'Re-baseline · nuova versione' : 'Crea baseline iniziale'} size="lg" footer={
      <>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
        <Btn variant="primary" size="sm" disabled={!valid || saving} onClick={() => onSubmit({ bacEur, baselineDate, plannedValueCurve: curve, notes })} data-testid="bp-baseline-submit">
          <Icon name="check" size={11}/> {saving ? 'Salvataggio…' : 'Salva baseline'}
        </Btn>
      </>
    }>
      <div className="col" style={{ gap: 12 }}>
        <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
          La <strong>baseline</strong> è il piano di spesa nel tempo (Planned Value). Il motore EVM la confronta con l'avanzamento reale dai SAL per calcolare CPI/SPI/EAC. Compila i campi: <strong>la curva viene costruita per te</strong>, niente da scrivere a mano.
        </div>
        <div className="grid grid-3">
          <div className="field"><label>BAC € (budget totale)</label><input type="number" value={bacEur} onChange={(e) => setBacEur(e.target.value)} data-testid="bp-baseline-bac"/></div>
          <div className="field"><label>Inizio</label><input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)}/></div>
          <div className="field"><label>Fine</label><input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)}/></div>
        </div>
        <div className="field">
          <label>Forma della curva</label>
          <div className="row" style={{ gap: 6, flexWrap: 'wrap' }}>
            <button type="button" className={`btn sm ${method === 'scurve' ? 'primary' : 'ghost'}`} onClick={() => setMethod('scurve')}>Curva a S (realistica)</button>
            <button type="button" className={`btn sm ${method === 'linear' ? 'primary' : 'ghost'}`} onClick={() => setMethod('linear')}>Lineare</button>
            <button type="button" className={`btn sm ${method === 'milestones' ? 'primary' : 'ghost'}`} onClick={() => setMethod('milestones')} disabled={milestones !== null && msInRange.length === 0} title={milestones !== null && msInRange.length === 0 ? 'Nessuna milestone con data nel periodo' : undefined}>Da milestone{milestones === null ? '…' : ` (${msInRange.length})`}</button>
          </div>
        </div>
        {method !== 'milestones' ? (
          <div className="field">
            <label>Granularità</label>
            <div className="row" style={{ gap: 6 }}>
              <button type="button" className={`btn sm ${granularity === 'monthly' ? 'primary' : 'ghost'}`} onClick={() => setGranularity('monthly')}>Mensile</button>
              <button type="button" className={`btn sm ${granularity === 'quarterly' ? 'primary' : 'ghost'}`} onClick={() => setGranularity('quarterly')}>Trimestrale</button>
            </div>
          </div>
        ) : (
          <div style={{ fontSize: 11.5, color: msInRange.length ? 'var(--text-2)' : 'var(--warn)', padding: '8px 10px', background: 'var(--bg-2)', borderRadius: 6 }}>
            {milestones === null
              ? 'Caricamento milestone…'
              : `Il BAC viene distribuito equamente sui ${msInRange.length} checkpoint del progetto, cumulativo fino al BAC a fine progetto.`}
          </div>
        )}

        <div style={{ padding: 8, background: 'var(--bg-2)', borderRadius: 6 }}>
          <div className="eyebrow" style={{ marginBottom: 4 }}>Anteprima Planned Value (cumulato → {fmtEUR(B, true)})</div>
          <PvPreviewChart curve={curve} bac={B} />
        </div>

        {curve.length >= 2 && (
          <div>
            <div className="eyebrow" style={{ marginBottom: 4 }}>Punti generati ({curve.length})</div>
            <div style={{ maxHeight: 160, overflowY: 'auto', border: '1px solid var(--line)', borderRadius: 6 }}>
              <table className="tbl">
                <thead><tr><th>Data</th><th className="num">PV cumulato</th><th className="num">% BAC</th></tr></thead>
                <tbody>
                  {curve.map((pt, i) => (
                    <tr key={i}>
                      <td className="mono" style={{ fontSize: 11 }}>{pt.date}</td>
                      <td className="num">{fmtEUR(pt.plannedValueEur, true)}</td>
                      <td className="num">{B > 0 ? Math.round((pt.plannedValueEur / B) * 100) : 0}%</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        )}

        <div className="field"><label>Note (opzionali)</label><textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} placeholder="es. Re-baseline post change order CO-01"/></div>
      </div>
    </Modal>
  );
}

function BudgetTab({ p, extraBudget = [] }) {
  const { user, pushToast, seedCustom } = useStore();
  const [addOpen, setAddOpen] = React.useState(false);
  // FASE 2c RBAC (sessione 103) — gating "Re-baseline" / "Crea baseline":
  // muta budget baseline + PV curve del progetto → project.update.
  const canRebaseline = window.can('project.update', user, seedCustom);
  // Sessione 81 / D2 — EVM live server-side da /api/projects/[id]/evm-kpi.
  // Sostituisce il CPI proxy (sessione 70) con calcoli reali da SAL + baseline.
  const [evm, setEvm] = React.useState(null);
  const [evmLoading, setEvmLoading] = React.useState(false);
  const [salForm, setSalForm] = React.useState(null); // {open, periodEnd, progressBp, cumulativeActualEur, notes}

  // Sessione 85 #12 — Re-baseline state
  const [baselineForm, setBaselineForm] = React.useState(null); // {bacEur, baselineDate, notes, curveText}
  const [baselineSaving, setBaselineSaving] = React.useState(false);

  React.useEffect(() => {
    if (!p?.id) return;
    let cancelled = false;
    setEvmLoading(true);
    (async () => {
      try {
        const r = await fetch(`/api/projects/${encodeURIComponent(p.id)}/evm-kpi`, {
          headers: { 'X-Actor-Persona-Id': user?.id || '' },
        });
        if (!r.ok) { if (!cancelled) setEvm(null); return; }
        const j = await r.json();
        if (cancelled) return;
        setEvm(j.data || null);
      } catch { if (!cancelled) setEvm(null); }
      finally { if (!cancelled) setEvmLoading(false); }
    })();
    return () => { cancelled = true; };
  }, [p?.id, user?.id]);

  const submitSal = async () => {
    if (!salForm || !p?.id) return;
    // L'utente inserisce l'avanzamento in PERCENTUALE (0..100, virgola o punto
    // ammessi). Il formato di storage/wire resta in basis points (×100): la
    // conversione è qui, l'utente non vede mai i basis points.
    const pct = parseFloat(String(salForm.progressPct ?? '').replace(',', '.'));
    if (!Number.isFinite(pct) || pct < 0 || pct > 100) {
      pushToast({ title: 'Avanzamento non valido', desc: 'Inserisci una percentuale tra 0 e 100 (es. 55).', tone: 'err' });
      return;
    }
    const progressBp = Math.round(pct * 100);
    try {
      const r = await fetch(`/api/projects/${encodeURIComponent(p.id)}/sal`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({
          periodEnd: salForm.periodEnd,
          progressBp,
          cumulativeActualEur: parseInt(salForm.cumulativeActualEur, 10),
          cumulativeInvoicedEur: parseInt(salForm.cumulativeInvoicedEur || salForm.cumulativeActualEur, 10),
          status: salForm.status || 'draft',
          notes: salForm.notes || null,
        }),
      });
      const j = await r.json();
      if (!r.ok) {
        pushToast({ title: 'SAL non creato', desc: j?.error || `HTTP ${r.status}`, tone: 'err' });
        return;
      }
      pushToast({ title: 'SAL creato', desc: `${j.data.id} · ${j.data.status}`, tone: 'ok' });
      setSalForm(null);
      // Re-fetch EVM
      const re = await fetch(`/api/projects/${encodeURIComponent(p.id)}/evm-kpi`, {
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
      });
      const reJ = await re.json();
      setEvm(reJ.data || null);
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'POST SAL fallito', tone: 'err' });
    }
  };

  // Sessione 85 #12 — Re-baseline submit
  const submitBaseline = async (payload) => {
    if (!payload || !p?.id) return;
    const { bacEur, baselineDate, plannedValueCurve, notes } = payload;
    if (!bacEur || !baselineDate || !Array.isArray(plannedValueCurve) || plannedValueCurve.length < 2) {
      pushToast({ title: 'Dati incompleti', desc: 'BAC, data baseline e curva (≥2 punti) richiesti.', tone: 'err' });
      return;
    }
    setBaselineSaving(true);
    try {
      const r = await fetch(`/api/projects/${encodeURIComponent(p.id)}/baseline`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({
          bacEur: parseInt(bacEur, 10),
          baselineDate,
          plannedValueCurve,
          notes: notes || null,
        }),
      });
      const j = await r.json();
      if (!r.ok) { pushToast({ title: 'Baseline fallita', desc: j?.error || j?.detail || `HTTP ${r.status}`, tone: 'err' }); return; }
      pushToast({ title: `Baseline v${j.data.version} creata`, desc: `BAC ${fmtEUR(j.data.bacEur, true)}`, tone: 'ok' });
      setBaselineForm(null);
      // Re-fetch evm
      const re = await fetch(`/api/projects/${encodeURIComponent(p.id)}/evm-kpi`, {
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
      });
      setEvm((await re.json()).data || null);
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'POST fallito', tone: 'err' });
    } finally { setBaselineSaving(false); }
  };


  const result = evm?.result;
  const hasEvm = !!result && !!evm?.baseline;

  const baseRows = [
    { v: 'Budget baseline (BAC)', a: hasEvm ? evm.baseline.bacEur : p.budget, p: '100%' },
    { v: 'Impegnato (PO emessi)', a: p.committed, p: (p.committed/p.budget*100).toFixed(1) + '%' },
    { v: 'Speso (fatturato)', a: p.spent, p: (p.spent/p.budget*100).toFixed(1) + '%' },
    { v: 'Residuo disponibile', a: p.budget - p.committed, p: ((p.budget-p.committed)/p.budget*100).toFixed(1) + '%' },
  ];
  const rows = [...baseRows, ...extraBudget];

  return (
    <div className="grid" style={{ gridTemplateColumns: '2fr 1fr', gap: 14 }}>
      <div className="card">
        <div className="card-header">
          <div className="title">Budget & Earned Value {hasEvm && <span style={{ fontSize: 10, color: 'var(--ok)', marginLeft: 6 }}>· server-side</span>}</div>
          <div className="actions">
            <Btn variant="ghost" size="sm" onClick={() => setSalForm({ open: true, periodEnd: new Date().toISOString().slice(0,10), progressPct: '', cumulativeActualEur: '', cumulativeInvoicedEur: '', status: 'draft', notes: '' })} data-testid="bp-new-sal-btn"><Icon name="plus" size={12}/> Nuovo SAL</Btn>
            <Btn variant="ghost" size="sm" onClick={() => setAddOpen(true)}><Icon name="plus" size={12}/> Aggiungi voce</Btn>
          </div>
        </div>
        <table className="tbl">
          <thead><tr><th>Voce</th><th>Tipo</th><th className="num">Importo</th><th className="num">% Budget</th></tr></thead>
          <tbody>{rows.map((r, i) => <tr key={i}><td>{r.v}</td><td style={{color:'var(--text-2)', fontSize:11.5}}>{r.kind || 'baseline'}</td><td className="num">{fmtEUR(r.a)}</td><td className="num">{r.p}</td></tr>)}</tbody>
        </table>
        {hasEvm && evm.salHistory.length > 0 && (
          <div style={{ marginTop: 14, padding: 14, borderTop: '1px solid var(--line)' }}>
            <div className="eyebrow" style={{ marginBottom: 8 }}>SAL storici ({evm.salHistory.length})</div>
            <table className="tbl" data-testid="bp-sal-history">
              <thead><tr><th>Periodo</th><th>Progress</th><th className="num">Actual</th><th>Stato</th></tr></thead>
              <tbody>
                {evm.salHistory.slice(0, 8).map((s) => (
                  <tr key={s.id}>
                    <td className="mono" style={{ fontSize: 11 }}>{s.periodEnd}</td>
                    <td><Meter value={s.progressBp/100} tone={s.progressBp >= 8000 ? 'ok' : 'warn'} /></td>
                    <td className="num mono">{fmtEUR(s.cumulativeActualEur)}</td>
                    <td><Chip kind={s.status === 'approved' ? 'ok' : s.status === 'draft' ? 'info' : 'warn'} dot>{s.status}</Chip></td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}

        {/* Sessione 85 #12 — Schedule baseline card */}
        <div style={{ marginTop: 14, padding: 14, borderTop: '1px solid var(--line)' }}>
          <div className="row" style={{ justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
            <div className="eyebrow">Schedule baseline {hasEvm && <span style={{ color: 'var(--text-3)' }}>v{evm.baseline.version} · {evm.baseline.baselineDate}</span>}</div>
            <Btn
              variant="ghost"
              size="xs"
              disabled={!canRebaseline}
              onClick={() => {
                if (!canRebaseline) return;
                setBaselineForm({ open: true });
              }}
              title={canRebaseline ? undefined : window.whyDisabled('project.update')}
              data-testid="bp-rebaseline-btn"
            >
              <Icon name="refresh" size={11}/> {hasEvm ? 'Re-baseline' : 'Crea baseline'}
            </Btn>
          </div>
          {hasEvm && (
            <div style={{ fontSize: 11.5, color: 'var(--text-2)' }}>
              BAC <b>{fmtEUR(evm.baseline.bacEur, true)}</b> · {evm.baseline.plannedValueCurve.length} punti PV curve · approvato {evm.baseline.approvedAt?.slice(0, 10) || '—'}
            </div>
          )}
        </div>

        <AddBudgetRowModal open={addOpen} onClose={() => setAddOpen(false)} projectId={p.id} totalBudget={p.budget} />
      </div>
      <div className="card">
        <div className="card-header"><div className="title">KPI EVM {hasEvm && <span style={{fontSize:10, color:'var(--ok)', marginLeft:6}} data-testid="bp-evm-source">{result.source.latestSalPeriod ? `SAL ${result.source.latestSalPeriod}` : 'no SAL'}</span>}</div></div>
        <div className="card-body">
          {evmLoading ? (
            <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Caricamento EVM…</div>
          ) : !hasEvm ? (
            <div style={{ fontSize: 11.5, color: 'var(--text-3)' }} data-testid="bp-evm-empty">
              Nessuna baseline + SAL configurati. Crea un SAL per attivare CPI/SPI/EAC reali.
            </div>
          ) : (
            <div className="grid grid-2" style={{ gap: 10 }} data-testid="bp-evm-grid">
              <Stat
                label="CPI"
                value={result.cpi != null ? result.cpi.toFixed(2) : '—'}
                delta={result.cpi != null ? (result.cpi >= 1 ? 'entro target' : 'sotto target') : 'no SAL'}
                tone={result.cpi != null ? (result.cpi >= 1 ? 'up' : 'down') : ''}
              />
              <Stat
                label="SPI"
                value={result.spi != null ? result.spi.toFixed(2) : '—'}
                delta={result.spi != null ? (result.spi >= 1 ? 'on schedule' : 'in ritardo') : 'no PV'}
                tone={result.spi != null ? (result.spi >= 1 ? 'up' : 'down') : ''}
              />
              <Stat label="BAC" value={fmtEUR(result.bacEur, true)} />
              <Stat
                label="EAC"
                value={result.eacEur != null ? fmtEUR(result.eacEur, true) : '—'}
                delta={result.vacEur != null ? (result.vacEur >= 0 ? `VAC +${fmtEUR(result.vacEur, true)}` : `VAC ${fmtEUR(result.vacEur, true)}`) : ''}
                tone={result.vacEur != null ? (result.vacEur >= 0 ? 'up' : 'down') : ''}
              />
              <Stat label="EV (Earned)" value={fmtEUR(result.evEur, true)} delta={`AC ${fmtEUR(result.acEur, true)}`} />
              <Stat label="PV (Planned)" value={fmtEUR(result.pvEur, true)} delta={`SV ${fmtEUR(result.svEur, true)}`} />
            </div>
          )}

          {/* Sessione 85 #12 — PV/EV/AC chart SVG */}
          {hasEvm && (
            <PvEvChart baseline={evm.baseline} salHistory={evm.salHistory} asOfDate={result.asOfDate}/>
          )}
        </div>
      </div>

      {/* SAL form modal */}
      {salForm && (
        <Modal open={!!salForm} onClose={() => setSalForm(null)} title="Nuovo SAL — progress claim" size="md" footer={
          <>
            <Btn variant="ghost" size="sm" onClick={() => setSalForm(null)}>Annulla</Btn>
            <Btn variant="primary" size="sm" onClick={submitSal} data-testid="bp-sal-submit">Crea SAL</Btn>
          </>
        }>
          <div className="col" style={{ gap: 10 }}>
            <div className="field">
              <label>Periodo fine (YYYY-MM-DD)</label>
              <input value={salForm.periodEnd} onChange={(e) => setSalForm({...salForm, periodEnd: e.target.value})} placeholder="2026-05-31" />
            </div>
            <div className="field">
              <label>Avanzamento (%)</label>
              <div className="row" style={{ gap: 6, alignItems: 'center' }}>
                <input
                  type="number" min="0" max="100" step="0.1" inputMode="decimal"
                  value={salForm.progressPct}
                  onChange={(e) => setSalForm({...salForm, progressPct: e.target.value})}
                  placeholder="55"
                  style={{ flex: 1 }}
                  data-testid="bp-sal-progress"
                />
                <span style={{ fontSize: 13, color: 'var(--text-2)' }}>%</span>
              </div>
              <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 4 }}>
                Percentuale di avanzamento al periodo (0–100). Es. 55 = 55% completato.
              </div>
            </div>
            <div className="field">
              <label>Cumulative Actual € (cost speso totale al periodo)</label>
              <input value={salForm.cumulativeActualEur} onChange={(e) => setSalForm({...salForm, cumulativeActualEur: e.target.value})} placeholder="2500000" />
            </div>
            <div className="field">
              <label>Stato</label>
              <select value={salForm.status} onChange={(e) => setSalForm({...salForm, status: e.target.value})}>
                <option value="draft">draft</option>
                <option value="submitted">submitted</option>
                <option value="approved">approved</option>
              </select>
            </div>
            <div className="field">
              <label>Note</label>
              <textarea value={salForm.notes} onChange={(e) => setSalForm({...salForm, notes: e.target.value})} rows={2} placeholder="es. SAL apr 2026 — ritardo collaudo"></textarea>
            </div>
            <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
              Solo SAL `approved` contribuiscono a CPI/SPI/EAC live. Draft visibile con flag `includeDrafts=true`.
            </div>
          </div>
        </Modal>
      )}

      {/* Baseline builder (no JSON): la PV curve è derivata da BAC + periodo + forma (S/lineare) o milestone */}
      {baselineForm && (
        <BaselineBuilderModal
          project={p}
          existing={hasEvm ? evm.baseline : null}
          saving={baselineSaving}
          onClose={() => setBaselineForm(null)}
          onSubmit={submitBaseline}
        />
      )}
    </div>
  );
}

// ============================================================
// Sessione 85 #12 — PvEvChart SVG component
// Mostra PV curve (line) + AC step (line) + EV marker (dot) + asOfDate vertical line.
// X axis: date (start → end baseline), Y axis: eur cumulative.
// ============================================================
function PvEvChart({ baseline, salHistory, asOfDate }) {
  if (!baseline || !Array.isArray(baseline.plannedValueCurve) || baseline.plannedValueCurve.length === 0) {
    return null;
  }
  const W = 520, H = 220, padL = 60, padR = 16, padT = 16, padB = 36;
  const innerW = W - padL - padR;
  const innerH = H - padT - padB;

  const curve = [...baseline.plannedValueCurve].sort((a, b) => new Date(a.date) - new Date(b.date));
  const startMs = new Date(curve[0].date).getTime();
  const endMs = new Date(curve[curve.length - 1].date).getTime();
  const bacEur = baseline.bacEur || curve[curve.length - 1].plannedValueEur;

  const xScale = (ms) => padL + ((ms - startMs) / (endMs - startMs || 1)) * innerW;
  const yScale = (v) => padT + innerH - (v / bacEur) * innerH;

  // PV path
  const pvPath = curve.map((pt, i) => {
    const x = xScale(new Date(pt.date).getTime());
    const y = yScale(pt.plannedValueEur);
    return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`;
  }).join(' ');

  // AC step path from SAL approved (cumulativeActualEur over periodEnd)
  const approvedSal = (salHistory || []).filter(s => s.status === 'approved').sort((a, b) => new Date(a.periodEnd) - new Date(b.periodEnd));
  const acPath = approvedSal.length > 0
    ? approvedSal.map((s, i) => {
        const x = xScale(new Date(s.periodEnd).getTime());
        const y = yScale(s.cumulativeActualEur);
        return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`;
      }).join(' ')
    : '';

  // EV dots (BAC × progressBp/10000)
  const evDots = approvedSal.map(s => ({
    x: xScale(new Date(s.periodEnd).getTime()),
    y: yScale(bacEur * (s.progressBp / 10000)),
    eur: Math.round(bacEur * (s.progressBp / 10000)),
    period: s.periodEnd,
  }));

  const asOfMs = asOfDate ? new Date(asOfDate).getTime() : null;
  const asOfX = asOfMs && asOfMs >= startMs && asOfMs <= endMs ? xScale(asOfMs) : null;

  // Y axis ticks (4)
  const yTicks = [0, 0.25, 0.5, 0.75, 1].map(p => ({ v: bacEur * p, y: yScale(bacEur * p) }));
  // X axis ticks (curve dates ogni 2)
  const xTicks = curve.filter((_, i) => i % Math.max(1, Math.floor(curve.length / 4)) === 0);

  const fmtEurShort = (v) => {
    if (v >= 1000000) return (v / 1000000).toFixed(1) + 'M';
    if (v >= 1000) return (v / 1000).toFixed(0) + 'k';
    return v.toString();
  };

  return (
    <div style={{ marginTop: 14, padding: 8, background: 'var(--bg-2)', borderRadius: 6 }} data-testid="bp-pvev-chart">
      <div className="eyebrow" style={{ marginBottom: 4 }}>Curve PV / EV / AC</div>
      <svg width={W} height={H} style={{ maxWidth: '100%', display: 'block' }}>
        {/* Grid horizontal */}
        {yTicks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={W - padR} y1={t.y} y2={t.y} stroke="var(--line)" strokeDasharray="2,2" strokeOpacity="0.5"/>
            <text x={padL - 6} y={t.y + 3} textAnchor="end" fontSize="9" fill="var(--text-3)" fontFamily="var(--font-mono)">{fmtEurShort(t.v)}€</text>
          </g>
        ))}
        {/* X axis ticks */}
        {xTicks.map((pt, i) => {
          const x = xScale(new Date(pt.date).getTime());
          return (
            <g key={i}>
              <line x1={x} x2={x} y1={H - padB} y2={H - padB + 3} stroke="var(--text-3)"/>
              <text x={x} y={H - padB + 14} textAnchor="middle" fontSize="8" fill="var(--text-3)" fontFamily="var(--font-mono)">{pt.date.slice(5)}</text>
            </g>
          );
        })}
        {/* PV curve (blue line) */}
        <path d={pvPath} fill="none" stroke="#3b82f6" strokeWidth="2"/>
        {/* AC step (red line) */}
        {acPath && <path d={acPath} fill="none" stroke="#ef4444" strokeWidth="2" strokeDasharray="4,2"/>}
        {/* EV dots (green) */}
        {evDots.map((d, i) => (
          <circle key={i} cx={d.x} cy={d.y} r="4" fill="#10b981" stroke="white" strokeWidth="1.5">
            <title>{d.period} · EV {fmtEurShort(d.eur)}€</title>
          </circle>
        ))}
        {/* AsOfDate vertical line */}
        {asOfX != null && (
          <g>
            <line x1={asOfX} x2={asOfX} y1={padT} y2={H - padB} stroke="var(--text-2)" strokeWidth="1" strokeDasharray="1,3"/>
            <text x={asOfX} y={padT - 4} textAnchor="middle" fontSize="9" fill="var(--text-2)">today</text>
          </g>
        )}
      </svg>
      <div className="row" style={{ gap: 14, fontSize: 11, color: 'var(--text-2)', marginTop: 4, justifyContent: 'center' }}>
        <span><span style={{ display: 'inline-block', width: 18, height: 2, background: '#3b82f6', verticalAlign: 'middle', marginRight: 4 }}/> PV (baseline)</span>
        <span><span style={{ display: 'inline-block', width: 18, height: 2, background: '#ef4444', verticalAlign: 'middle', marginRight: 4, borderTop: '2px dashed' }}/> AC (actual cost)</span>
        <span><span style={{ display: 'inline-block', width: 10, height: 10, background: '#10b981', borderRadius: '50%', verticalAlign: 'middle', marginRight: 4 }}/> EV (earned)</span>
      </div>
    </div>
  );
}

// ============================================================
// FASE 19 — ProjectAnalyzerInline (s132): runner condiviso AI brief
// ------------------------------------------------------------
// Componente unificato che esegue PROJECT_ANALYZER e ne renderizza il brief.
// Usato sia da `ProjectAnalyzerCard` (Box Panoramica, mode='compact') sia da
// `AITab` (mode='full'). Sostituisce il vecchio comportamento "card →
// navigate al tab Insight AI" con esecuzione inline.
// Compact: severity + score + summary + top 3 risks (no actions, no headlines).
// Full: tutto (headlines, risks, actions, meta footer).
// ============================================================
function ProjectAnalyzerInline({ project, compact = false }) {
  const { user, pushToast } = useStore();
  const [running, setRunning] = React.useState(false);
  const [brief, setBrief] = React.useState(null);
  const [meta, setMeta] = React.useState(null);
  const [error, setError] = React.useState(null);

  const readUserPrefs = () => {
    if (!user?.id || typeof window === 'undefined') return {};
    try {
      const raw = window.localStorage.getItem(`lgs.ai.user.${user.id}.preferences`);
      if (!raw) return {};
      return JSON.parse(raw) || {};
    } catch { return {}; }
  };

  const runAnalyzer = React.useCallback(async () => {
    if (running) return;
    setRunning(true);
    setError(null);
    const prefs = readUserPrefs();
    const headers = { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' };
    if (prefs.mode && prefs.mode !== 'default') headers['X-AI-Mode-Override'] = prefs.mode;
    if (prefs.provider && prefs.provider !== 'default') headers['X-AI-Provider-Override'] = prefs.provider;
    try {
      const r = await fetch('/api/ai/project-analyzer/run', {
        method: 'POST',
        headers,
        credentials: 'same-origin',
        body: JSON.stringify({ projectId: project.id }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || j.detail || 'HTTP ' + r.status);
      setBrief(j.brief);
      setMeta({
        provider: j.provider,
        model: j.model,
        ctxSize: j.ctxSize,
        durationMs: j.durationMs,
        droppedCount: j.droppedCount,
      });
      pushToast({ title: 'Analisi completata', desc: `${j.provider} · ${j.brief?.risks?.length || 0} risks · ${j.brief?.actions?.length || 0} actions`, tone: 'ok' });
    } catch (e) {
      const msg = String(e?.message || e);
      setError(msg);
      pushToast({ title: 'PROJECT_ANALYZER fallito', desc: msg.slice(0, 200), tone: 'err' });
    } finally {
      setRunning(false);
    }
  }, [project.id, running, user?.id, pushToast]);

  const severityKind = (s) => s === 'critical' ? 'err' : s === 'high' ? 'warn' : s === 'medium' ? 'info' : 'ok';
  const ownerLabel = { pm: 'PM', cfo: 'CFO', buyer: 'Buyer', plant: 'Plant', eng: 'Engineering', auditor: 'Auditor' };
  const priorityKind = (pr) => pr === 'high' ? 'err' : pr === 'medium' ? 'warn' : 'info';

  // --- Render: idle / loading / error / brief ---
  // Bottone "Esegui": sempre presente; per compact resta inline; per full sta nell'header parent.
  const runBtn = (
    <Btn variant="ai" size="sm" onClick={runAnalyzer} disabled={running} data-action="run-analyzer">
      {running
        ? (<><Icon name="refresh" size={11}/> Analisi in corso…</>)
        : brief
          ? (<><Icon name="refresh" size={11}/> Rigenera</>)
          : (<><Icon name="sparkle" size={11}/> Esegui PROJECT_ANALYZER</>)}
    </Btn>
  );

  // Idle state
  if (!brief && !running && !error) {
    return (
      <div style={{ padding: compact ? 12 : 16, textAlign: 'center', color: 'var(--text-2)', fontSize: 12.5, lineHeight: 1.6 }}>
        <p style={{ margin: '0 0 12px 0' }}>
          L'agent <strong>PROJECT_ANALYZER</strong> esamina anomalie aperte, RdA pendenti, burn rate e milestone in ritardo del progetto <code>{project.code}</code> e produce raccomandazioni con provenance.
        </p>
        <div className="row" style={{ gap: 8, justifyContent: 'center' }}>{runBtn}</div>
      </div>
    );
  }

  // Error state
  if (error && !brief) {
    return (
      <div style={{ padding: 12 }}>
        <div style={{ padding: 10, border: '1px solid var(--err)', borderRadius: 6, background: 'color-mix(in oklch, var(--err) 6%, transparent)', color: 'var(--err)', fontSize: 12, marginBottom: 10 }}>
          <Icon name="alerts" size={11}/> Errore: {error}
        </div>
        <div className="row" style={{ gap: 8, justifyContent: 'center' }}>{runBtn}</div>
      </div>
    );
  }

  // Running but no brief yet
  if (running && !brief) {
    return (
      <div style={{ padding: 16, textAlign: 'center', color: 'var(--text-3)', fontSize: 12 }}>
        <Icon name="refresh" size={12}/> Analisi in corso…
      </div>
    );
  }

  // Brief loaded
  return (
    <div data-brief="loaded">
      {/* Header severity + score + bottone rigenera */}
      <div className="row" style={{ gap: 12, alignItems: 'center', marginBottom: 14 }}>
        <Chip kind={severityKind(brief.severity)} dot>{brief.severity}</Chip>
        <div>
          <div className="eyebrow">Score salute</div>
          <div style={{ fontSize: compact ? 18 : 22, fontWeight: 600, fontFamily: 'var(--font-display)' }}>
            {(brief.scoreBp / 100).toFixed(1)}<span style={{ fontSize: 12, color: 'var(--text-3)' }}>/100</span>
          </div>
        </div>
        <div style={{ flex: 1 }}>
          <Meter value={brief.scoreBp / 100} tone={brief.scoreBp >= 7000 ? 'ok' : brief.scoreBp >= 4000 ? 'info' : 'err'} thick />
        </div>
        {runBtn}
      </div>

      {/* Summary */}
      <div style={{ marginBottom: 14, fontSize: 12.5, lineHeight: 1.6, color: 'var(--text-1)' }}>
        {brief.summary}
      </div>

      {/* Headlines (solo full) */}
      {!compact && brief.headlines?.length > 0 && (
        <div style={{ marginBottom: 14 }}>
          <div className="eyebrow" style={{ marginBottom: 6 }}>Headlines</div>
          <ul style={{ margin: 0, paddingLeft: 18, fontSize: 12, lineHeight: 1.7 }}>
            {brief.headlines.map((h, i) => <li key={i}>{h}</li>)}
          </ul>
        </div>
      )}

      {/* Risks: top 3 in compact, tutti in full */}
      {brief.risks?.length > 0 && (
        <div style={{ marginBottom: 14 }}>
          <div className="eyebrow" style={{ marginBottom: 6 }}>
            {compact ? `Top rischi (${Math.min(3, brief.risks.length)}/${brief.risks.length})` : `Top rischi (${brief.risks.length})`}
          </div>
          <div className="col" style={{ gap: 8 }}>
            {(compact ? brief.risks.slice(0, 3) : brief.risks).map((r, i) => (
              <div key={i} style={{ padding: 10, border: '1px solid var(--line)', borderRadius: 6, background: 'var(--bg-2)' }}>
                <div className="row" style={{ gap: 8, marginBottom: 4 }}>
                  <Chip kind={severityKind(r.severity)} dot>{r.severity}</Chip>
                  <strong style={{ fontSize: 12.5 }}>{r.title}</strong>
                  <span className="spacer"/>
                  <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{r.entityType}:{r.entityId}</span>
                </div>
                <div style={{ fontSize: 12, color: 'var(--text-2)', lineHeight: 1.5 }}>{r.description}</div>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Actions (solo full) */}
      {!compact && brief.actions?.length > 0 && (
        <div style={{ marginBottom: 12 }}>
          <div className="eyebrow" style={{ marginBottom: 6 }}>Azioni suggerite ({brief.actions.length})</div>
          <div className="col" style={{ gap: 8 }}>
            {brief.actions.map((a, i) => (
              <div key={i} style={{ padding: 10, border: '1px solid var(--line)', borderRadius: 6 }}>
                <div className="row" style={{ gap: 8, marginBottom: 4 }}>
                  <Chip kind={priorityKind(a.priority)} dot>{a.priority}</Chip>
                  <strong style={{ fontSize: 12.5 }}>{a.title}</strong>
                  <span className="spacer"/>
                  <span className="mono" style={{ fontSize: 10.5, color: 'var(--accent)' }}>→ {ownerLabel[a.owner] || a.owner}</span>
                </div>
                <div style={{ fontSize: 12, color: 'var(--text-2)', lineHeight: 1.5 }}>{a.description}</div>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Meta footer (solo full) */}
      {!compact && meta && (
        <div className="mono" style={{ fontSize: 10, color: 'var(--text-3)', borderTop: '1px solid var(--line)', paddingTop: 8 }}>
          provider <strong>{meta.provider}</strong> · model {meta.model} · ctx {meta.ctxSize.anomalies}A/{meta.ctxSize.rda}R/{meta.ctxSize.milestones}M/{meta.ctxSize.communications}C · {meta.durationMs}ms{meta.droppedCount > 0 ? ` · ${meta.droppedCount} risks scartati (anti-hallucination)` : ''}
        </div>
      )}
    </div>
  );
}

// ============================================================
// FASE 19 — AITab con PROJECT_ANALYZER agent multi-provider
// s132 — Arricchito con dati operativi: team + RdA pendenti + alert.
// ============================================================
function AITab({ p, alerts, related, projectRda = [], projectOda = [] }) {
  const { user, navigate } = useStore();
  const projectAlerts = (alerts || []).filter((a) => a.project === p.id || a.entityId === p.id);
  const openAlerts = projectAlerts.filter((a) => a.status !== 'resolved' && a.status !== 'dismissed');
  const pendingRda = projectRda.filter((r) => r.status !== 'approved' && r.status !== 'cancelled' && r.status !== 'rejected');
  const pendingOda = projectOda.filter((o) => !['confirmed', 'delivered', 'closed', 'cancelled'].includes(o.status));

  // s132 — Team del progetto (fetch /api/projects/[id]/members come fa OverviewTab).
  const [team, setTeam] = React.useState([]);
  const [teamLoading, setTeamLoading] = React.useState(false);
  React.useEffect(() => {
    let cancelled = false;
    setTeamLoading(true);
    fetch(`/api/projects/${p.id}/members`, { headers: { 'X-Actor-Persona-Id': user?.id || '' } })
      .then((r) => r.json().catch(() => ({})))
      .then((j) => { if (!cancelled && Array.isArray(j?.data)) setTeam(j.data); })
      .catch(() => {})
      .finally(() => { if (!cancelled) setTeamLoading(false); });
    return () => { cancelled = true; };
  }, [p.id, user?.id]);

  return (
    <div className="col" style={{ gap: 14 }}>
      {/* PROJECT_ANALYZER brief (full) */}
      <div className="card">
        <div className="card-header">
          <div className="title"><Icon name="sparkle" size={12}/> PROJECT_ANALYZER — analisi context-aware</div>
          <div className="desc">Agent AI multi-provider · context da DB live (anomalie, RDA, milestone, comunicazioni)</div>
        </div>
        <div className="card-body">
          <ProjectAnalyzerInline project={p} compact={false} />
        </div>
        <div className="card-footer">
          <span className="mono" style={{ fontSize: 10, color: 'var(--text-3)' }}>
            human-in-the-loop · provider configurabile in Customizing → AI · override personale in Settings
          </span>
        </div>
      </div>

      {/* Dati operativi (s132): team + RdA pendenti + Alert + OdA pendenti */}
      <div className="grid" style={{ gridTemplateColumns: '1fr 1fr', gap: 14 }}>
        {/* Team del progetto */}
        <div className="card">
          <div className="card-header">
            <div className="title"><Icon name="team" size={12}/> Team del progetto</div>
            <div className="desc">{teamLoading ? 'Caricamento…' : `${team.length} membri attivi`}</div>
          </div>
          <div className="card-body">
            {!teamLoading && team.length === 0 && (
              <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Nessun membro registrato.</div>
            )}
            {team.length > 0 && (
              <div className="col" style={{ gap: 6 }}>
                {team.map((m) => (
                  <div key={m.id} className="row" style={{ gap: 8, alignItems: 'center', padding: '6px 4px', borderBottom: '1px dashed var(--line)' }}>
                    <div style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--accent-bg)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 10.5, fontWeight: 600, color: 'var(--accent)' }}>
                      {(m.personaName || m.personaId || '?').slice(0, 2).toUpperCase()}
                    </div>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 12.5, fontWeight: 500 }}>{m.personaName || m.personaId}</div>
                      <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{m.personaEmail || '—'}</div>
                    </div>
                    <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-2)' }}>{m.roleLabel || m.roleInProject || '—'}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>

        {/* Alert / Anomalie aperte */}
        <div className="card">
          <div className="card-header">
            <div className="title"><Icon name="alerts" size={12}/> Anomalie aperte</div>
            <div className="desc">{openAlerts.length} di {projectAlerts.length} totali</div>
            {openAlerts.length > 0 && (
              <div className="actions">
                <Btn variant="ghost" size="sm" onClick={() => navigate('alerts', p.id)}>Vai agli alert</Btn>
              </div>
            )}
          </div>
          <div className="card-body">
            {openAlerts.length === 0 && (
              <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Nessuna anomalia aperta — progetto in regola.</div>
            )}
            {openAlerts.length > 0 && (
              <div className="col" style={{ gap: 6 }}>
                {openAlerts.slice(0, 8).map((a) => (
                  <div key={a.id} style={{ padding: '6px 0', borderBottom: '1px dashed var(--line)', fontSize: 12 }}>
                    <div className="row" style={{ gap: 6, alignItems: 'center', marginBottom: 2 }}>
                      <Chip kind={a.severity === 'critical' ? 'err' : a.severity === 'high' ? 'warn' : 'info'} dot>{a.severity || 'medium'}</Chip>
                      <strong style={{ fontSize: 12 }}>{a.title || a.kind || a.id}</strong>
                    </div>
                    {a.description && <div style={{ fontSize: 11, color: 'var(--text-3)', marginLeft: 2 }}>{a.description}</div>}
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>

        {/* RdA pendenti */}
        <div className="card">
          <div className="card-header">
            <div className="title"><Icon name="rda" size={12}/> RdA pendenti</div>
            <div className="desc">{pendingRda.length} di {projectRda.length} totali</div>
          </div>
          <div className="card-body">
            {pendingRda.length === 0 && (
              <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Nessuna RdA in attesa.</div>
            )}
            {pendingRda.length > 0 && (
              <div className="col" style={{ gap: 6 }}>
                {pendingRda.slice(0, 6).map((r) => (
                  <div key={r.id} className="row" style={{ gap: 8, alignItems: 'center', padding: '6px 0', borderBottom: '1px dashed var(--line)', fontSize: 12 }}>
                    <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{r.id}</span>
                    <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.subject || r.title || '—'}</span>
                    <Chip kind="info" dot>{r.status || 'draft'}</Chip>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>

        {/* OdA pendenti */}
        <div className="card">
          <div className="card-header">
            <div className="title"><Icon name="oda" size={12}/> OdA pendenti</div>
            <div className="desc">{pendingOda.length} di {projectOda.length} totali</div>
          </div>
          <div className="card-body">
            {pendingOda.length === 0 && (
              <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Nessun OdA in attesa.</div>
            )}
            {pendingOda.length > 0 && (
              <div className="col" style={{ gap: 6 }}>
                {pendingOda.slice(0, 6).map((o) => (
                  <div key={o.id} className="row" style={{ gap: 8, alignItems: 'center', padding: '6px 0', borderBottom: '1px dashed var(--line)', fontSize: 12 }}>
                    <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{o.id}</span>
                    <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{o.subject || o.title || o.vendor || '—'}</span>
                    <Chip kind="info" dot>{o.status || 'draft'}</Chip>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// FASE 19 — Tab RdA collegate (estratto da OverviewTab)
// ============================================================
// FASE 10.5 — Tab "OdA collegate": lista leaner degli OdA del progetto. Riusa
// window.OdaDetailModal + window.OdaStatusChip (definiti in Oda.jsx, caricato dopo
// ProjectDetail.jsx ma risolti a render-time). Il workflow OdA compare già nel tab
// Workflow via il resolver (FASE 10.1); qui è la vista anagrafica degli ordini.
function OdaTab({ projectOda = [], project }) {
  const { user, seedCustom } = useStore();
  const [sel, setSel] = React.useState(null);
  const [showNew, setShowNew] = React.useState(false);
  const OdaModal = (typeof window !== 'undefined' && window.OdaDetailModal) || null;
  const NewModal = (typeof window !== 'undefined' && window.NewOdaModal) || null;
  const Chip = (typeof window !== 'undefined' && window.OdaStatusChip) || (({ status }) => <span>{status}</span>);
  const canCreateOda = typeof window !== 'undefined' && window.can ? window.can('po.create', user, seedCustom) : true;

  const newBtn = (
    <Btn
      variant={canCreateOda ? 'primary' : 'ghost'}
      size="sm"
      disabled={!canCreateOda}
      title={canCreateOda ? 'Crea un nuovo OdA su questo progetto' : window.whyDisabled('po.create')}
      onClick={() => { if (canCreateOda) setShowNew(true); }}
      data-testid="project-oda-new-btn"
    ><Icon name="plus" size={11} /> Nuovo OdA</Btn>
  );
  const newModal = showNew && NewModal && (
    <NewModal
      projectId={project?.id}
      onClose={() => setShowNew(false)}
      onCreated={(created) => { setShowNew(false); setSel(created); }}
    />
  );

  if (projectOda.length === 0) {
    return (
      <>
        <div className="card" style={{ padding: 32, textAlign: 'center', color: 'var(--text-3)' }}>
          <Icon name="rda" size={28} />
          <div style={{ marginTop: 8 }}>Nessun OdA collegato a questo progetto.</div>
          <div style={{ fontSize: 12, marginTop: 4 }}>Crea un OdA diretto, oppure generane uno da una RdA approvata (RdA → dettaglio → "Genera OdA").</div>
          <div style={{ marginTop: 14 }}>{newBtn}</div>
        </div>
        {newModal}
        {sel && OdaModal && <OdaModal oda={sel} onClose={() => setSel(null)} />}
      </>
    );
  }
  const total = projectOda.reduce((a, b) => a + (b.amount || 0), 0);
  const nClosed = projectOda.filter((o) => ['confirmed', 'delivered', 'closed'].includes(o.status)).length;
  return (
    <div className="col" style={{ gap: 12 }}>
      <div className="grid grid-3">
        <div className="card"><Stat label="OdA collegate" value={String(projectOda.length)} /></div>
        <div className="card"><Stat label="Valore totale" value={fmtEUR(total, true)} /></div>
        <div className="card"><Stat label="Confermate / chiuse" value={String(nClosed)} /></div>
      </div>
      <div className="card">
        <div className="card-header">
          <div className="title">OdA collegate ({projectOda.length})</div>
          <div className="actions">{newBtn}</div>
        </div>
        <table className="tbl" data-testid="project-oda-table">
          <thead><tr><th>OdA</th><th>Vendor</th><th className="num">Importo</th><th>Stato</th><th>RdA origine</th></tr></thead>
          <tbody>
            {projectOda.map((o) => (
              <tr key={o.id} className="clickable" onClick={() => setSel(o)} data-oda-id={o.id}>
                <td className="mono" style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-1)' }}>{o.id}</td>
                <td>{o.vendor || '—'}</td>
                <td className="num">{fmtEUR(o.amount, true)}</td>
                <td><Chip status={o.status} /></td>
                <td className="mono" style={{ fontSize: 11 }}>{o.rdaId || '—'}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      {newModal}
      {sel && OdaModal && <OdaModal oda={sel} onClose={() => setSel(null)} />}
    </div>
  );
}

function RdaTab({ projectRda = [], project }) {
  const { navigate } = useStore();
  const [filterStatus, setFilterStatus] = React.useState('*');
  // Sessione 88 — Modal compatto inline RDA detail: l'utente resta dentro
  // ProjectDetail invece di essere portato via su /rda. Bottone "Apri pagina RdA"
  // resta disponibile per chi vuole la vista completa con editing.
  const [selRda, setSelRda] = React.useState(null);

  const filtered = React.useMemo(
    () => projectRda.filter((r) => filterStatus === '*' || r.status === filterStatus),
    [projectRda, filterStatus],
  );
  const stats = React.useMemo(() => {
    const byStatus = {};
    let total = 0;
    let approved = 0;
    let pending = 0;
    for (const r of projectRda) {
      byStatus[r.status] = (byStatus[r.status] || 0) + 1;
      total += Number(r.amount || 0);
      if (r.status === 'approved') approved += Number(r.amount || 0);
      if (r.status === 'draft' || r.status === 'in_review') pending += Number(r.amount || 0);
    }
    return { byStatus, total, approved, pending };
  }, [projectRda]);

  if (projectRda.length === 0) {
    return (
      <div className="card">
        <div className="card-header">
          <div className="title">RdA del progetto</div>
          <div className="desc">Tutte le richieste d'acquisto collegate a {project.code}</div>
          <div className="actions">
            <Btn variant="primary" size="sm" onClick={() => navigate('rda')}>
              <Icon name="plus" size={11}/> Nuova RdA
            </Btn>
          </div>
        </div>
        <div className="card-body">
          <EmptyState
            icon={<Icon name="rda" size={20}/>}
            title="Nessuna RdA emessa"
            desc="Apri la pagina RdA per crearne una collegata a questo progetto."
          />
        </div>
      </div>
    );
  }

  const statusKinds = { draft: '', in_review: 'warn', approved: 'ok', rejected: 'err', cancelled: '' };

  return (
    <>
      <div className="grid grid-4" style={{ marginBottom: 14 }}>
        <div className="card"><Stat label="RdA totali" value={String(projectRda.length)} delta={`${stats.byStatus.approved || 0} approvate · ${stats.byStatus.in_review || 0} in revisione`} /></div>
        <div className="card"><Stat label="Importo totale" value={fmtEUR(stats.total, true)} delta="somma di tutte le RdA del progetto" /></div>
        <div className="card"><Stat label="Approvato" value={fmtEUR(stats.approved, true)} delta={stats.total > 0 ? Math.round(stats.approved/stats.total*100) + '% del totale' : '—'} tone="up"/></div>
        <div className="card"><Stat label="In attesa" value={fmtEUR(stats.pending, true)} delta={stats.total > 0 ? Math.round(stats.pending/stats.total*100) + '% del totale' : '—'} /></div>
      </div>

      <div className="card">
        <div className="card-header">
          <div className="title">RdA collegate ({filtered.length})</div>
          <div className="actions">
            <div className="row" style={{ gap: 2, background: 'var(--bg-2)', padding: 2, borderRadius: 6 }}>
              {['*', 'draft', 'in_review', 'approved', 'rejected', 'cancelled'].map((s) => (
                <button
                  key={s}
                  className={`btn sm ${filterStatus === s ? 'primary' : 'ghost'}`}
                  onClick={() => setFilterStatus(s)}
                  data-rda-filter={s}
                >
                  {s === '*' ? `Tutte (${projectRda.length})` : `${s} ${stats.byStatus[s] ? `(${stats.byStatus[s]})` : '0'}`}
                </button>
              ))}
            </div>
            <Btn variant="ghost" size="sm" onClick={() => navigate('rda')}>
              <Icon name="link" size={11}/> Pagina RdA
            </Btn>
          </div>
        </div>
        {filtered.length === 0 ? (
          <div className="card-body" style={{ color: 'var(--text-3)', fontSize: 12 }}>
            Nessuna RdA con filtro <code>{filterStatus}</code>. Cambia il filtro o azzeralo per vedere tutte le RdA del progetto.
          </div>
        ) : (
          <table className="tbl dense">
            <thead>
              <tr>
                <th>ID</th>
                <th>Titolo</th>
                <th>Vendor</th>
                <th style={{ textAlign: 'right' }}>Importo</th>
                <th>Urgenza</th>
                <th>Stato</th>
                <th>Creata</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map((r) => (
                <tr key={r.id} className="clickable" onClick={() => setSelRda(r)} data-rda-row={r.id}>
                  <td className="mono" style={{ fontSize: 11 }}>{r.id}</td>
                  <td style={{ fontSize: 11.5 }}>{r.title}</td>
                  <td>{r.vendor || '—'}</td>
                  <td className="num" style={{ textAlign: 'right' }}>{fmtEUR(r.amount || 0, true)}</td>
                  <td style={{ fontSize: 11 }}>{r.urgency || '—'}</td>
                  <td><Chip kind={statusKinds[r.status] || 'info'} dot>{r.status}</Chip></td>
                  <td className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{r.created || '—'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>

      {/* Sessione 88 — Riuso del modal RdA completo (definito in RdA.jsx).
          L'utente resta dentro ProjectDetail: tutte le azioni (status,
          checklist eval server-side, AI bozza, allegati upload, workflow,
          SLA, waiver) sono disponibili inline senza cambiare pagina. */}
      {selRda && typeof window !== 'undefined' && window.RdaDetailModal && (
        <window.RdaDetailModal rda={selRda} onClose={() => setSelRda(null)} />
      )}
    </>
  );
}

// ============================================================
// FASE 19 — Tab Workflow state istanze (live da DB)
// ============================================================
// FASE 16 (sessione 92) — Tab Workflow del progetto: avvia + gestisci.
// Riusa il componente generico `WorkflowInstancesCard` (definito in RdA.jsx,
// esposto su window) che fornisce avvio istanza, lista, e modale dettaglio
// con stepper + approva/rifiuta/skip. Il read-only di prima è stato sostituito
// da una vista realmente gestibile.
// FASE 3 Project Cockpit (s105) — vista unificata workflow del progetto.
// Tab Workflow trasformato da semplice wrapper a vista aggregata: mostra
// 2 sezioni distintive (governance + operativi) via
// /api/projects/[id]/workflows-aggregated, ognuna con card espandibile.
function WorkflowInstancesTab({ project, setTab }) {
  return <ProjectWorkflowsAggregatedView project={project} setTab={setTab} />;
}

// ============================================================
// FASE 19 — ProjectAnalyzerCard (Box "Raccomandazione AI" della Panoramica)
// s132 — Ora esegue PROJECT_ANALYZER **inline** invece di navigare al tab
// Insight AI. Usa `ProjectAnalyzerInline compact` (severity + score + summary
// + top 3 risks). La tab Insight AI mostra la versione full + dati operativi
// (team, RdA pendenti, OdA pendenti, anomalie aperte).
// ============================================================
function ProjectAnalyzerCard({ project }) {
  return <ProjectAnalyzerInline project={project} compact={true} />;
}

// ============================================================
// FASE 2 Project Cockpit (sessione 105) — ChecklistTab
// ------------------------------------------------------------
// Tab dedicato che mostra lo stato compliance documentale del progetto in
// 5 sezioni accordion:
//   🔴 Mancanti          — con requiredBy chip per fonte + signers atteso
//   🟡 Presenti da firmare — bottone "Firma ora" riusa SignatureRequestModal
//   ⚠️  Firme respinte     — bottone "Re-upload" (deep-link tab Documenti)
//   ✅ Presenti firmati   — firmatario + data + chip ruolo
//   📋 Waiver              — active vs scaduti con badge expiry
//
// Mental model: "tab Documenti = cosa c'è caricato" vs "tab Checklist =
// cosa serve, chi è atteso, perché". Fetch live da
// `/api/projects/[id]/checklist-evaluation`.
// ============================================================
function ChecklistTab({ p, setTab }) {
  const { user, pushToast, seedCustom } = useStore();
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [signDoc, setSignDoc] = React.useState(null);
  // FIX audit Fase 2 (s105): bottone "Solleva eccezione" in sezione Mancanti
  // (piano linea 689, gated `doc.waive`). Stesso pattern del waiverForm in
  // OverviewTab (linea 242+) replicato qui per coerenza UX.
  const [waiverForm, setWaiverForm] = React.useState(null);
  const [waiverSaving, setWaiverSaving] = React.useState(false);
  const canWaive = typeof window !== 'undefined' && window.can
    ? window.can('doc.waive', user, seedCustom)
    : true;
  const [openSections, setOpenSections] = React.useState({
    missing: true,
    presentToSign: true,
    rejectedSignatures: true,
    presentSigned: false,
    waivers: false,
  });

  const reload = React.useCallback(async () => {
    if (!p?.id) return;
    setLoading(true);
    setError(null);
    try {
      const r = await fetch(`/api/projects/${p.id}/checklist-evaluation`, {
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        setError(j?.detail || j?.error || `HTTP ${r.status}`);
        setData(null);
      } else {
        setData(j?.data ?? null);
      }
    } catch (err) {
      setError(err?.message || 'rete');
      setData(null);
    } finally {
      setLoading(false);
    }
  }, [p?.id, user?.id]);

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

  const toggle = (k) => setOpenSections((s) => ({ ...s, [k]: !s[k] }));

  if (loading && !data) {
    return <div style={{ fontSize: 12, color: 'var(--text-3)', padding: 14 }}>Caricamento checklist…</div>;
  }
  if (error) {
    return (
      <div className="card" style={{ padding: 14 }}>
        <div style={{ color: 'var(--err)', fontSize: 12 }}>Errore caricamento checklist: {error}</div>
        <Btn variant="ghost" size="sm" onClick={reload} style={{ marginTop: 8 }}>Riprova</Btn>
      </div>
    );
  }
  if (!data) return null;

  const cnt = {
    missing: data.missing.length,
    missingBlocking: data.missing.filter((m) => m.blocking).length,
    presentToSign: data.presentToSign.length,
    rejectedSignatures: data.rejectedSignatures.length,
    presentSigned: data.presentSigned.length,
    waivers: data.waivers.length,
    waiversActive: data.waivers.filter((w) => w.isActive).length,
  };

  return (
    <>
      {/* Header summary */}
      <div className="card" style={{ marginBottom: 14 }}>
        <div className="card-body row" style={{ gap: 14, alignItems: 'center', flexWrap: 'wrap' }}>
          <div>
            <div className="eyebrow">Compliance score</div>
            <div style={{ fontSize: 22, fontWeight: 600, fontFamily: 'var(--font-display)' }}>
              {(data.scoreBp / 100).toFixed(1)}%
            </div>
          </div>
          <div style={{ flex: 1 }}>
            {data.blocking ? (
              <Chip kind="err" dot>🔴 Bloccante — {cnt.missingBlocking} doc mancante/i blocca un workflow attivo</Chip>
            ) : (
              <Chip kind="ok" dot>✅ Nessun blocco attivo sui workflow</Chip>
            )}
          </div>
          <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
            Eval: {new Date(data.evaluatedAt).toLocaleString('it-IT')}
            {data.latencyMs != null && ` · ${data.latencyMs}ms`}
          </div>
          <Btn variant="ghost" size="sm" onClick={reload} disabled={loading}>
            <Icon name="history" size={11}/> {loading ? 'Aggiorno…' : 'Aggiorna'}
          </Btn>
        </div>
      </div>

      {/* ── 1. Mancanti ─────────────────────────────────────────────── */}
      <ChecklistSection
        title={`🔴 Mancanti (${cnt.missing}${cnt.missingBlocking > 0 ? ` · ${cnt.missingBlocking} blocking` : ''})`}
        open={openSections.missing}
        onToggle={() => toggle('missing')}
        empty={data.missing.length === 0 && 'Nessun documento mancante 🎉'}
      >
        {data.missing.map((m) => (
          <div key={m.docCode} style={{ padding: 12, borderTop: '1px solid var(--line)' }}>
            <div className="row" style={{ gap: 10, alignItems: 'center', marginBottom: 6 }}>
              <div style={{ flex: 1 }}>
                <span className="mono" style={{ fontSize: 11.5, fontWeight: 600 }}>{m.docCode}</span>
                <span style={{ fontSize: 12.5, marginLeft: 8 }}>{m.docTypeName}</span>
              </div>
              {m.blocking && <Chip kind="err" dot>blocca workflow</Chip>}
            </div>
            <div style={{ fontSize: 11, color: 'var(--text-2)', marginBottom: 6 }}>
              <strong>Richiesto da:</strong>{' '}
              {m.requiredBy.length === 0 ? <span style={{ color: 'var(--text-3)' }}>—</span> : null}
              <span className="row" style={{ display: 'inline-flex', gap: 6, flexWrap: 'wrap', verticalAlign: 'middle' }}>
                {m.requiredBy.map((rb, i) => rb.source === 'checklist_rule' ? (
                  <Chip key={`r${i}`} kind="info" title={`Rule ${rb.ruleId}`}>📋 {rb.ruleName}</Chip>
                ) : (
                  <Chip key={`g${i}`} kind={rb.isStepActive ? 'warn' : ''} dot={rb.isStepActive} title={`Step ${rb.stepOrder} di ${rb.workflowCode}`}>
                    ⚙ {rb.workflowName} · {rb.stepName}{rb.isStepActive ? ' (active)' : ''}
                  </Chip>
                ))}
              </span>
            </div>
            {m.expectedSignerPersonas.length > 0 && (
              <div style={{ fontSize: 11, color: 'var(--text-2)' }}>
                <strong>Firmatari attesi:</strong>{' '}
                <span className="row" style={{ display: 'inline-flex', gap: 6, flexWrap: 'wrap', verticalAlign: 'middle' }}>
                  {m.expectedSignerPersonas.map((s) => (
                    <Chip key={s.personaId} kind={s.source === 'team' ? 'ok' : ''} title={s.source === 'team' ? 'Team progetto' : 'Persona globale del tenant'}>
                      {s.source === 'team' ? '👥' : '🌐'} {s.name}
                    </Chip>
                  ))}
                  {m.totalMatchingMembers > m.expectedSignerPersonas.length && (
                    <span style={{ fontSize: 10.5, color: 'var(--text-3)' }}>+{m.totalMatchingMembers - m.expectedSignerPersonas.length} altri</span>
                  )}
                </span>
              </div>
            )}
            {m.expectedSignerRoles.length > 0 && m.expectedSignerPersonas.length === 0 && (
              <div style={{ fontSize: 11, color: 'var(--text-3)' }}>
                Ruolo firmatario: {m.expectedSignerRoles.join(', ')} — nessuna persona match nel team/tenant
              </div>
            )}
            <div style={{ marginTop: 8 }}>
              <Btn
                variant="ghost"
                size="xs"
                onClick={() => canWaive && setWaiverForm({ docCode: m.docCode, reason: 'not_applicable', justification: '', expiresAt: '' })}
                disabled={!canWaive}
                title={canWaive ? 'Solleva eccezione (waiver) per questo documento' : 'Permesso doc.waive richiesto'}
                data-testid={`checklist-waiver-btn-${m.docCode}`}
              >
                <Icon name="alert-triangle" size={11}/> Solleva eccezione
              </Btn>
            </div>
          </div>
        ))}
      </ChecklistSection>

      {/* ── 2. Presenti da firmare ──────────────────────────────────── */}
      <ChecklistSection
        title={`🟡 Presenti da firmare (${cnt.presentToSign})`}
        open={openSections.presentToSign}
        onToggle={() => toggle('presentToSign')}
        empty={cnt.presentToSign === 0 && 'Nessun documento in attesa di firma'}
      >
        {data.presentToSign.map((d) => (
          <div key={d.docId} className="row" style={{ gap: 10, padding: 12, borderTop: '1px solid var(--line)', alignItems: 'center' }}>
            <div style={{ flex: 1 }}>
              <div className="row" style={{ gap: 8, alignItems: 'center' }}>
                <span className="mono" style={{ fontSize: 11, fontWeight: 600 }}>{d.docCode}</span>
                <span style={{ fontSize: 12.5 }}>{d.title}</span>
                {d.signatureStatus === 'pending' && <Chip kind="warn" dot>pending</Chip>}
                {d.signatureStatus === 'unsigned' && <Chip kind="info">da firmare</Chip>}
              </div>
              <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 2 }}>
                {d.uploadedBy ? `Caricato da ${d.uploadedBy}` : 'Caricato'}
                {d.uploadedAt && ` · ${new Date(d.uploadedAt).toLocaleDateString('it-IT')}`}
                {d.fileVersion != null && ` · v${d.fileVersion}`}
              </div>
              {d.expectedSignerPersonas.length > 0 && (
                <div style={{ fontSize: 10.5, color: 'var(--text-2)', marginTop: 4 }}>
                  Firmatari attesi:{' '}
                  {d.expectedSignerPersonas.map((s) => s.name).join(', ')}
                  {d.totalMatchingMembers > d.expectedSignerPersonas.length && ` +${d.totalMatchingMembers - d.expectedSignerPersonas.length}`}
                </div>
              )}
            </div>
            <Btn
              variant="primary"
              size="sm"
              onClick={() => setSignDoc({ id: d.docId, title: d.title, type: d.docCode, fileVersion: d.fileVersion })}
            >
              <Icon name="sparkle" size={11}/> Firma ora
            </Btn>
          </div>
        ))}
      </ChecklistSection>

      {/* ── 3. Firme respinte ───────────────────────────────────────── */}
      {cnt.rejectedSignatures > 0 && (
        <ChecklistSection
          title={`⚠️  Firme respinte (${cnt.rejectedSignatures})`}
          open={openSections.rejectedSignatures}
          onToggle={() => toggle('rejectedSignatures')}
          highlight
        >
          {data.rejectedSignatures.map((d) => (
            <div key={d.docId} className="row" style={{ gap: 10, padding: 12, borderTop: '1px solid var(--line)', alignItems: 'center' }}>
              <div style={{ flex: 1 }}>
                <div className="row" style={{ gap: 8, alignItems: 'center' }}>
                  <span className="mono" style={{ fontSize: 11, fontWeight: 600 }}>{d.docCode}</span>
                  <span style={{ fontSize: 12.5 }}>{d.title}</span>
                  <Chip kind="err" dot>respinta</Chip>
                </div>
                <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
                  {d.rejectedAt && `Respinta il ${new Date(d.rejectedAt).toLocaleDateString('it-IT')}`}
                </div>
              </div>
              <Btn variant="ghost" size="sm" onClick={() => setTab('docs')}>
                <Icon name="docs" size={11}/> Re-upload
              </Btn>
            </div>
          ))}
        </ChecklistSection>
      )}

      {/* ── 4. Presenti firmati ─────────────────────────────────────── */}
      <ChecklistSection
        title={`✅ Presenti e firmati (${cnt.presentSigned})`}
        open={openSections.presentSigned}
        onToggle={() => toggle('presentSigned')}
        empty={cnt.presentSigned === 0 && 'Nessun documento firmato ancora'}
      >
        {data.presentSigned.map((d) => (
          <div key={d.docId} className="row" style={{ gap: 10, padding: 12, borderTop: '1px solid var(--line)', alignItems: 'center' }}>
            <div style={{ flex: 1 }}>
              <div className="row" style={{ gap: 8, alignItems: 'center' }}>
                <span className="mono" style={{ fontSize: 11, fontWeight: 600 }}>{d.docCode}</span>
                <span style={{ fontSize: 12.5 }}>{d.title}</span>
                <Chip kind="ok" dot>firmato</Chip>
              </div>
              <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
                {d.signedByPersonaName ? `Firmato da ${d.signedByPersonaName}` : d.signedBy ? `Firmato da ${d.signedBy}` : 'Firmato'}
                {d.signedAt && ` · ${new Date(d.signedAt).toLocaleDateString('it-IT')}`}
              </div>
            </div>
          </div>
        ))}
      </ChecklistSection>

      {/* ── 5. Waiver ───────────────────────────────────────────────── */}
      {cnt.waivers > 0 && (
        <ChecklistSection
          title={`📋 Waiver (${cnt.waiversActive} attivi${cnt.waivers > cnt.waiversActive ? ` · ${cnt.waivers - cnt.waiversActive} scaduti` : ''})`}
          open={openSections.waivers}
          onToggle={() => toggle('waivers')}
        >
          {data.waivers.map((w) => {
            const daysToExpire = w.expiresAt ? Math.floor((new Date(w.expiresAt).getTime() - Date.now()) / 86400000) : null;
            return (
              <div key={w.exceptionId} style={{ padding: 12, borderTop: '1px solid var(--line)', opacity: w.isActive ? 1 : 0.6 }}>
                <div className="row" style={{ gap: 8, alignItems: 'center', marginBottom: 4 }}>
                  <span className="mono" style={{ fontSize: 11, fontWeight: 600 }}>{w.docCode}</span>
                  <span style={{ fontSize: 12.5 }}>{w.docTypeName}</span>
                  {w.isActive ? <Chip kind="info" dot>attivo</Chip> : <Chip kind="warn">⏰ scaduto</Chip>}
                  {w.isActive && daysToExpire != null && daysToExpire < 30 && (
                    <Chip kind="warn">scade tra {daysToExpire}gg</Chip>
                  )}
                </div>
                <div style={{ fontSize: 11, color: 'var(--text-2)', marginBottom: 4 }}>
                  <strong>Motivo:</strong> {w.reason} — {w.justification}
                </div>
                <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
                  Approvato {w.approvedBy ? `da ${w.approvedBy} ` : ''}il {new Date(w.approvedAt).toLocaleDateString('it-IT')}
                  {w.expiresAt && ` · scade ${new Date(w.expiresAt).toLocaleDateString('it-IT')}`}
                </div>
              </div>
            );
          })}
        </ChecklistSection>
      )}

      {signDoc && (
        <SignatureRequestModal
          doc={signDoc}
          onClose={() => setSignDoc(null)}
          onComplete={() => {
            setSignDoc(null);
            pushToast({ title: 'Firma completata', desc: 'Aggiorno checklist…', tone: 'ok' });
            reload();
          }}
        />
      )}

      {waiverForm && (
        <Modal
          open={!!waiverForm}
          onClose={() => setWaiverForm(null)}
          title={`Solleva eccezione — ${waiverForm.docCode}`}
          size="md"
          footer={
            <>
              <Btn variant="ghost" size="sm" onClick={() => setWaiverForm(null)}>Annulla</Btn>
              <Btn
                variant="primary"
                size="sm"
                disabled={waiverSaving || !waiverForm.justification || waiverForm.justification.length < 10}
                onClick={async () => {
                  if (!p?.id) 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: 'project', entityId: p.id, docCode: waiverForm.docCode,
                        reason: waiverForm.reason, justification: waiverForm.justification,
                        expiresAt: waiverForm.expiresAt || null,
                      }),
                    });
                    const j = await r.json().catch(() => ({}));
                    if (!r.ok) {
                      pushToast({ title: 'Waiver fallito', desc: j?.detail || j?.error || `HTTP ${r.status}`, tone: 'err' });
                      return;
                    }
                    pushToast({ title: 'Eccezione sollevata', desc: `Waiver per ${waiverForm.docCode} creato. Aggiorno checklist…`, tone: 'ok' });
                    setWaiverForm(null);
                    reload();
                  } catch (err) {
                    pushToast({ title: 'Errore di rete', desc: err?.message || 'fail', tone: 'err' });
                  } finally {
                    setWaiverSaving(false);
                  }
                }}
                data-testid="checklist-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)' }}>
              Sollevo eccezione documentale su <code className="mono">{waiverForm.docCode}</code> per il progetto <code className="mono">{p.id}</code>. Il documento verrà escluso dalla checklist obbligatoria finché il waiver è attivo.
            </div>
            <div className="field">
              <label>Motivo *</label>
              <select
                value={waiverForm.reason}
                onChange={(e) => setWaiverForm({ ...waiverForm, reason: e.target.value })}
                data-testid="checklist-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 * (min 10 caratteri)</label>
              <textarea
                rows={3}
                value={waiverForm.justification}
                onChange={(e) => setWaiverForm({ ...waiverForm, justification: e.target.value })}
                placeholder="Es. Doc non applicabile per importo &lt; soglia X, vedi mail HSE 2026-04-12…"
                data-testid="checklist-waiver-justification"
              />
            </div>
            <div className="field">
              <label>Scadenza waiver (opzionale)</label>
              <input
                type="datetime-local"
                value={waiverForm.expiresAt ? new Date(waiverForm.expiresAt).toISOString().slice(0, 16) : ''}
                onChange={(e) => setWaiverForm({ ...waiverForm, expiresAt: e.target.value ? new Date(e.target.value).toISOString() : '' })}
              />
              <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 4 }}>
                Senza scadenza il waiver resta attivo finché non viene revocato manualmente.
              </div>
            </div>
          </div>
        </Modal>
      )}
    </>
  );
}

function ChecklistSection({ title, open, onToggle, empty, highlight, children }) {
  return (
    <div className="card" style={{ marginBottom: 10, border: highlight ? '1px solid var(--err)' : undefined }}>
      <button
        type="button"
        onClick={onToggle}
        style={{
          width: '100%', padding: '10px 14px', border: 0, background: 'transparent',
          textAlign: 'left', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8,
          fontSize: 13, fontWeight: 600, color: 'var(--text-1)',
        }}
      >
        <Icon name={open ? 'chev_d' : 'chev_r'} size={11}/>
        <span style={{ flex: 1 }}>{title}</span>
      </button>
      {open && (
        <div>
          {empty ? (
            <div style={{ padding: '6px 14px 14px', fontSize: 11.5, color: 'var(--text-3)' }}>{empty}</div>
          ) : (
            children
          )}
        </div>
      )}
    </div>
  );
}

// ============================================================
// FASE 3 Project Cockpit (s105) — ProjectWorkflowsAggregatedView
// ------------------------------------------------------------
// Vista unificata che mostra TUTTI i workflow_instance in_progress che
// insistono sul progetto, divisi in 2 sezioni:
//   🟦 Governance      — entity_type='project' (es. WF_PROJ_CAPEX)
//   🟨 Operativi       — rda/sal/altro (workflow operativi day-to-day)
//
// Card per ogni workflow_instance, espandibile per vedere gli step con:
//   - step status badge (active/pending/completed/skipped)
//   - approver resolved alla persona del team (kind='role') o chip matrix
//   - gates con stato firma (✅/🔴/🟡)
//   - SLA badge in_time/warn/late
//
// Click su una card → apre WorkflowInstanceDetailModal (esposto da RdA.jsx
// su window, sessione 102) per drill-down + azioni.
// ============================================================
function ProjectWorkflowsAggregatedView({ project, setTab }) {
  const { user, pushToast } = useStore();
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [openInstanceId, setOpenInstanceId] = React.useState(null);
  const [openStepStateId, setOpenStepStateId] = React.useState(null);
  const [expandedInstances, setExpandedInstances] = React.useState({});
  // FASE 3b Cockpit (s105) — toggle Lista / Organigramma persistito
  // per progetto in localStorage per UX continuità.
  const viewKey = `lgs.workflow.view.${project?.id || ''}`;
  const [view, setView] = React.useState(() => {
    if (typeof window === 'undefined') return 'list';
    return localStorage.getItem(viewKey) || 'list';
  });
  React.useEffect(() => {
    if (typeof window !== 'undefined' && project?.id) {
      localStorage.setItem(viewKey, view);
    }
  }, [view, viewKey, project?.id]);

  const reload = React.useCallback(async () => {
    if (!project?.id) return;
    setLoading(true);
    setError(null);
    try {
      const r = await fetch(`/api/projects/${project.id}/workflows-aggregated`, {
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        setError(j?.detail || j?.error || `HTTP ${r.status}`);
        setData(null);
      } else {
        setData(j?.data ?? null);
      }
    } catch (err) {
      setError(err?.message || 'rete');
      setData(null);
    } finally {
      setLoading(false);
    }
  }, [project?.id, user?.id]);

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

  const toggleInstance = (id) => setExpandedInstances((s) => ({ ...s, [id]: !s[id] }));

  if (loading && !data) {
    return <div style={{ fontSize: 12, color: 'var(--text-3)', padding: 14 }}>Caricamento workflow…</div>;
  }
  if (error) {
    return (
      <div className="card" style={{ padding: 14 }}>
        <div style={{ color: 'var(--err)', fontSize: 12 }}>Errore caricamento: {error}</div>
        <Btn variant="ghost" size="sm" onClick={reload} style={{ marginTop: 8 }}>Riprova</Btn>
      </div>
    );
  }
  if (!data) return null;

  const noWorkflows = data.totalInstances === 0;

  return (
    <>
      {/* Header summary */}
      <div className="card" style={{ marginBottom: 14 }}>
        <div className="card-body row" style={{ gap: 14, alignItems: 'center', flexWrap: 'wrap' }}>
          <div>
            <div className="eyebrow">Workflow attivi</div>
            <div style={{ fontSize: 22, fontWeight: 600, fontFamily: 'var(--font-display)' }}>
              {data.totalInstances}
            </div>
          </div>
          <div style={{ flex: 1 }}>
            {data.totalBlockedSteps > 0 ? (
              <Chip kind="err" dot>🔴 {data.totalBlockedSteps} step bloccati da gate documentale</Chip>
            ) : data.totalInstances > 0 ? (
              <Chip kind="ok" dot>✅ Nessun gate documentale aperto</Chip>
            ) : (
              <Chip kind="info">Nessun workflow avviato</Chip>
            )}
          </div>
          <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
            Eval: {new Date(data.evaluatedAt).toLocaleString('it-IT')}
            {data.latencyMs != null && ` · ${data.latencyMs}ms`}
          </div>
          <Btn variant="ghost" size="sm" onClick={reload} disabled={loading}>
            <Icon name="history" size={11}/> {loading ? 'Aggiorno…' : 'Aggiorna'}
          </Btn>
        </div>
        {/* FASE 3b Cockpit (s105) — toggle Lista / Organigramma */}
        {!noWorkflows && (
          <div className="card-body" style={{ paddingTop: 0, borderTop: '1px solid var(--line)' }}>
            <div className="row" style={{ gap: 6, alignItems: 'center' }}>
              <span style={{ fontSize: 11, color: 'var(--text-3)' }}>Visualizzazione:</span>
              <Btn
                variant={view === 'list' ? 'primary' : 'ghost'}
                size="sm"
                onClick={() => setView('list')}
                data-testid="wf-view-list"
              >
                <Icon name="menu" size={11}/> Lista
              </Btn>
              <Btn
                variant={view === 'orgchart' ? 'primary' : 'ghost'}
                size="sm"
                onClick={() => setView('orgchart')}
                data-testid="wf-view-orgchart"
              >
                <Icon name="flow" size={11}/> Organigramma
              </Btn>
              <span style={{ flex: 1 }}/>
              {view === 'orgchart' && (
                <span style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
                  Process-map orizzontale · scroll laterale · click card → dettaglio step
                </span>
              )}
            </div>
          </div>
        )}
      </div>

      {noWorkflows && (
        <div className="card" style={{ padding: 24, textAlign: 'center' }}>
          <div style={{ fontSize: 13, color: 'var(--text-2)', marginBottom: 8 }}>
            Nessun workflow di approvazione attivo per questo progetto.
          </div>
          <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>
            Il workflow CAPEX viene avviato automaticamente alla creazione del progetto se la categoria/classe lo richiede.
          </div>
        </div>
      )}

      {view === 'orgchart' && !noWorkflows && (
        <WorkflowOrgChart
          data={data}
          onOpenStep={(instanceId, stepStateId) => {
            setOpenInstanceId(instanceId);
            setOpenStepStateId(stepStateId);
          }}
          onGateClick={() => setTab?.('checklist')}
        />
      )}

      {/* Sezione Governance — solo vista Lista */}
      {view === 'list' && data.governance.length > 0 && (
        <div style={{ marginBottom: 14 }}>
          <div style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--text-2)', marginBottom: 8 }}>
            🟦 Workflow di governance ({data.governance.length})
          </div>
          {data.governance.map((wf) => (
            <WorkflowInstanceCard
              key={wf.instanceId}
              wf={wf}
              expanded={!!expandedInstances[wf.instanceId]}
              onToggle={() => toggleInstance(wf.instanceId)}
              onOpenDetail={() => setOpenInstanceId(wf.instanceId)}
              onGateClick={() => setTab?.('checklist')}
            />
          ))}
        </div>
      )}

      {/* Sezione Operativi — solo vista Lista */}
      {view === 'list' && data.operational.length > 0 && (
        <div style={{ marginBottom: 14 }}>
          <div style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--text-2)', marginBottom: 8 }}>
            🟨 Workflow operativi ({data.operational.length})
          </div>
          {data.operational.map((wf) => (
            <WorkflowInstanceCard
              key={wf.instanceId}
              wf={wf}
              expanded={!!expandedInstances[wf.instanceId]}
              onToggle={() => toggleInstance(wf.instanceId)}
              onOpenDetail={() => setOpenInstanceId(wf.instanceId)}
              onGateClick={() => setTab?.('checklist')}
            />
          ))}
        </div>
      )}

      {openInstanceId && typeof window !== 'undefined' && window.WorkflowInstanceDetailModal && (
        React.createElement(window.WorkflowInstanceDetailModal, {
          instanceId: openInstanceId,
          focusStepId: openStepStateId,
          onClose: () => { setOpenInstanceId(null); setOpenStepStateId(null); },
          onTransition: () => { setOpenInstanceId(null); setOpenStepStateId(null); reload(); pushToast({ title: 'Workflow aggiornato', tone: 'ok' }); },
          user,
          pushToast,
        })
      )}
    </>
  );
}

function WorkflowInstanceCard({ wf, expanded, onToggle, onOpenDetail, onGateClick }) {
  const blockedStepsCount = wf.steps.filter((s) => s.status === 'active' && s.gates.result?.blocked).length;
  const currentStep = wf.steps.find((s) => s.status === 'active');
  return (
    <div className="card" style={{ marginBottom: 8, borderLeft: blockedStepsCount > 0 ? '3px solid var(--err)' : '3px solid var(--accent)' }}>
      <button
        type="button"
        onClick={onToggle}
        style={{
          width: '100%', padding: '10px 12px', border: 0, background: 'transparent',
          textAlign: 'left', cursor: 'pointer', display: 'flex', gap: 10, alignItems: 'center',
        }}
      >
        <Icon name={expanded ? 'chev_d' : 'chev_r'} size={11}/>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="row" style={{ gap: 8, alignItems: 'center' }}>
            <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{wf.workflowCode}</span>
            <span style={{ fontSize: 12.5, fontWeight: 600 }}>{wf.workflowName}</span>
            {wf.entityType !== 'project' && (
              <Chip kind="info">{wf.entityType} · {wf.entityId}</Chip>
            )}
            {blockedStepsCount > 0 && <Chip kind="err" dot>🔴 bloccato</Chip>}
          </div>
          {currentStep && (
            <div style={{ fontSize: 10.5, color: 'var(--text-2)', marginTop: 2 }}>
              Step corrente: <strong>{currentStep.stepName}</strong>
              {' · '}
              {currentStep.approver.kind === 'role' && (currentStep.approver.personas?.length > 0)
                ? (() => {
                    const ps = currentStep.approver.personas;
                    const teamCount = ps.filter((p) => p.source === 'team').length;
                    const icon = teamCount > 0 ? '👥' : '🌐';
                    const tail = currentStep.approver.totalMatchingMembers > ps.length
                      ? ` +${currentStep.approver.totalMatchingMembers - ps.length}` : '';
                    return `${icon} ${ps.map((p) => p.name).join(', ')}${tail}`;
                  })()
                : currentStep.approver.kind === 'role'
                ? `ruolo ${currentStep.approver.roleId} (nessun match nel tenant)`
                : currentStep.approver.kind === 'matrix'
                ? '🔀 Approver matrix (runtime)'
                : currentStep.approver.kind === 'persona'
                ? `👤 ${currentStep.approver.personaName || currentStep.approver.personaId}`
                : '—'}
            </div>
          )}
        </div>
        <span onClick={(e) => { e.stopPropagation(); onOpenDetail(); }} className="btn ghost sm" style={{ flexShrink: 0 }}>
          <Icon name="external" size={11}/> Dettaglio
        </span>
      </button>

      {expanded && (
        <div style={{ borderTop: '1px solid var(--line)' }}>
          {wf.steps.map((s, i) => (
            <WorkflowStepRow
              key={s.stepStateId}
              step={s}
              isLast={i === wf.steps.length - 1}
              onGateClick={onGateClick}
            />
          ))}
        </div>
      )}
    </div>
  );
}

function WorkflowStepRow({ step, isLast, onGateClick }) {
  const statusChip = (() => {
    if (step.isSkipped) return <Chip>⏭ saltato</Chip>;
    if (step.status === 'completed') return <Chip kind="ok" dot>✅ completato</Chip>;
    if (step.status === 'rejected') return <Chip kind="err" dot>❌ rifiutato</Chip>;
    if (step.status === 'active') return <Chip kind="warn" dot>🟡 attivo</Chip>;
    return <Chip>⏳ pending</Chip>;
  })();
  const slaChip = (() => {
    if (step.sla.badge === 'completed') return null;
    if (step.sla.badge === 'not_started') return null;
    if (step.sla.badge === 'in_time') return <Chip kind="ok">SLA OK</Chip>;
    if (step.sla.badge === 'warn') return <Chip kind="warn" dot>⚠ SLA {step.sla.progressPct}%</Chip>;
    if (step.sla.badge === 'late') return <Chip kind="err" dot>🔴 SLA scaduto</Chip>;
    return null;
  })();
  return (
    <div
      style={{
        padding: '8px 12px',
        borderBottom: isLast ? 'none' : '1px solid var(--line)',
        opacity: step.isSkipped ? 0.55 : 1,
      }}
    >
      <div className="row" style={{ gap: 8, alignItems: 'center', marginBottom: 4, flexWrap: 'wrap' }}>
        <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', minWidth: 24 }}>#{step.stepOrder + 1}</span>
        <span style={{ fontSize: 12, fontWeight: 500 }}>{step.stepName}</span>
        {statusChip}
        {slaChip}
      </div>
      <div style={{ fontSize: 10.5, color: 'var(--text-2)', marginLeft: 32, marginBottom: 4 }}>
        Approver:{' '}
        {step.approver.kind === 'role' && (step.approver.personas?.length > 0)
          ? <>
              {step.approver.personas.map((p, i) => (
                <span key={p.personaId} title={p.source === 'team' ? 'Team del progetto' : 'Persona globale del tenant'}>
                  {i > 0 && ', '}
                  {p.source === 'team' ? '👥' : '🌐'} {p.name}
                </span>
              ))}
              {step.approver.totalMatchingMembers > step.approver.personas.length
                && <span style={{ color: 'var(--text-3)' }}> +{step.approver.totalMatchingMembers - step.approver.personas.length}</span>}
            </>
          : step.approver.kind === 'role'
          ? <em>ruolo <code className="mono">{step.approver.roleId}</code> — nessun match nel tenant</em>
          : step.approver.kind === 'matrix'
          ? <em>🔀 matrix (risolto a runtime sull'importo)</em>
          : step.approver.kind === 'persona'
          ? <>👤 {step.approver.personaName || step.approver.personaId}</>
          : <em>—</em>}
      </div>
      {step.gates.docCodes.length > 0 && (
        <div style={{ fontSize: 10.5, color: 'var(--text-2)', marginLeft: 32 }}>
          Gate:{' '}
          <span className="row" style={{ display: 'inline-flex', gap: 6, flexWrap: 'wrap', verticalAlign: 'middle' }}>
            {step.gates.docCodes.map((code) => {
              const m = step.gates.result?.missing?.find((mm) => mm.docTypeCode === code);
              const tone = !m ? 'ok' : m.reason === 'unsigned' ? 'warn' : 'err';
              const label = !m ? '✅ ok' : m.reason === 'unsigned' ? '🟡 da firmare' : '🔴 assente';
              return (
                <Chip
                  key={code}
                  kind={tone}
                  dot={tone !== 'ok'}
                  onClick={onGateClick}
                  style={{ cursor: 'pointer' }}
                  title="Vai al tab Checklist"
                >
                  {code} {label}
                </Chip>
              );
            })}
          </span>
        </div>
      )}
    </div>
  );
}

// ============================================================
// FASE 3b Project Cockpit (s105) — WorkflowOrgChart
// ------------------------------------------------------------
// Process-map orizzontale scrollable: CSS Grid auto-flow column +
// scroll-snap mandatory + step card stazione con avatar/ruolo/gates/SLA.
// NO librerie chart (D3, react-flow, mermaid): pattern coerente con il
// design system esistente, render istantaneo, accessibilità nativa via
// <button> semantici.
//
// Riusa /api/projects/[id]/workflows-aggregated (Fase 3) — no nuova API.
// Click su una card stazione → apre WorkflowInstanceDetailModal con
// focusStepId per scroll allo step specifico.
//
// Layout:
//   WF_PROJ_CAPEX · CAPEX standard (7 step)
//   [S1✅] → [S2✅] → [S3🟡] → [S4⏳] → [S5⏳] → [S6⏭️] → [S7⏳]
//
//   Workflow operativi collegati (collassabile):
//     ▸ WF_RDA_STD · RDA-2026-0126 · step 3/6
// ============================================================
function WorkflowOrgChart({ data, onOpenStep, onGateClick }) {
  if (!data || data.totalInstances === 0) return null;

  return (
    <>
      <style>{`
        .wf-orgchart-track {
          display: grid;
          grid-auto-flow: column;
          grid-auto-columns: 220px;
          gap: 28px;
          overflow-x: auto;
          scroll-snap-type: x mandatory;
          padding: 14px 4px 18px;
          position: relative;
        }
        .wf-orgchart-step {
          scroll-snap-align: start;
          border: 1px solid var(--line);
          border-radius: 8px;
          padding: 12px;
          position: relative;
          background: var(--bg-1);
          cursor: pointer;
          text-align: left;
          display: flex; flex-direction: column; gap: 6px;
          min-height: 150px;
        }
        .wf-orgchart-step:hover { background: var(--bg-2); }
        .wf-orgchart-step::after {
          content: '→';
          position: absolute;
          right: -22px;
          top: 50%;
          transform: translateY(-50%);
          color: var(--text-3);
          font-size: 14px;
          pointer-events: none;
        }
        .wf-orgchart-step:last-child::after { content: ''; }
        .wf-orgchart-step.skipped { opacity: 0.4; background: var(--bg-2); }
        .wf-orgchart-step.blocked { border-color: var(--err); box-shadow: 0 0 6px rgba(220, 60, 60, 0.2); }
        .wf-orgchart-step.active  {
          border-color: var(--warn, #c98e36);
          box-shadow: 0 0 8px rgba(220, 180, 80, 0.28);
        }
        .wf-orgchart-step.completed { border-color: var(--ok, #2c8c4a); }
        .wf-orgchart-step-num {
          font-family: var(--font-display);
          font-size: 10.5px;
          color: var(--text-3);
        }
        .wf-orgchart-step-name {
          font-size: 12.5px;
          font-weight: 600;
          line-height: 1.3;
          color: var(--text-1);
          min-height: 32px;
        }
        .wf-orgchart-step-actor {
          font-size: 11px;
          color: var(--text-2);
          display: flex; align-items: center; gap: 6px;
          min-height: 22px;
        }
        .wf-orgchart-step-actor-initials {
          width: 22px; height: 22px;
          border-radius: 50%;
          background: color-mix(in oklch, var(--ok) 18%, var(--bg-1));
          display: flex; align-items: center; justify-content: center;
          font-size: 9.5px; font-weight: 600;
          color: var(--text-1);
          flex-shrink: 0;
          border: 1px solid color-mix(in oklch, var(--ok) 30%, transparent);
        }
        .wf-orgchart-step-actor-initials.global-source {
          background: var(--bg-2);
          border-color: var(--line);
          color: var(--text-2);
        }
        .wf-orgchart-step-gates {
          display: flex; flex-direction: column; gap: 3px;
          margin-top: auto;
        }
        .wf-orgchart-gate-row {
          font-size: 10px;
          padding: 3px 6px;
          border-radius: 4px;
          display: inline-flex; gap: 4px; align-items: center;
          width: fit-content;
        }
        .wf-orgchart-gate-ok    { background: color-mix(in oklch, var(--ok) 14%, var(--bg-1)); color: var(--ok); }
        .wf-orgchart-gate-warn  { background: color-mix(in oklch, var(--warn) 16%, var(--bg-1)); color: var(--warn); }
        .wf-orgchart-gate-err   { background: color-mix(in oklch, var(--err) 16%, var(--bg-1)); color: var(--err); cursor: pointer; }
        .wf-orgchart-gate-err:hover { background: color-mix(in oklch, var(--err) 24%, var(--bg-1)); }
      `}</style>
      {data.governance.map((wf) => (
        <WorkflowOrgChartTrack
          key={wf.instanceId}
          wf={wf}
          onOpenStep={onOpenStep}
          onGateClick={onGateClick}
        />
      ))}
      {data.operational.length > 0 && (
        <details style={{ marginTop: 14 }} open>
          <summary style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-2)', cursor: 'pointer', padding: '8px 4px' }}>
            🟨 Workflow operativi collegati ({data.operational.length})
          </summary>
          <div style={{ marginTop: 8 }}>
            {data.operational.map((wf) => (
              <WorkflowOrgChartTrack
                key={wf.instanceId}
                wf={wf}
                onOpenStep={onOpenStep}
                onGateClick={onGateClick}
                compact
              />
            ))}
          </div>
        </details>
      )}
    </>
  );
}

function WorkflowOrgChartTrack({ wf, onOpenStep, onGateClick, compact }) {
  const subtitle = wf.entityType === 'project'
    ? `${wf.steps.length} step`
    : `su ${wf.entityType.toUpperCase()} · ${wf.entityId} · ${wf.steps.length} step`;
  return (
    <div style={{ marginBottom: compact ? 8 : 14 }}>
      <div className="row" style={{ gap: 8, alignItems: 'center', marginBottom: 4, padding: '0 4px' }}>
        <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{wf.workflowCode}</span>
        <span style={{ fontSize: compact ? 11.5 : 12.5, fontWeight: 600 }}>{wf.workflowName}</span>
        <span style={{ fontSize: 10.5, color: 'var(--text-3)' }}>· {subtitle}</span>
      </div>
      <div className="wf-orgchart-track">
        {wf.steps.map((s) => (
          <WorkflowOrgChartStepCard
            key={s.stepStateId}
            step={s}
            onOpen={() => onOpenStep(wf.instanceId, s.stepStateId)}
            onGateClick={onGateClick}
          />
        ))}
      </div>
    </div>
  );
}

function WorkflowOrgChartStepCard({ step, onOpen, onGateClick }) {
  const stateClass = step.isSkipped
    ? 'skipped'
    : step.gates.result?.blocked && step.status === 'active' ? 'blocked'
    : step.status === 'active' ? 'active'
    : step.status === 'completed' ? 'completed'
    : '';

  const statusIcon = step.isSkipped ? '⏭️'
    : step.status === 'completed' ? '✅'
    : step.status === 'rejected' ? '❌'
    : step.status === 'active' ? '🟡'
    : '⏳';

  const initials = (name) => {
    if (!name) return '·';
    return name.split(/\s+/).map((p) => p[0]).slice(0, 2).join('').toUpperCase();
  };

  const actor = step.approver;
  return (
    <button
      type="button"
      className={`wf-orgchart-step ${stateClass}`}
      onClick={onOpen}
      title={step.isSkipped ? 'Step saltato (condizione di entry non rispettata)' : `Apri step "${step.stepName}"`}
    >
      <div className="row" style={{ gap: 6, alignItems: 'center', justifyContent: 'space-between' }}>
        <span className="wf-orgchart-step-num">{statusIcon} #{step.stepOrder + 1}</span>
        {step.sla.badge === 'late' && <Chip kind="err" dot>SLA</Chip>}
        {step.sla.badge === 'warn' && <Chip kind="warn" dot>SLA {step.sla.progressPct}%</Chip>}
      </div>
      <div className="wf-orgchart-step-name">{step.stepName}</div>
      <div className="wf-orgchart-step-actor">
        {actor.kind === 'role' && (actor.personas?.length > 0) ? (
          <>
            <span
              className={`wf-orgchart-step-actor-initials ${actor.personas[0].source === 'global' ? 'global-source' : ''}`}
              title={`${actor.personas.map((p) => `${p.name}${p.source === 'global' ? ' (global)' : ''}`).join(', ')}`}
            >
              {initials(actor.personas[0].name)}
            </span>
            <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
              {actor.personas[0].name}
              {actor.personas[0].source === 'global' && (
                <span style={{ color: 'var(--text-3)', fontSize: 9.5, marginLeft: 4 }} title="Persona globale del tenant, non nel team">🌐</span>
              )}
              {actor.totalMatchingMembers > 1 && (
                <span style={{ color: 'var(--text-3)', fontSize: 10 }}> +{actor.totalMatchingMembers - 1}</span>
              )}
            </span>
          </>
        ) : actor.kind === 'role' ? (
          <span style={{ color: 'var(--text-3)', fontStyle: 'italic' }} title={`Ruolo ${actor.roleId} — nessuno nel team`}>
            [non assegnato] · {actor.roleId}
          </span>
        ) : actor.kind === 'matrix' ? (
          <span title={actor.explainHint || 'Approver determinato a runtime'}>
            🔀 Matrix
          </span>
        ) : actor.kind === 'persona' ? (
          <>
            <span className="wf-orgchart-step-actor-initials">{initials(actor.personaName || actor.personaId)}</span>
            <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
              {actor.personaName || actor.personaId}
            </span>
          </>
        ) : (
          <span style={{ color: 'var(--text-3)' }}>—</span>
        )}
      </div>
      {step.gates.docCodes.length > 0 && (
        <div className="wf-orgchart-step-gates">
          {step.gates.docCodes.map((code) => {
            const m = step.gates.result?.missing?.find((mm) => mm.docTypeCode === code);
            const cls = !m ? 'wf-orgchart-gate-ok' : m.reason === 'unsigned' ? 'wf-orgchart-gate-warn' : 'wf-orgchart-gate-err';
            const icon = !m ? '✅' : m.reason === 'unsigned' ? '🟡' : '🔴';
            const label = !m ? 'firmato' : m.reason === 'unsigned' ? 'da firmare' : 'assente';
            const clickable = !!m;
            return (
              <span
                key={code}
                className={`wf-orgchart-gate-row ${cls}`}
                onClick={(e) => { if (clickable) { e.stopPropagation(); onGateClick?.(); } }}
                title={clickable ? `Vai al tab Checklist — ${code} ${label}` : `${code} ${label}`}
              >
                {icon} {code}
              </span>
            );
          })}
        </div>
      )}
      {step.gates.docCodes.length === 0 && (
        <div style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 'auto' }}>
          {step.isSkipped ? 'saltato' : 'no gates'}
        </div>
      )}
    </button>
  );
}

// Sessione 102 — esporta SignatureRequestModal per riuso dal centro attività
// (Alerts.jsx): un task `sign_document` deve essere firmabile IN-PLACE, anche
// per ruoli che non hanno accesso alla rotta del progetto (es. VENDOR esterno).
// Stesso pattern di WorkflowInstanceDetailModal esposto da RdA.jsx.

// ============================================================
// MyWorkTab — Fase 4 Project Cockpit (sessione 107)
// ============================================================
// Tab "Il mio lavoro" del progetto: 4 sezioni che mostrano alla persona
// loggata cosa deve fare LEI qui, cosa sta aspettando da altri, cosa è
// bloccato dalla compliance documentale, e cosa è già stato fatto.
//
// Pattern day-1:
// - task-azionabile-in-place: click "Da fare TU" → apre modal in-place
//   via window.<ModalName> (SignatureRequestModal / WorkflowInstanceDetailModal)
// - cross-tenant 404 graceful (mostra empty state se project not found)
// - error envelope 503 honored: se BE risponde 503 mostriamo banner con
//   bottone "Riprova"
//
// Empty state contestuali per ogni sezione: "Nessun task per te in questo
// progetto" / "Tutti i membri del team sono liberi" / "Tutti gli step
// sbloccati" / "Nessuna attività recente".
function MyWorkTab({ p, setTab, onCountChange }) {
  const { user, navigate, pushToast } = useStore();
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [reloadKey, setReloadKey] = React.useState(0);
  // Task azionabili (pattern task-azionabile-in-place): firma doc -> deep-link al
  // documento con evidenziazione; workflow -> modale dettaglio istanza.
  const [actionTask, setActionTask] = React.useState(null);

  React.useEffect(() => {
    if (!p?.id || !user?.id) return;
    let cancelled = false;
    setLoading(true);
    setError(null);
    (async () => {
      try {
        const res = await fetch(
          `/api/projects/${encodeURIComponent(p.id)}/my-work`,
          {
            cache: 'no-store',
            headers: { 'X-Actor-Persona-Id': user.id },
            credentials: 'same-origin',
          },
        );
        if (!res.ok) {
          const j = await res.json().catch(() => ({}));
          throw new Error(j.detail || j.error || `HTTP ${res.status}`);
        }
        const json = await res.json();
        if (cancelled) return;
        setData(json.data || null);
        if (typeof onCountChange === 'function') {
          onCountChange(json.data?.todoCount ?? 0);
        }
      } catch (err) {
        if (!cancelled) setError(String(err?.message || err));
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [p?.id, user?.id, reloadKey]);

  const reload = React.useCallback(() => setReloadKey((k) => k + 1), []);

  // Click su task "Da fare TU":
  //  - sign_document: porta al tab Documenti del progetto ed EVIDENZIA il documento
  //    (bordo rosso + scroll) via deep-link posizionale projectId|docs|docId, consumato
  //    da DocsTab.focusDocId. L'utente guarda il documento e decide se firmarlo: NESSUNA
  //    apertura automatica della firma (la firma si fa dalla schermata del documento).
  //  - workflow_approval: apre il dettaglio istanza (vista step + approva/rifiuta).
  const openTaskInPlace = React.useCallback((task) => {
    if (task.kind === 'sign_document' && task.entityId) {
      navigate('project_detail', [p.id, 'docs', task.entityId].join('|'));
      return;
    }
    if (task.kind === 'workflow_approval' && task.meta?.instanceId) {
      setActionTask(task);
      return;
    }
    setTab?.('overview');
  }, [navigate, p?.id, setTab]);

  if (loading && !data) {
    return (
      <div className="card" style={{ padding: 24, textAlign: 'center', color: 'var(--text-2)' }}>
        Caricamento…
      </div>
    );
  }
  if (error) {
    return (
      <div className="card" style={{ padding: 18 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
          <span style={{ color: 'var(--danger)', fontSize: 13, fontWeight: 600 }}>
            ⚠️ Caricamento fallito
          </span>
        </div>
        <div style={{ color: 'var(--text-2)', fontSize: 12, marginBottom: 12 }}>{error}</div>
        <Btn variant="primary" size="sm" onClick={reload}>Riprova</Btn>
      </div>
    );
  }
  if (!data) return null;

  const { todo = [], waitingOthers = [], blockedByDocs = [], timeline = [], isCallerInTeam, timelineHasMore } = data;

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      {/* Banner contesto */}
      {!isCallerInTeam && (
        <div className="card" style={{ padding: 12, background: 'rgba(245, 158, 11, 0.08)', borderColor: 'rgba(245, 158, 11, 0.3)' }}>
          <div style={{ fontSize: 12, color: 'var(--text-1)' }}>
            ℹ️ Hai task su questo progetto ma <strong>non sei membro del team</strong>.
            Contatta il PM per essere aggiunto se vuoi rimanere allineato sul resto del lavoro.
          </div>
        </div>
      )}

      {/* Layout 4 colonne desktop / stack mobile */}
      <div className="mywork-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 16 }}>

        {/* SEZIONE 1: Da fare TU */}
        <section className="card" style={{ padding: 14 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
            <span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--accent)' }} />
            <strong style={{ fontSize: 12, letterSpacing: 0.5, textTransform: 'uppercase' }}>Da fare TU</strong>
            <span className="mono" style={{ color: 'var(--text-3)', fontSize: 11, marginLeft: 'auto' }}>({todo.length})</span>
          </div>
          {todo.length === 0 ? (
            <div style={{ fontSize: 12, color: 'var(--text-3)', padding: '8px 0' }}>
              ✅ Nessun task per te in questo progetto.
            </div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {todo.map((t) => (
                <div key={t.id} className="card" style={{ padding: 10, background: 'var(--bg-2)' }}>
                  <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 2 }}>{t.title}</div>
                  <div style={{ fontSize: 11, color: 'var(--text-2)', marginBottom: 8 }}>{t.subtitle}</div>
                  <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
                    <Chip kind={t.priority === 'high' ? 'danger' : t.priority === 'medium' ? 'warn' : 'info'} size="xs">
                      {t.priority}
                    </Chip>
                    <Btn variant="primary" size="xs" onClick={() => openTaskInPlace(t)} style={{ marginLeft: 'auto' }}>Apri</Btn>
                  </div>
                </div>
              ))}
            </div>
          )}
        </section>

        {/* SEZIONE 2: In attesa di altri */}
        <section className="card" style={{ padding: 14 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
            <span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--warn)' }} />
            <strong style={{ fontSize: 12, letterSpacing: 0.5, textTransform: 'uppercase' }}>In attesa di altri</strong>
            <span className="mono" style={{ color: 'var(--text-3)', fontSize: 11, marginLeft: 'auto' }}>({waitingOthers.length})</span>
          </div>
          {waitingOthers.length === 0 ? (
            <div style={{ fontSize: 12, color: 'var(--text-3)', padding: '8px 0' }}>
              ✅ Tutti i membri del team sono liberi.
            </div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {waitingOthers.map((person) => (
                <div key={person.personaId} className="card" style={{ padding: 10, background: 'var(--bg-2)' }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
                    <div style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--accent-soft)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 600 }}>
                      {person.personaName.split(' ').map(w => w[0]).slice(0,2).join('').toUpperCase()}
                    </div>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 12, fontWeight: 600 }}>{person.personaName}</div>
                      <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{person.projectRole}</div>
                    </div>
                    <Chip kind="warn" size="xs">{person.todoCount} task</Chip>
                  </div>
                  {person.mostUrgentTask && (
                    <div style={{ fontSize: 11, color: 'var(--text-2)', marginTop: 4 }}>
                      Più urgente: <em>{person.mostUrgentTask.title}</em>
                    </div>
                  )}
                </div>
              ))}
            </div>
          )}
        </section>

        {/* SEZIONE 3: Bloccato da documenti */}
        <section className="card" style={{ padding: 14 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
            <span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--danger)' }} />
            <strong style={{ fontSize: 12, letterSpacing: 0.5, textTransform: 'uppercase' }}>Bloccato da documenti</strong>
            <span className="mono" style={{ color: 'var(--text-3)', fontSize: 11, marginLeft: 'auto' }}>({blockedByDocs.length})</span>
          </div>
          {blockedByDocs.length === 0 ? (
            <div style={{ fontSize: 12, color: 'var(--text-3)', padding: '8px 0' }}>
              ✅ Tutti gli step sono sbloccati.
            </div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {blockedByDocs.map((b) => (
                <div key={b.stepStateId} className="card" style={{ padding: 10, background: 'var(--bg-2)' }}>
                  <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 2 }}>
                    {b.workflowName} · step {b.stepName}
                  </div>
                  <div style={{ fontSize: 11, color: 'var(--text-2)', marginBottom: 8 }}>
                    Manca: {b.blockingDocCodes.map((c) => <code key={c} className="mono" style={{ marginRight: 4, fontSize: 10 }}>{c}</code>)}
                  </div>
                  <Btn variant="ghost" size="xs" onClick={() => setTab?.('checklist')}>Vai a Checklist →</Btn>
                </div>
              ))}
            </div>
          )}
        </section>

        {/* SEZIONE 4: Timeline */}
        <section className="card" style={{ padding: 14 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
            <span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--ok)' }} />
            <strong style={{ fontSize: 12, letterSpacing: 0.5, textTransform: 'uppercase' }}>Cronologia recente</strong>
            <span className="mono" style={{ color: 'var(--text-3)', fontSize: 11, marginLeft: 'auto' }}>({timeline.length}{timelineHasMore ? '+' : ''})</span>
          </div>
          {timeline.length === 0 ? (
            <div style={{ fontSize: 12, color: 'var(--text-3)', padding: '8px 0' }}>
              Nessuna attività recente sul progetto.
            </div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 400, overflowY: 'auto' }}>
              {timeline.map((ev) => (
                <div key={ev.auditId} style={{ padding: '6px 0', borderBottom: '1px solid var(--line)', fontSize: 11 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                    <span style={{ color: 'var(--text-3)', fontSize: 10 }}>{new Date(ev.ts).toLocaleString('it-IT', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}</span>
                    <Chip size="xs" kind={ev.actorType === 'system' ? 'info' : 'default'}>{ev.actorType === 'system' ? '🤖' : '👤'} {ev.actorName}</Chip>
                  </div>
                  <div style={{ marginTop: 2, color: 'var(--text-1)' }}>
                    <strong>{ev.action}</strong> · {ev.entityType} <code className="mono" style={{ fontSize: 10 }}>{ev.entityId}</code>
                  </div>
                </div>
              ))}
            </div>
          )}
        </section>

      </div>

      <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
        <Btn variant="ghost" size="sm" onClick={reload}>
          <Icon name="reload" size={12} /> Aggiorna
        </Btn>
      </div>

      {/* Workflow approval azionabile in-place (vista istanza + approva/rifiuta).
          La firma documento NON apre un modale: porta al tab Documenti ed evidenzia. */}
      {actionTask && actionTask.kind === 'workflow_approval' && actionTask.meta?.instanceId && typeof window !== 'undefined' && window.WorkflowInstanceDetailModal && (
        <window.WorkflowInstanceDetailModal
          instanceId={actionTask.meta.instanceId}
          user={user}
          pushToast={pushToast}
          onTransition={() => { reload(); window.dispatchEvent(new Event('my_tasks_changed')); }}
          onClose={() => setActionTask(null)}
        />
      )}
    </div>
  );
}

Object.assign(window, { ProjectDetail, SignatureRequestModal, MyWorkTab });
