// ============================================================
// customizing-sub-1.jsx — Sotto-pagine Customizing (parte 1)
// Doctypes · Checklists · Workflows · Matrix · Roles
// ============================================================

// -------------- DOC TYPES --------------
// Componenti condivisi tra modale crea e modale modifica (sessione 98, Tier 1).
const DOC_MIME_PRESETS = [
  { label: 'PDF', val: 'application/pdf' },
  { label: 'Word', val: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
  { label: 'Excel', val: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
  { label: 'Immagine', val: 'image/*' },
  { label: 'ZIP', val: 'application/zip' },
  { label: 'DWG/CAD', val: 'application/acad' },
  { label: 'XML', val: 'application/xml' },
];

// Selettore MIME ammessi: toggle preset + chip per valori custom non in preset.
function DocTypeMimePicker({ value, onChange }) {
  const mime = Array.isArray(value) ? value : [];
  const known = new Set(DOC_MIME_PRESETS.map((m) => m.val));
  const custom = mime.filter((m) => !known.has(m));
  return (
    <div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
        {DOC_MIME_PRESETS.map((m) => {
          const on = mime.includes(m.val);
          return (
            <button key={m.val} className={`btn sm ${on ? 'primary' : 'ghost'}`} style={{ fontSize: 10, padding: '2px 6px' }}
              onClick={() => onChange(on ? mime.filter((x) => x !== m.val) : [...mime, m.val])}>{m.label}</button>
          );
        })}
      </div>
      {custom.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 }}>
          {custom.map((m) => (
            <span key={m} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10, padding: '2px 6px', border: '1px solid var(--line)', borderRadius: 4, background: 'var(--bg-2)' }}>
              {m}
              <span onClick={() => onChange(mime.filter((x) => x !== m))} style={{ cursor: 'pointer', color: 'var(--text-3)' }}>×</span>
            </span>
          ))}
        </div>
      )}
    </div>
  );
}

// Selettore ruoli (chip dei selezionati + dropdown dai ROLES reali). Generico:
// usato per ruoli firmatari (doc type) e per i ruoli di sistema suggeriti di un
// ruolo di progetto (pre-rank Team, s124). Salva sempre role.id.
function DocTypeSignerRolesPicker({ value, seedCustom, onChange, placeholder = '+ aggiungi ruolo firmatario…' }) {
  const selected = Array.isArray(value) ? value : [];
  const roles = ((seedCustom && seedCustom.ROLES) || []).filter((r) => r.active !== false);
  const roleLabel = (rid) => {
    const r = roles.find((x) => x.id === rid || x.code === rid);
    return r ? `${r.name} (${r.code})` : rid;
  };
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, alignItems: 'center' }}>
      {selected.map((rid) => (
        <span key={rid} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10.5, padding: '2px 6px', border: '1px solid var(--line)', borderRadius: 4, background: 'var(--bg-2)' }}>
          {roleLabel(rid)}
          <span onClick={() => onChange(selected.filter((x) => x !== rid))} style={{ cursor: 'pointer', color: 'var(--text-3)' }}>×</span>
        </span>
      ))}
      <select value="" style={{ flex: '0 0 220px' }}
        onChange={(e) => { if (e.target.value) onChange([...selected, e.target.value]); }}>
        <option value="">{placeholder}</option>
        {roles.filter((r) => !selected.includes(r.id) && !selected.includes(r.code)).map((r) => <option key={r.id} value={r.id}>{r.name} ({r.code})</option>)}
      </select>
    </div>
  );
}

// Tipi ammessi per un campo dello schema metadati AI.
const DOC_META_TYPES = [
  { v: 'string', label: 'Testo' },
  { v: 'number', label: 'Numero' },
  { v: 'date', label: 'Data' },
  { v: 'boolean', label: 'Sì / No' },
];

// Editor dello schema metadati AI: Record<campo, tipo>. Campi esistenti read-only
// (rimuovi + ri-aggiungi per rinominare), riga in fondo per aggiungere.
function DocTypeMetadataSchemaEditor({ value, onChange }) {
  const schema = value && typeof value === 'object' ? value : {};
  const entries = Object.entries(schema);
  const [newKey, setNewKey] = React.useState('');
  const [newType, setNewType] = React.useState('string');
  const setField = (k, t) => onChange({ ...schema, [k]: t });
  const removeField = (k) => {
    const next = { ...schema };
    delete next[k];
    onChange(Object.keys(next).length ? next : null);
  };
  const addField = () => {
    const k = newKey.trim();
    if (!k || schema[k]) return;
    onChange({ ...schema, [k]: newType });
    setNewKey('');
    setNewType('string');
  };
  return (
    <div className="col" style={{ gap: 6 }}>
      {entries.length === 0 && (
        <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
          Nessun campo — aggiungine almeno uno per guidare l'estrazione AI.
        </div>
      )}
      {entries.map(([k, t]) => (
        <div key={k} className="row" style={{ gap: 6, alignItems: 'center' }}>
          <span className="mono" style={{ flex: '1 1 150px', fontSize: 11 }}>{k}</span>
          <select value={t} onChange={(e) => setField(k, e.target.value)} style={{ flex: '0 0 120px' }}>
            {DOC_META_TYPES.map((x) => <option key={x.v} value={x.v}>{x.label}</option>)}
          </select>
          <Btn variant="ghost" size="sm" onClick={() => removeField(k)}><Icon name="x" size={10}/></Btn>
        </div>
      ))}
      <div className="row" style={{ gap: 6, alignItems: 'center' }}>
        <input value={newKey} onChange={(e) => setNewKey(e.target.value)} placeholder="nome campo (es. importo_totale)" style={{ flex: '1 1 150px' }}/>
        <select value={newType} onChange={(e) => setNewType(e.target.value)} style={{ flex: '0 0 120px' }}>
          {DOC_META_TYPES.map((x) => <option key={x.v} value={x.v}>{x.label}</option>)}
        </select>
        <Btn variant="ghost" size="sm" onClick={addField} disabled={!newKey.trim()}><Icon name="plus" size={10}/> Aggiungi</Btn>
      </div>
    </div>
  );
}

function CustDocTypes() {
  const { seedCustom, extras } = useStore();
  // FASE 3a.3: merge extras.docTypesExt + seedCustom.DOC_TYPES con dedup per id (extras vince).
  // Pattern dedup-extras-seed.
  const types = React.useMemo(() => {
    const seedList = seedCustom.DOC_TYPES || [];
    const extList = extras?.docTypesExt || [];
    const seenIds = new Set();
    const out = [];
    for (const t of [...extList, ...seedList]) {
      if (!t?.id || seenIds.has(t.id)) continue;
      seenIds.add(t.id);
      out.push(t);
    }
    return out;
  }, [seedCustom.DOC_TYPES, extras?.docTypesExt]);
  const [q, setQ] = React.useState('');
  const [cat, setCat] = React.useState('*');
  const [sel, setSel] = React.useState(null);
  const [showNew, setShowNew] = React.useState(false);

  const categories = React.useMemo(() => Array.from(new Set(types.map(t => t.category))), [types]);
  // Sessione 98: "Uso" reale — quante regole checklist referenziano il code del doc type
  // (required + optional + forbidden). Sostituisce il placeholder usedIn=0.
  const usedByCode = React.useMemo(() => {
    const m = new Map();
    for (const r of seedCustom.CHECKLIST_RULES || []) {
      const codes = new Set([...(r.required || []), ...(r.optional || []), ...(r.forbidden || [])]);
      for (const c of codes) m.set(c, (m.get(c) || 0) + 1);
    }
    return m;
  }, [seedCustom.CHECKLIST_RULES]);
  const rows = types.filter(t => (cat === '*' || t.category === cat) && (!q || (t.name + t.code).toLowerCase().includes(q.toLowerCase())));

  return (
    <>
      <div className="row" style={{ gap: 8, marginBottom: 12, alignItems: 'center' }}>
        <input className="input" placeholder="Cerca tipo documento…" value={q} onChange={(e)=>setQ(e.target.value)} style={{ flex: 1, maxWidth: 320 }}/>
        <select value={cat} onChange={(e)=>setCat(e.target.value)}>
          <option value="*">Tutte le categorie</option>
          {categories.map(c => <option key={c} value={c}>{c}</option>)}
        </select>
        <span className="spacer"/>
        <Chip>{rows.length} su {types.length}</Chip>
        <ConfigWriteBtn onClick={()=>setShowNew(true)}><Icon name="plus" size={11}/> Nuovo tipo</ConfigWriteBtn>
      </div>

      <table className="tbl dense">
        <thead><tr>
          <th style={{width:110}}>Code</th><th>Nome</th><th style={{width:120}}>Categoria</th>
          <th style={{width:90,textAlign:'center'}}>Firma</th>
          <th style={{width:90,textAlign:'center'}}>Scadenza</th>
          <th style={{width:90,textAlign:'center'}}>AI extract</th>
          <th style={{width:80,textAlign:'right'}} title="Regole checklist che referenziano questo tipo">Checklist</th>
          <th style={{width:40}}></th>
        </tr></thead>
        <tbody>
          {rows.map(t => (
            <tr key={t.id} className="clickable" onClick={()=>setSel(t)}>
              <td className="mono" style={{fontSize:11}}>{t.code}</td>
              <td style={{fontWeight:500}}>{t.name}</td>
              <td><Chip>{t.category}</Chip></td>
              <td style={{textAlign:'center'}}>{t.requiresSignature ? <Chip kind={t.digitalSignature?'ok':''} dot>{t.digitalSignature?'digitale':'grafometrica'}</Chip> : <span style={{color:'var(--text-3)'}}>—</span>}</td>
              <td style={{textAlign:'center'}}>{t.hasExpiry ? <Chip kind="warn">{t.validityMonths}m</Chip> : <span style={{color:'var(--text-3)'}}>—</span>}</td>
              <td style={{textAlign:'center'}}>{t.aiExtract ? <Chip kind="ai" dot>on</Chip> : <span style={{color:'var(--text-3)'}}>off</span>}</td>
              <td className="mono num" style={{textAlign:'right'}}>{usedByCode.get(t.code) || 0}</td>
              <td><Icon name="chevron_right" size={12}/></td>
            </tr>
          ))}
        </tbody>
      </table>

      <DocTypeDetailModal sel={sel} onClose={()=>setSel(null)} categories={categories} />

      <NewDocTypeModal open={showNew} onClose={()=>setShowNew(false)} types={types} categories={categories}/>
    </>
  );
}

function NewDocTypeModal({ open, onClose, types, categories }) {
  const { addDocType, pushToast, user, seedCustom } = useStore();
  const [form, setForm] = React.useState({
    code: '', name: '', category: categories[0] || 'tecnico', description: '',
    mime: ['application/pdf'], maxSizeMB: 25, versioning: true,
    requiresSignature: false, digitalSignature: false,
    hasExpiry: false, validityMonths: 12,
    signerRoles: [], aiClassify: false, aiExtract: false,
    templateId: '', aiClassifyPromptId: '', aiMetadataSchema: null,
  });
  const [saving, setSaving] = React.useState(false);
  const [serverError, setServerError] = React.useState(null);
  React.useEffect(() => {
    if (open) {
      setForm({
        code: '', name: '', category: categories[0] || 'tecnico', description: '',
        mime: ['application/pdf'], maxSizeMB: 25, versioning: true,
        requiresSignature: false, digitalSignature: false,
        hasExpiry: false, validityMonths: 12,
        signerRoles: [], aiClassify: false, aiExtract: false,
      });
      setServerError(null);
    }
  }, [open]);
  const set = (k, v) => setForm(f => ({...f, [k]: v}));

  const codeValid = /^[A-Z][A-Z0-9_]{1,31}$/.test(form.code);
  const codeUnique = !types.some(t => t.code === form.code);
  const valid = form.name.trim() && codeValid && codeUnique;

  async function handleSubmit() {
    if (!valid || saving) return;
    setSaving(true);
    setServerError(null);
    try {
      const res = await fetch('/api/config/doc-types', {
        method: 'POST',
        headers: {
          'content-type': 'application/json',
          ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}),
        },
        body: JSON.stringify({
          code: form.code,
          name: form.name.trim(),
          description: form.description.trim() || null,
          category: form.category, // BE Zod normalizza "qualità"/"HSE" → "qualita"/"hse"
          mime: form.mime,
          maxSizeMB: form.maxSizeMB,
          versioning: !!form.versioning,
          requiresSignature: !!form.requiresSignature,
          digitalSignature: !!form.digitalSignature,
          signerRoles: form.signerRoles,
          hasExpiry: !!form.hasExpiry,
          validityMonths: form.hasExpiry ? form.validityMonths : null,
          aiClassify: !!form.aiClassify,
          aiExtract: !!form.aiExtract,
          aiClassifyPromptId: form.aiClassifyPromptId || null,
          aiMetadataSchema: form.aiMetadataSchema || null,
          templateId: form.templateId || null,
          active: true,
        }),
      });
      const json = await res.json().catch(() => ({}));
      if (!res.ok) {
        const msg = json?.error === 'validation_error'
          ? `Validazione fallita: ${(json.issues || []).map(i => i.message).join(' · ') || 'campi non validi'}`
          : (json?.error || `HTTP ${res.status}`);
        setServerError(msg);
        return;
      }
      const created = json?.data;
      if (created) {
        addDocType(created);
        pushToast({ title: `${created.code} · ${created.name}`, desc: 'Tipo documento salvato in DB. Audit log registrato.', tone: 'ok' });
      }
      onClose();
    } catch (err) {
      setServerError(String(err?.message || err));
    } finally {
      setSaving(false);
    }
  }

  return (
    <Modal open={open} onClose={onClose} title="Nuovo tipo documento" size="lg"
      footer={<>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
        <Btn variant="primary" size="sm" disabled={!valid || saving} onClick={handleSubmit}>{saving ? 'Salvataggio…' : 'Crea tipo'}</Btn>
      </>}>
      <div className="col" style={{gap:14}}>
        <div style={{fontSize:11.5, color:'var(--text-2)', lineHeight:1.5}}>
          Registra un nuovo tipo documento riconosciuto dal DMS. Sarà utilizzabile nelle <strong>regole checklist</strong>, nei <strong>gate di workflow</strong> e nei template di comunicazione. Vedi <code>docs/page-documents.md §Tipologie</code>.
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Identificativo</div>
          <div className="grid grid-3">
            <div className="field"><label>Code <span style={{color:'var(--err)'}}>*</span></label>
              <input value={form.code} onChange={e=>set('code', e.target.value.toUpperCase().replace(/[^A-Z0-9_]/g,''))} placeholder="SPEC_TECH" style={{fontFamily:'var(--font-mono)'}}/>
            </div>
            <div className="field" style={{gridColumn:'span 2'}}><label>Nome visualizzato <span style={{color:'var(--err)'}}>*</span></label>
              <input value={form.name} onChange={e=>set('name', e.target.value)} placeholder="es. Specifica tecnica firmata"/>
            </div>
          </div>
          <div className="field" style={{marginTop:8}}><label>Descrizione</label>
            <textarea rows={2} value={form.description} onChange={e=>set('description', e.target.value)} placeholder="A cosa serve, chi lo produce, link a template"/>
          </div>
          {form.code && !codeValid && <div style={{fontSize:10.5, color:'var(--err)'}}><Icon name="alert-triangle" size={10}/> Code deve iniziare con maiuscola e contenere solo A-Z, 0-9, _</div>}
          {codeValid && !codeUnique && <div style={{fontSize:10.5, color:'var(--err)'}}><Icon name="alert-triangle" size={10}/> Code già esistente</div>}
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Classificazione</div>
          <div className="grid grid-2">
            <div className="field"><label>Categoria</label>
              <select value={form.category} onChange={e=>set('category', e.target.value)}>
                {categories.map(c => <option key={c} value={c}>{c}</option>)}
                <option value="altro">altro</option>
              </select>
            </div>
            <div className="field"><label>Dim. max (MB)</label>
              <input type="number" min={1} max={500} value={form.maxSizeMB ?? ''} onChange={e=>set('maxSizeMB', Number(e.target.value))}/>
            </div>
          </div>
          <div style={{marginTop:8}}>
            <label style={{fontSize:10.5, color:'var(--text-3)'}}>MIME ammessi</label>
            <div style={{marginTop:4}}>
              <DocTypeMimePicker value={form.mime} onChange={(v)=>set('mime', v)}/>
            </div>
          </div>
          <div className="field" style={{marginTop:8}}><label>Template documentale associato</label>
            <window.Autocomplete value={form.templateId || ''} onChange={v=>set('templateId', v)}
              options={(seedCustom.TEMPLATES || []).map(tpl => ({ value: tpl.id, label: tpl.name, sublabel: tpl.code }))}
              placeholder="Nessun template · cerca…" testId="cust-doctype-template-ac" />
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Versioning & firma</div>
          <div className="col" style={{gap:6}}>
            <label className="row" style={{gap:6, fontSize:11.5}}>
              <input type="checkbox" checked={!!form.versioning} onChange={e=>set('versioning', e.target.checked)}/>
              Versioning abilitato (tiene storico upload)
            </label>
            <label className="row" style={{gap:6, fontSize:11.5}}>
              <input type="checkbox" checked={!!form.requiresSignature} onChange={e=>set('requiresSignature', e.target.checked)}/>
              Richiede firma
            </label>
            {form.requiresSignature && (
              <div style={{marginLeft:20, padding:8, border:'1px solid var(--line)', borderRadius:4}}>
                <label className="row" style={{gap:6, fontSize:11.5}}>
                  <input type="checkbox" checked={!!form.digitalSignature} onChange={e=>set('digitalSignature', e.target.checked)}/>
                  Firma digitale qualificata (eIDAS/CAdES). Se off: firma grafometrica o OTP.
                </label>
                <div style={{marginTop:8}}>
                  <div style={{fontSize:10.5, color:'var(--text-3)', marginBottom:4}}>Ruoli firmatari ammessi</div>
                  <DocTypeSignerRolesPicker value={form.signerRoles} seedCustom={seedCustom} onChange={(v)=>set('signerRoles', v)}/>
                </div>
              </div>
            )}
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Scadenza</div>
          <label className="row" style={{gap:6, fontSize:11.5}}>
            <input type="checkbox" checked={!!form.hasExpiry} onChange={e=>set('hasExpiry', e.target.checked)}/>
            Il documento ha una scadenza (certificazioni, polizze, DURC…)
          </label>
          {form.hasExpiry && (
            <div className="grid grid-2" style={{marginTop:8, marginLeft:20}}>
              <div className="field"><label>Validità (mesi)</label>
                <input type="number" min={1} max={120} value={form.validityMonths} onChange={e=>set('validityMonths', Number(e.target.value))}/>
              </div>
            </div>
          )}
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>AI</div>
          <div className="col" style={{gap:6}}>
            <label className="row" style={{gap:6, fontSize:11.5}}>
              <input type="checkbox" checked={!!form.aiClassify} onChange={e=>set('aiClassify', e.target.checked)}/>
              AI classification on upload (riconosce il tipo anche se l'utente sbaglia categoria)
            </label>
            {form.aiClassify && (
              <div className="field" style={{marginLeft:20}}><label>Agente AI per la classificazione</label>
                <window.Autocomplete value={form.aiClassifyPromptId || ''} onChange={v=>set('aiClassifyPromptId', v)}
                  options={(seedCustom.AI_AGENTS || []).map(a => ({ value: a.id, label: a.name, sublabel: a.code }))}
                  placeholder="Agente di default · cerca…" testId="cust-doctype-aiagent-ac" />
              </div>
            )}
            <label className="row" style={{gap:6, fontSize:11.5}}>
              <input type="checkbox" checked={!!form.aiExtract} onChange={e=>set('aiExtract', e.target.checked)}/>
              AI metadata extraction (estrae campi strutturati: importi, date, P.IVA, ecc.)
            </label>
            {form.aiExtract && (
              <div style={{marginLeft:20}}>
                <div style={{fontSize:10.5, color:'var(--text-3)', marginBottom:4}}>Schema metadati da estrarre (campo → tipo)</div>
                <DocTypeMetadataSchemaEditor value={form.aiMetadataSchema} onChange={(v)=>set('aiMetadataSchema', v)}/>
              </div>
            )}
          </div>
        </div>

        <div style={{fontSize:10.5, color:'var(--text-3)', padding:'8px 10px', background:'var(--bg-2)', borderRadius:4, lineHeight:1.5}}>
          <Icon name="info" size={10}/> <code>POST /api/config/doc-types</code> persiste in DB con audit log automatico, incluse le impostazioni di firma, scadenza, template e AI.
        </div>

        {serverError && (
          <div style={{fontSize:11, color:'var(--err)', padding:'8px 10px', background:'rgba(239,68,68,0.06)', border:'1px solid var(--err)', borderRadius:4}}>
            <Icon name="alert-triangle" size={10}/> {serverError}
          </div>
        )}
      </div>
    </Modal>
  );
}
function CustChecklists() {
  const { seedCustom, user } = useStore();
  const canWriteConfig = window.can('config.update', user, seedCustom);
  const rules = seedCustom.CHECKLIST_RULES || [];
  const docs = seedCustom.DOC_TYPES || [];
  const byCode = {};
  docs.forEach(d => { byCode[d.code] = d; });
  const [sel, setSel] = React.useState(null);
  const [filter, setFilter] = React.useState('*');
  const [showNew, setShowNew] = React.useState(false);
  const [showSim, setShowSim] = React.useState(false);
  const entityTypes = Array.from(new Set(rules.map(r => r.entityType)));

  const rows = filter === '*' ? rules : rules.filter(r => r.entityType === filter);

  return (
    <>
      <div className="row" style={{ gap: 8, marginBottom: 12, alignItems:'center' }}>
        <div className="row" style={{ gap: 2, background: 'var(--bg-2)', padding: 2, borderRadius: 6 }}>
          <button className={`btn sm ${filter==='*'?'primary':'ghost'}`} onClick={()=>setFilter('*')}>Tutte</button>
          {entityTypes.map(t => (
            <button key={t} className={`btn sm ${filter===t?'primary':'ghost'}`} onClick={()=>setFilter(t)}>{t}</button>
          ))}
        </div>
        <span className="spacer"/>
        <Btn variant="ghost" size="sm" onClick={()=>setShowSim(true)}><Icon name="eye" size={11}/> Simula su entità</Btn>
        <Btn
          variant={canWriteConfig ? 'primary' : 'ghost'}
          size="sm"
          disabled={!canWriteConfig}
          title={canWriteConfig ? undefined : window.whyDisabled('config.update')}
          onClick={()=>{ if (canWriteConfig) setShowNew(true); }}
        ><Icon name="plus" size={11}/> Nuova regola</Btn>
      </div>

      <div style={{ fontSize: 11.5, color: 'var(--text-2)', padding: 10, background: 'var(--bg-2)', borderRadius: 8, marginBottom: 12 }}>
        <Icon name="info" size={11}/> Le regole sono valutate in ordine di <strong>priorità crescente</strong>: i documenti richiesti vengono uniti da tutte le regole matching. Una regola con <code>allowOverride=false</code> impedisce l'override manuale.
      </div>

      <table className="tbl dense">
        <thead><tr>
          <th style={{width:100}}>ID</th>
          <th>Nome</th>
          <th style={{width:90}}>Entità</th>
          <th style={{width:70,textAlign:'center'}}>Prio</th>
          <th>Condizione</th>
          <th style={{width:60,textAlign:'center'}}>Req</th>
          <th style={{width:60,textAlign:'center'}}>Opt</th>
          <th style={{width:90,textAlign:'center'}}>Override</th>
          <th style={{width:70,textAlign:'center'}}>Stato</th>
        </tr></thead>
        <tbody>
          {rows.map(r => (
            <tr key={r.id} className="clickable" onClick={()=>setSel(r)}>
              <td className="mono" style={{fontSize:10.5}}>{r.id}</td>
              <td style={{fontWeight:500}}>{r.name}</td>
              <td><Chip>{r.entityType}</Chip></td>
              <td className="mono num">{r.priority}</td>
              <td style={{fontSize:11.5, color:'var(--text-2)'}}>{formatCondition(r.conditions)}</td>
              <td className="mono num">{(r.required||[]).length}</td>
              <td className="mono num" style={{color:'var(--text-3)'}}>{(r.optional||[]).length}</td>
              <td style={{textAlign:'center'}}>{r.allowOverride ? <Chip>sì</Chip> : <Chip kind="err">no</Chip>}</td>
              <td style={{textAlign:'center'}}><Chip kind={r.active?'ok':''} dot>{r.active?'attiva':'off'}</Chip></td>
            </tr>
          ))}
        </tbody>
      </table>

      <Modal open={!!sel} onClose={()=>setSel(null)} title={sel ? sel.name : ''} size="lg" footer={
        <>
          <Btn variant="ghost" size="sm" onClick={()=>setSel(null)}>Chiudi</Btn>
          <Btn variant="ghost" size="sm" onClick={()=>{ setShowSim(true); setSel(null); }}><Icon name="eye" size={11}/> Simula</Btn>
          <Btn variant="primary" size="sm"><Icon name="check" size={11}/> Salva</Btn>
        </>
      }>
        {sel && (
          <div className="col" style={{ gap: 14 }}>
            <div className="grid grid-3">
              <div className="field"><label>ID</label><input defaultValue={sel.id} disabled/></div>
              <div className="field"><label>Entità target</label><select defaultValue={sel.entityType}>{entityTypes.map(t => <option key={t}>{t}</option>)}</select></div>
              <div className="field"><label>Priorità</label><input type="number" defaultValue={sel.priority}/></div>
            </div>
            <div className="field"><label>Condizione di match</label>
              <div style={{ padding: 10, background: 'var(--bg-2)', borderRadius: 8, fontFamily: 'var(--font-mono)', fontSize: 12 }}>
                {renderConditionTree(sel.conditions)}
              </div>
            </div>
            <div className="grid grid-2" style={{ gap: 14 }}>
              <div>
                <div className="eyebrow" style={{marginBottom:6}}>Documenti obbligatori ({(sel.required||[]).length})</div>
                <div style={{ display:'grid', gap:8 }}>
                  {groupDocTypesByCategory((sel.required||[]).map(code => byCode[code] || { code, name: code, category: 'altro' })).map(({ category, label, items }) => (
                    <div key={category}>
                      <div className="eyebrow" style={{ fontSize: 9.5, marginBottom: 4, color: 'var(--text-3)' }}>{label}</div>
                      <div style={{ display:'grid', gap:4 }}>
                        {items.map(d => (
                          <div key={d.code} className="row" style={{ gap: 8, padding: '6px 10px', border: '1px solid var(--line)', borderRadius: 6, background: 'var(--bg-1)' }}>
                            <Icon name="lock" size={12}/>
                            <div style={{fontSize:12.5, fontWeight:500}}>{d.name || d.code}</div>
                            <span className="spacer"/>
                            <Chip>{d.code}</Chip>
                          </div>
                        ))}
                      </div>
                    </div>
                  ))}
                </div>
              </div>
              <div>
                <div className="eyebrow" style={{marginBottom:6}}>Documenti opzionali ({(sel.optional||[]).length})</div>
                <div style={{ display:'grid', gap:8 }}>
                  {((sel.optional||[]).length > 0) ? groupDocTypesByCategory((sel.optional||[]).map(code => byCode[code] || { code, name: code, category: 'altro' })).map(({ category, label, items }) => (
                    <div key={category}>
                      <div className="eyebrow" style={{ fontSize: 9.5, marginBottom: 4, color: 'var(--text-3)' }}>{label}</div>
                      <div style={{ display:'grid', gap:4 }}>
                        {items.map(d => (
                          <div key={d.code} className="row" style={{ gap: 8, padding: '6px 10px', border: '1px dashed var(--line)', borderRadius: 6, background: 'var(--bg-1)' }}>
                            <Icon name="check" size={12}/>
                            <div style={{fontSize:12.5}}>{d.name || d.code}</div>
                            <span className="spacer"/>
                            <Chip>{d.code}</Chip>
                          </div>
                        ))}
                      </div>
                    </div>
                  )) : <div style={{fontSize:12, color:'var(--text-3)'}}>Nessun opzionale</div>}
                </div>
              </div>
            </div>
            <div className="row" style={{ gap: 14, padding: 10, background: 'var(--bg-2)', borderRadius: 8 }}>
              <div><div className="eyebrow">Versione</div><div style={{fontSize:13, fontWeight:500}}>v{sel.version}</div></div>
              <div><div className="eyebrow">Aggiornata il</div><div style={{fontSize:13}}>{sel.updatedAt}</div></div>
              <div><div className="eyebrow">Da</div><div style={{fontSize:13}}>{sel.updatedBy}</div></div>
              <div><div className="eyebrow">Override consentito</div><div style={{fontSize:13}}><Chip kind={sel.allowOverride?'ok':'err'}>{sel.allowOverride?'sì':'no'}</Chip></div></div>
            </div>
          </div>
        )}
      </Modal>

      <NewChecklistRuleModal open={showNew} onClose={()=>setShowNew(false)} rules={rules} docs={docs} entityTypes={entityTypes}/>
      <ChecklistSimulateModal open={showSim} onClose={()=>setShowSim(false)} rules={rules} docs={docs} entityTypes={entityTypes}/>
    </>
  );
}

// ---------- checklist helpers ----------
function evalCondition(c, ctx) {
  if (!c) return true;
  if (c.op === 'always') return true;
  if (c.op === 'never') return false;
  if (c.op === 'and') return (c.args||[]).every(a => evalCondition(a, ctx));
  if (c.op === 'or')  return (c.args||[]).some(a => evalCondition(a, ctx));
  if (c.op === 'not') return !evalCondition(c.arg || (c.args && c.args[0]), ctx);
  const v = (c.field || '').split('.').reduce((o,k)=>o==null?undefined:o[k], ctx);
  switch (c.op) {
    case 'eq': return v === c.value;
    case 'neq': return v !== c.value;
    case 'gt': return Number(v) > Number(c.value);
    case 'gte': return Number(v) >= Number(c.value);
    case 'lt': return Number(v) < Number(c.value);
    case 'lte': return Number(v) <= Number(c.value);
    case 'in': return Array.isArray(c.value) && c.value.includes(v);
    case 'nin': return Array.isArray(c.value) && !c.value.includes(v);
    case 'exists': return v != null;
    default: return false;
  }
}

// Raggruppamento tipi documento per categoria — UX selezione checklist
// (più facile individuarli a colpo d'occhio). Categorie = enum doc_type_category.
const DOC_CATEGORY_ORDER = ['tecnico', 'economico', 'legale', 'qualita', 'hse', 'approvazione', 'altro'];
const DOC_CATEGORY_LABELS = {
  tecnico: 'Tecnico', economico: 'Economico', legale: 'Legale',
  qualita: 'Qualità', hse: 'HSE', approvazione: 'Approvazione', altro: 'Altro',
};
function groupDocTypesByCategory(docList) {
  const buckets = new Map();
  for (const d of docList || []) {
    const cat = d.category || 'altro';
    if (!buckets.has(cat)) buckets.set(cat, []);
    buckets.get(cat).push(d);
  }
  const rank = (cat) => {
    const i = DOC_CATEGORY_ORDER.indexOf(cat);
    return i < 0 ? DOC_CATEGORY_ORDER.length : i;
  };
  return Array.from(buckets.entries())
    .sort((a, b) => rank(a[0]) - rank(b[0]) || a[0].localeCompare(b[0]))
    .map(([category, items]) => ({
      category,
      label: DOC_CATEGORY_LABELS[category] || (category.charAt(0).toUpperCase() + category.slice(1)),
      items: items.slice().sort((x, y) => (x.code || '').localeCompare(y.code || '')),
    }));
}

// Bottone di scrittura config gated (pattern fe-capability-gating-button centralizzato).
// Legge user/seedCustom da useStore: se manca il permesso (default config.update) il
// bottone è disabilitato con tooltip esplicativo invece di aprire la modale e prendere
// un 403. Usabile in tutte le customizing-sub-* (funzione globale, hoisted cross-script).
function ConfigWriteBtn({ perm = 'config.update', variant = 'primary', size = 'sm', title, onClick, disabled, children, ...rest }) {
  const { user, seedCustom } = useStore();
  const allowed = window.can(perm, user, seedCustom);
  return (
    <Btn
      variant={allowed ? variant : 'ghost'}
      size={size}
      disabled={disabled || !allowed}
      title={allowed ? title : window.whyDisabled(perm)}
      onClick={(e) => { if (allowed && onClick) onClick(e); }}
      {...rest}
    >
      {children}
    </Btn>
  );
}
if (typeof window !== 'undefined') window.ConfigWriteBtn = ConfigWriteBtn;

function NewChecklistRuleModal({ open, onClose, rules, docs, entityTypes }) {
  const nextId = React.useMemo(() => {
    const prefix = 'CR_NEW_';
    const nums = rules.filter(r => r.id.startsWith(prefix)).length;
    return `${prefix}${String(nums + 1).padStart(3, '0')}`;
  }, [rules, open]);

  const [form, setForm] = React.useState({
    name: '', entityType: entityTypes[0] || 'rda', priority: 10,
    condOp: 'always', condField: 'entity.amount', condCmp: 'gt', condValue: '',
    required: [], optional: [],
    allowOverride: false, active: true,
  });
  React.useEffect(() => {
    if (open) setForm({
      name: '', entityType: entityTypes[0] || 'rda', priority: 10,
      condOp: 'always', condField: 'entity.amount', condCmp: 'gt', condValue: '',
      required: [], optional: [],
      allowOverride: false, active: true,
    });
  }, [open]);
  const set = (k, v) => setForm(f => ({...f, [k]: v}));

  const valid = form.name.trim() && form.required.length > 0 && (form.condOp === 'always' || (form.condField && form.condValue !== ''));

  const buildCondition = () => {
    if (form.condOp === 'always') return { op: 'always' };
    let value = form.condValue;
    if (form.condCmp === 'in' || form.condCmp === 'nin') {
      value = String(value).split(',').map(s => s.trim()).filter(Boolean);
    } else if (['gt','gte','lt','lte'].includes(form.condCmp)) {
      value = Number(value);
    }
    return { op: form.condCmp, field: form.condField, value };
  };

  const toggleDoc = (code, list) => {
    const other = list === 'required' ? 'optional' : 'required';
    if (form[list].includes(code)) set(list, form[list].filter(x => x !== code));
    else {
      set(list, [...form[list], code]);
      if (form[other].includes(code)) set(other, form[other].filter(x => x !== code));
    }
  };

  const availableDocs = docs; // all doc types

  return (
    <Modal open={open} onClose={onClose} title="Nuova regola checklist" size="lg"
      footer={<>
        <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
        <Btn variant="primary" size="sm" disabled={!valid} onClick={onClose}>Crea regola</Btn>
      </>}>
      <div className="col" style={{gap:14}}>
        <div style={{fontSize:11.5, color:'var(--text-2)', lineHeight:1.5}}>
          Le regole checklist determinano quali documenti sono richiesti su un'entità (RdA, vendor, progetto…). Più regole matchanti su stessa entità vengono <strong>unite</strong>. Vedi <code>docs/checklist-engine.md</code>.
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Identificativo</div>
          <div className="grid grid-3">
            <div className="field"><label>ID regola</label><input disabled value={nextId} style={{fontFamily:'var(--font-mono)', fontSize:11}}/></div>
            <div className="field" style={{gridColumn:'span 2'}}><label>Nome <span style={{color:'var(--err)'}}>*</span></label>
              <input value={form.name} onChange={e=>set('name', e.target.value)} placeholder="es. RdA con vendor extra-UE"/>
            </div>
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Scope e priorità</div>
          <div className="grid grid-3">
            <div className="field"><label>Entità target</label>
              <select value={form.entityType} onChange={e=>set('entityType', e.target.value)}>
                {['rda','vendor','progetto','contratto','sal','variante'].map(t => <option key={t} value={t}>{t}</option>)}
              </select>
            </div>
            <div className="field"><label>Priorità (più alta = applicata dopo)</label>
              <input type="number" min={0} max={100} value={form.priority} onChange={e=>set('priority', Number(e.target.value))}/>
            </div>
            <div className="field"><label>Stato</label>
              <select value={form.active?'on':'off'} onChange={e=>set('active', e.target.value==='on')}>
                <option value="on">Attiva</option><option value="off">Draft</option>
              </select>
            </div>
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Condizione di match</div>
          <div className="grid grid-2">
            <div className="field"><label>Tipo</label>
              <select value={form.condOp} onChange={e=>set('condOp', e.target.value)}>
                <option value="always">Sempre applicabile</option>
                <option value="expr">Espressione su entità</option>
              </select>
            </div>
          </div>
          {form.condOp === 'expr' && (
            <div className="grid grid-3" style={{marginTop:8}}>
              <div className="field"><label>Campo</label>
                <input value={form.condField} onChange={e=>set('condField', e.target.value)} placeholder="entity.amount" style={{fontFamily:'var(--font-mono)', fontSize:11}}/>
              </div>
              <div className="field"><label>Operatore</label>
                <select value={form.condCmp} onChange={e=>set('condCmp', e.target.value)}>
                  <option value="eq">= uguale</option>
                  <option value="neq">≠ diverso</option>
                  <option value="gt">&gt; maggiore</option>
                  <option value="gte">≥ maggiore o uguale</option>
                  <option value="lt">&lt; minore</option>
                  <option value="lte">≤ minore o uguale</option>
                  <option value="in">in (lista)</option>
                  <option value="nin">∉ non in (lista)</option>
                  <option value="exists">exists</option>
                </select>
              </div>
              <div className="field"><label>Valore {['in','nin'].includes(form.condCmp) && <span style={{color:'var(--text-3)'}}>(virgole)</span>}</label>
                <input value={form.condValue} onChange={e=>set('condValue', e.target.value)} placeholder={['in','nin'].includes(form.condCmp) ? 'IT, DE, FR' : '100000'}/>
              </div>
            </div>
          )}
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Documenti richiesti (click per aggiungere)</div>
          <div style={{padding:8, border:'1px solid var(--line)', borderRadius:4, maxHeight:280, overflowY:'auto'}}>
            {groupDocTypesByCategory(availableDocs).map(({ category, label, items }) => (
              <div key={category} style={{ marginBottom: 10 }}>
                <div className="eyebrow" style={{ fontSize: 9.5, marginBottom: 4, color: 'var(--text-3)' }}>{label} · {items.length}</div>
                <div style={{display:'flex', flexWrap:'wrap', gap:4}}>
                  {items.map(d => {
                    const inReq = form.required.includes(d.code);
                    const inOpt = form.optional.includes(d.code);
                    return (
                      <div key={d.code} className="row" style={{gap:2, padding:'2px 6px', border:'1px solid var(--line)', borderRadius:4, fontSize:10.5}} title={d.name || d.code}>
                        <span style={{fontFamily:'var(--font-mono)'}}>{d.code}</span>
                        <span style={{color:'var(--text-3)', marginLeft:4, marginRight:4}}>·</span>
                        <button className={`btn sm ${inReq?'primary':'ghost'}`} style={{fontSize:9, padding:'1px 4px'}} onClick={()=>toggleDoc(d.code, 'required')}>req</button>
                        <button className={`btn sm ${inOpt?'primary':'ghost'}`} style={{fontSize:9, padding:'1px 4px'}} onClick={()=>toggleDoc(d.code, 'optional')}>opt</button>
                      </div>
                    );
                  })}
                </div>
              </div>
            ))}
          </div>
          <div style={{marginTop:6, fontSize:10.5, color:'var(--text-3)'}}>
            <strong style={{color:'var(--text-2)'}}>Richiesti ({form.required.length}):</strong> {form.required.join(', ') || '—'} · <strong style={{color:'var(--text-2)'}}>Opzionali ({form.optional.length}):</strong> {form.optional.join(', ') || '—'}
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Override</div>
          <label className="row" style={{gap:6, fontSize:11.5, cursor:'pointer'}}>
            <input type="checkbox" checked={form.allowOverride} onChange={e=>set('allowOverride', e.target.checked)}/>
            Consenti override manuale (utente privilegiato può saltare i documenti mancanti)
          </label>
        </div>

        {!valid && (
          <div style={{fontSize:10.5, color:'var(--err)', padding:'6px 8px', background:'var(--bg-2)', borderRadius:4}}>
            <Icon name="alert-triangle" size={10}/> Nome, almeno un documento richiesto e (se espressione) campo+valore sono obbligatori.
          </div>
        )}

        <div style={{fontSize:10.5, color:'var(--text-3)', padding:'8px 10px', background:'var(--bg-2)', borderRadius:4, lineHeight:1.5}}>
          <Icon name="info" size={10}/> Mock: <code>POST /api/config/checklist-rules</code> crea una nuova versione in draft; attiva dopo publish (vedi <code>customizing-overview.md §Versioning</code>).
        </div>
      </div>
    </Modal>
  );
}

function ChecklistSimulateModal({ open, onClose, rules, docs, entityTypes }) {
  const [entityType, setEntityType] = React.useState(entityTypes[0] || 'rda');
  const [fields, setFields] = React.useState({ amount: 150000, category: 'Automazione linea', country: 'IT', atex: false, budget: 500000 });

  const setField = (k, v) => setFields(f => ({...f, [k]: v}));

  const ctx = { entity: fields };
  const applicable = rules
    .filter(r => r.entityType === entityType && r.active)
    .map(r => ({ r, match: evalCondition(r.conditions, ctx) }))
    .sort((a,b) => a.r.priority - b.r.priority);

  // Merge required/optional from all matching
  const merged = applicable.filter(x => x.match).reduce((acc, x) => {
    (x.r.required||[]).forEach(c => acc.required.add(c));
    (x.r.optional||[]).forEach(c => acc.optional.add(c));
    return acc;
  }, { required: new Set(), optional: new Set() });
  // remove from optional if already required
  merged.optional = new Set([...merged.optional].filter(c => !merged.required.has(c)));

  const overrideBlocked = applicable.filter(x => x.match && !x.r.allowOverride).map(x => x.r.id);

  return (
    <Modal open={open} onClose={onClose} title="Simulazione checklist" size="lg"
      footer={<Btn variant="ghost" size="sm" onClick={onClose}>Chiudi</Btn>}>
      <div className="col" style={{gap:14}}>
        <div style={{fontSize:11.5, color:'var(--text-2)'}}>
          Inserisci i parametri di un'entità per vedere quali regole la matchano e quali documenti saranno richiesti.
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Parametri entità</div>
          <div className="grid grid-3">
            <div className="field"><label>Tipo entità</label>
              <select value={entityType} onChange={e=>setEntityType(e.target.value)}>
                {entityTypes.map(t => <option key={t} value={t}>{t}</option>)}
              </select>
            </div>
            {entityType === 'rda' && <>
              <div className="field"><label>Importo (€)</label>
                <input type="number" value={fields.amount} onChange={e=>setField('amount', Number(e.target.value))}/>
              </div>
              <div className="field"><label>Categoria</label>
                <input value={fields.category} onChange={e=>setField('category', e.target.value)}/>
              </div>
              <div className="field"><label>Area ATEX</label>
                <select value={fields.atex?'yes':'no'} onChange={e=>setField('atex', e.target.value==='yes')}>
                  <option value="no">No</option><option value="yes">Sì</option>
                </select>
              </div>
            </>}
            {entityType === 'vendor' && <>
              <div className="field"><label>Paese</label>
                <input value={fields.country} onChange={e=>setField('country', e.target.value)} placeholder="IT"/>
              </div>
              <div className="field"><label>Categoria</label>
                <input value={fields.category} onChange={e=>setField('category', e.target.value)}/>
              </div>
            </>}
            {entityType === 'progetto' && (
              <div className="field"><label>Budget (€)</label>
                <input type="number" value={fields.budget} onChange={e=>setField('budget', Number(e.target.value))}/>
              </div>
            )}
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Regole valutate ({applicable.length})</div>
          <table className="tbl dense">
            <thead><tr><th style={{width:110}}>ID</th><th>Nome</th><th style={{width:70}}>Prio</th><th>Condizione</th><th style={{width:70, textAlign:'center'}}>Match</th></tr></thead>
            <tbody>{applicable.map(x => (
              <tr key={x.r.id} style={{opacity: x.match ? 1 : 0.45}}>
                <td className="mono" style={{fontSize:10.5}}>{x.r.id}</td>
                <td>{x.r.name}</td>
                <td className="mono num">{x.r.priority}</td>
                <td style={{fontSize:11, color:'var(--text-2)'}}>{formatCondition(x.r.conditions)}</td>
                <td style={{textAlign:'center'}}>{x.match ? <Chip kind="ok" dot>sì</Chip> : <Chip>no</Chip>}</td>
              </tr>
            ))}</tbody>
          </table>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Checklist risultante</div>
          <div className="grid grid-2" style={{gap:12}}>
            <div>
              <div style={{fontSize:10.5, color:'var(--text-3)', marginBottom:4}}>Obbligatori ({merged.required.size})</div>
              <div style={{display:'flex', flexWrap:'wrap', gap:4, padding:8, border:'1px solid var(--line)', borderRadius:4, minHeight:48}}>
                {merged.required.size === 0 && <span style={{color:'var(--text-3)', fontSize:11}}>Nessun documento richiesto</span>}
                {[...merged.required].map(c => <Chip key={c} kind="err">{c}</Chip>)}
              </div>
            </div>
            <div>
              <div style={{fontSize:10.5, color:'var(--text-3)', marginBottom:4}}>Opzionali ({merged.optional.size})</div>
              <div style={{display:'flex', flexWrap:'wrap', gap:4, padding:8, border:'1px dashed var(--line)', borderRadius:4, minHeight:48}}>
                {merged.optional.size === 0 && <span style={{color:'var(--text-3)', fontSize:11}}>Nessuno</span>}
                {[...merged.optional].map(c => <Chip key={c}>{c}</Chip>)}
              </div>
            </div>
          </div>
        </div>

        {overrideBlocked.length > 0 && (
          <div style={{fontSize:10.5, color:'var(--warn)', padding:'8px 10px', background:'color-mix(in oklch, var(--warn) 8%, var(--bg-1))', borderRadius:4}}>
            <Icon name="lock" size={10}/> Override bloccato da: <strong>{overrideBlocked.join(', ')}</strong>. Documenti obbligatori non saltabili.
          </div>
        )}
      </div>
    </Modal>
  );
}

// helper: human-readable condition
function formatCondition(c) {
  if (!c) return '—';
  if (c.op === 'always') return 'sempre';
  if (c.op === 'never')  return 'mai';
  if (c.op === 'and') return (c.args||[]).map(formatCondition).join(' AND ');
  if (c.op === 'or')  return (c.args||[]).map(formatCondition).join(' OR ');
  if (c.op === 'not') return 'NOT ' + formatCondition(c.arg || (c.args && c.args[0]));
  const opSym = { eq:'=', neq:'≠', gt:'>', gte:'≥', lt:'<', lte:'≤', in:'in', nin:'∉', exists:'exists' }[c.op] || c.op;
  const val = Array.isArray(c.value) ? `[${c.value.slice(0,2).join(', ')}${c.value.length>2?'…':''}]` : (typeof c.value === 'number' ? c.value.toLocaleString('it-IT') : String(c.value));
  return `${c.field} ${opSym} ${val}`;
}

function renderConditionTree(c, depth = 0) {
  if (!c) return <span>—</span>;
  if (c.op === 'and' || c.op === 'or') {
    return (
      <div style={{ paddingLeft: depth ? 14 : 0 }}>
        <span style={{ color: 'var(--accent)', fontWeight: 600 }}>{c.op.toUpperCase()}</span>
        <div style={{ borderLeft: '2px solid var(--line)', paddingLeft: 10, marginTop: 4 }}>
          {(c.args||[]).map((a, i) => <div key={i} style={{ marginTop: i===0?0:4 }}>{renderConditionTree(a, depth+1)}</div>)}
        </div>
      </div>
    );
  }
  return <span>{formatCondition(c)}</span>;
}

// FASE 3a.15: detail modal editable per DocType (PATCH /api/config/doc-types/[id]).
function DocTypeDetailModal({ sel, onClose, categories }) {
  const { addDocType, pushToast, user, seedCustom } = useStore();
  const initial = React.useMemo(() => {
    if (!sel) return null;
    return {
      code: sel.code || '',
      name: sel.name || '',
      description: sel.description || null,
      category: sel.category || categories[0] || 'tecnico',
      mime: Array.isArray(sel.mime) ? sel.mime : [],
      maxSizeMB: Number(sel.maxSizeMB || 25),
      versioning: !!sel.versioning,
      requiresSignature: !!sel.requiresSignature,
      digitalSignature: !!sel.digitalSignature,
      signerRoles: Array.isArray(sel.signerRoles) ? sel.signerRoles : [],
      hasExpiry: !!sel.hasExpiry,
      validityMonths: sel.validityMonths || null,
      aiClassify: !!sel.aiClassify,
      aiExtract: !!sel.aiExtract,
      aiClassifyPromptId: sel.aiClassifyPromptId || '',
      aiMetadataSchema: sel.aiMetadataSchema || null,
      templateId: sel.templateId || '',
      active: sel.active !== false,
    };
  }, [sel, categories]);

  const { form, set, isDirty, saving, serverError, save } = useEditableEntity(initial || {}, {
    url: () => sel ? `/api/config/doc-types/${sel.id}` : null,
    actorId: user?.id,
    buildBody: (f) => ({
      code: f.code,
      name: f.name?.trim(),
      description: f.description?.trim() || null,
      category: f.category,
      mime: f.mime,
      maxSizeMB: Number(f.maxSizeMB),
      versioning: !!f.versioning,
      requiresSignature: !!f.requiresSignature,
      digitalSignature: !!f.digitalSignature,
      signerRoles: f.signerRoles,
      hasExpiry: !!f.hasExpiry,
      validityMonths: f.hasExpiry ? Number(f.validityMonths || 12) : null,
      aiClassify: !!f.aiClassify,
      aiExtract: !!f.aiExtract,
      aiClassifyPromptId: f.aiClassifyPromptId || null,
      aiMetadataSchema: f.aiMetadataSchema || null,
      templateId: f.templateId || null,
      active: !!f.active,
    }),
    onSaved: (json) => {
      addDocType(json.data);
      pushToast({ title: 'Tipo documento aggiornato', desc: `${json.data.code} · ${json.data.name} salvato. ${json.changed ? 'Audit registrato.' : 'Nessuna modifica server.'}`, tone: 'ok' });
      onClose();
    },
  });

  if (!sel) return <Modal open={false} onClose={onClose} title="" />;

  const codeValid = !form.code || /^[A-Z][A-Z0-9_]{1,31}$/.test(form.code);
  const valid = form.name?.trim() && codeValid;

  return (
    <Modal open={!!sel} onClose={onClose} title={`${sel.code} · ${sel.name}`} size="lg"
      footer={<>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
        <Btn variant="primary" size="sm" disabled={!valid || saving || !isDirty} onClick={save}>
          {saving ? 'Salvataggio…' : isDirty ? 'Salva modifiche' : 'Nessuna modifica'}
        </Btn>
      </>}>
      <div className="col" style={{ gap: 14 }}>
        {serverError && (
          <div style={{ padding: '10px 12px', border: '1px solid var(--err, #c0392b)', borderRadius: 6, background: 'rgba(192,57,43,0.08)', color: 'var(--err, #c0392b)', fontSize: 12 }}>
            <strong>Errore salvataggio:</strong> {serverError}
          </div>
        )}

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Identificativo</div>
          <div className="grid grid-3">
            <div className="field"><label>Codice</label>
              <input value={form.code || ''} onChange={e=>set('code', e.target.value.toUpperCase().replace(/[^A-Z0-9_]/g,''))} style={{fontFamily:'var(--font-mono)'}}/>
            </div>
            <div className="field" style={{gridColumn:'span 2'}}><label>Nome visualizzato</label>
              <input value={form.name || ''} onChange={e=>set('name', e.target.value)}/>
            </div>
          </div>
          {form.code && !codeValid && <div style={{fontSize:10.5, color:'var(--err)', marginTop:4}}>Code: maiuscola + A-Z/0-9/_ (max 32)</div>}
          <div className="field" style={{marginTop:8}}><label>Descrizione</label>
            <textarea rows={2} value={form.description || ''} onChange={e=>set('description', e.target.value || null)}/>
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Classificazione</div>
          <div className="grid grid-2">
            <div className="field"><label>Categoria</label>
              <select value={form.category} onChange={e=>set('category', e.target.value)}>
                {categories.map(c => <option key={c} value={c}>{c}</option>)}
              </select>
            </div>
            <div className="field"><label>Dim. max (MB)</label>
              <input type="number" min={1} max={500} value={form.maxSizeMB ?? ''} onChange={e=>set('maxSizeMB', Number(e.target.value))}/>
            </div>
          </div>
          <div style={{marginTop:8}}>
            <label style={{fontSize:10.5, color:'var(--text-3)'}}>MIME ammessi</label>
            <div style={{marginTop:4}}>
              <DocTypeMimePicker value={form.mime} onChange={(v)=>set('mime', v)}/>
            </div>
          </div>
          <div className="field" style={{marginTop:8}}><label>Template documentale associato</label>
            <window.Autocomplete value={form.templateId || ''} onChange={v=>set('templateId', v)}
              options={(seedCustom.TEMPLATES || []).map(tpl => ({ value: tpl.id, label: tpl.name, sublabel: tpl.code }))}
              placeholder="Nessun template · cerca…" testId="cust-doctype-template-ac" />
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Versioning & firma</div>
          <div className="col" style={{gap:6}}>
            <label className="row" style={{gap:6, fontSize:11.5}}>
              <input type="checkbox" checked={!!form.versioning} onChange={e=>set('versioning', e.target.checked)}/>
              Versioning abilitato (tiene storico upload)
            </label>
            <label className="row" style={{gap:6, fontSize:11.5}}>
              <input type="checkbox" checked={!!form.requiresSignature} onChange={e=>set('requiresSignature', e.target.checked)}/>
              Richiede firma
            </label>
            {form.requiresSignature && (
              <div style={{marginLeft:20, padding:8, border:'1px solid var(--line)', borderRadius:4}}>
                <label className="row" style={{gap:6, fontSize:11.5}}>
                  <input type="checkbox" checked={!!form.digitalSignature} onChange={e=>set('digitalSignature', e.target.checked)}/>
                  Firma digitale qualificata (eIDAS/CAdES). Se off: firma grafometrica o OTP.
                </label>
                <div style={{marginTop:8}}>
                  <div style={{fontSize:10.5, color:'var(--text-3)', marginBottom:4}}>Ruoli firmatari ammessi</div>
                  <DocTypeSignerRolesPicker value={form.signerRoles} seedCustom={seedCustom} onChange={(v)=>set('signerRoles', v)}/>
                </div>
              </div>
            )}
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Scadenza</div>
          <label className="row" style={{gap:6, fontSize:11.5}}>
            <input type="checkbox" checked={!!form.hasExpiry} onChange={e=>set('hasExpiry', e.target.checked)}/>
            Il documento ha una scadenza (certificazioni, polizze, DURC…)
          </label>
          {form.hasExpiry && (
            <div className="grid grid-2" style={{marginTop:8, marginLeft:20}}>
              <div className="field"><label>Validità (mesi)</label>
                <input type="number" min={1} max={120} value={form.validityMonths || 12} onChange={e=>set('validityMonths', Number(e.target.value))}/>
              </div>
            </div>
          )}
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>AI</div>
          <div className="col" style={{gap:6}}>
            <label className="row" style={{gap:6, fontSize:11.5}}>
              <input type="checkbox" checked={!!form.aiClassify} onChange={e=>set('aiClassify', e.target.checked)}/>
              AI classification on upload (riconosce il tipo anche se l'utente sbaglia categoria)
            </label>
            {form.aiClassify && (
              <div className="field" style={{marginLeft:20}}><label>Agente AI per la classificazione</label>
                <window.Autocomplete value={form.aiClassifyPromptId || ''} onChange={v=>set('aiClassifyPromptId', v)}
                  options={(seedCustom.AI_AGENTS || []).map(a => ({ value: a.id, label: a.name, sublabel: a.code }))}
                  placeholder="Agente di default · cerca…" testId="cust-doctype-aiagent-ac" />
              </div>
            )}
            <label className="row" style={{gap:6, fontSize:11.5}}>
              <input type="checkbox" checked={!!form.aiExtract} onChange={e=>set('aiExtract', e.target.checked)}/>
              AI metadata extraction (estrae campi strutturati: importi, date, P.IVA, ecc.)
            </label>
            {form.aiExtract && (
              <div style={{marginLeft:20}}>
                <div style={{fontSize:10.5, color:'var(--text-3)', marginBottom:4}}>Schema metadati da estrarre (campo → tipo)</div>
                <DocTypeMetadataSchemaEditor value={form.aiMetadataSchema} onChange={(v)=>set('aiMetadataSchema', v)}/>
              </div>
            )}
          </div>
        </div>

        <label className="row" style={{gap:6, fontSize:11.5}}>
          <input type="checkbox" checked={!!form.active} onChange={e=>set('active', e.target.checked)}/>
          Tipo documento attivo
        </label>

        {!isDirty && (
          <div style={{fontSize:10.5, color:'var(--text-3)', padding:'6px 10px', background:'var(--bg-2)', borderRadius:4}}>
            Modifica un campo per abilitare "Salva modifiche".
          </div>
        )}
      </div>
    </Modal>
  );
}

// ============================================================
// Workflow structured editor — componenti condivisi (sessione 97, Fase C).
// Sostituiscono l'editing JSON grezzo di steps + startConditions: la modale
// crea/modifica usa dropdown/checkbox alimentati dai valori reali di config.
// ============================================================
const WF_COND_FIELDS = [
  { v: 'entity.amount', label: 'Importo RdA (€)', kind: 'number' },
  { v: 'entity.budget', label: 'Budget progetto (€)', kind: 'number' },
  { v: 'entity.category', label: 'Categoria', kind: 'category' },
  { v: 'entity.capexClass', label: 'Classe CAPEX', kind: 'capexClass' },
];
const WF_NUM_OPS = [
  { v: 'gte', label: '≥ maggiore o uguale' },
  { v: 'gt', label: '> maggiore' },
  { v: 'lte', label: '≤ minore o uguale' },
  { v: 'lt', label: '< minore' },
  { v: 'eq', label: '= uguale' },
  { v: 'neq', label: '≠ diverso' },
];
const WF_STR_OPS = [
  { v: 'eq', label: '= uguale a' },
  { v: 'neq', label: '≠ diverso da' },
];
const WF_STEP_ACTIONS = [
  { v: 'notify', label: 'Notifica' },
  { v: 'escalate', label: 'Escalation' },
];

function wfFieldKind(field) {
  return (WF_COND_FIELDS.find((f) => f.v === field) || WF_COND_FIELDS[0]).kind;
}
function wfGenStepId() {
  return 's-' + Date.now().toString(36) + '-' + Math.floor(Math.random() * 9000 + 1000);
}
function wfCondComplete(c) {
  return !!c && !!c.field && !!c.op && c.value !== '' && c.value != null;
}
function wfStartCondValid(sc) {
  const all = sc && Array.isArray(sc.all) ? sc.all : [];
  return all.every(wfCondComplete);
}
function wfStepsValid(steps) {
  const list = Array.isArray(steps) ? steps : [];
  if (list.length === 0) return false;
  return list.every(
    (s) =>
      s &&
      (s.name || '').trim() &&
      (s.approver?.kind === 'matrix' || (s.approver?.kind === 'role' && s.approver?.roleId)) &&
      (!s.enterCondition || wfCondComplete(s.enterCondition)),
  );
}

// Una condizione { field, op, value } su un campo del contesto entità.
function WorkflowConditionRow({ cond, seedCustom, onChange, onRemove }) {
  const kind = wfFieldKind(cond.field);
  const ops = kind === 'number' ? WF_NUM_OPS : WF_STR_OPS;
  const categories = (seedCustom && seedCustom.CATEGORIES_EXT) || [];
  const capexClasses = (seedCustom && seedCustom.CAPEX_CLASSES) || [];
  const changeField = (field) => {
    const newOps = wfFieldKind(field) === 'number' ? WF_NUM_OPS : WF_STR_OPS;
    const op = newOps.some((o) => o.v === cond.op) ? cond.op : newOps[0].v;
    onChange({ field, op, value: '' });
  };
  return (
    <div className="row" style={{ gap: 6, alignItems: 'center' }}>
      <select value={cond.field} onChange={(e) => changeField(e.target.value)} style={{ flex: '1 1 150px' }}>
        {WF_COND_FIELDS.map((f) => <option key={f.v} value={f.v}>{f.label}</option>)}
      </select>
      <select value={cond.op} onChange={(e) => onChange({ ...cond, op: e.target.value })} style={{ flex: '0 0 150px' }}>
        {ops.map((o) => <option key={o.v} value={o.v}>{o.label}</option>)}
      </select>
      {kind === 'number' && (
        <input type="number" value={cond.value ?? ''} placeholder="valore" style={{ flex: '1 1 110px' }}
          onChange={(e) => onChange({ ...cond, value: e.target.value === '' ? '' : Number(e.target.value) })}/>
      )}
      {kind === 'category' && (
        <select value={cond.value ?? ''} onChange={(e) => onChange({ ...cond, value: e.target.value })} style={{ flex: '1 1 150px' }}>
          <option value="">— categoria —</option>
          {categories.map((c) => <option key={c.id} value={c.name}>{c.name}</option>)}
        </select>
      )}
      {kind === 'capexClass' && (
        <select value={cond.value ?? ''} onChange={(e) => onChange({ ...cond, value: e.target.value })} style={{ flex: '1 1 150px' }}>
          <option value="">— classe —</option>
          {capexClasses.map((c) => <option key={c.id} value={c.code}>{c.name} ({c.code})</option>)}
        </select>
      )}
      {onRemove && <Btn variant="ghost" size="sm" onClick={onRemove}><Icon name="x" size={10}/></Btn>}
    </div>
  );
}

// startConditions del workflow: lista di condizioni in AND.
function WorkflowStartConditionsEditor({ value, seedCustom, onChange }) {
  const all = value && Array.isArray(value.all) ? value.all : [];
  const setAll = (next) => onChange({ all: next });
  return (
    <div className="col" style={{ gap: 8 }}>
      <div className="row" style={{ gap: 8 }}>
        <div className="eyebrow">Condizioni di avvio</div>
        <span className="spacer"/>
        <Btn variant="ghost" size="sm" onClick={() => setAll([...all, { field: 'entity.amount', op: 'gte', value: '' }])}>
          <Icon name="plus" size={10}/> Aggiungi condizione
        </Btn>
      </div>
      {all.length === 0 ? (
        <div style={{ fontSize: 11, color: 'var(--text-3)', padding: '8px 10px', background: 'var(--bg-2)', borderRadius: 4 }}>
          Nessuna condizione → il workflow è sempre applicabile all'entità.
        </div>
      ) : (
        <div className="col" style={{ gap: 6 }}>
          <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
            Il workflow si avvia solo se <strong>tutte</strong> le condizioni sono soddisfatte:
          </div>
          {all.map((c, i) => (
            <WorkflowConditionRow key={i} cond={c} seedCustom={seedCustom}
              onChange={(nc) => setAll(all.map((x, j) => (j === i ? nc : x)))}
              onRemove={() => setAll(all.filter((_, j) => j !== i))}/>
          ))}
        </div>
      )}
    </div>
  );
}

// Card editabile di un singolo step del workflow.
function WorkflowStepCard({ step, index, total, seedCustom, onChange, onRemove, onMove }) {
  const roles = ((seedCustom && seedCustom.ROLES) || []).filter((r) => r.active !== false);
  const docTypes = (seedCustom && seedCustom.DOC_TYPES) || [];
  const docCodes = React.useMemo(() => {
    const seen = new Set();
    const out = [];
    for (const d of docTypes) {
      if (d && d.code && !seen.has(d.code)) { seen.add(d.code); out.push(d.code); }
    }
    return out.sort();
  }, [docTypes]);
  const gates = Array.isArray(step.gates) ? step.gates : [];
  const docGates = gates.filter((g) => g.startsWith('doc_signed:'));
  const patch = (p) => onChange({ ...step, ...p });
  const setGates = (next) => patch({ gates: next.length ? next : undefined });
  const toggleArr = (arr, v) => (arr.includes(v) ? arr.filter((x) => x !== v) : [...arr, v]);
  const toggleAction = (key, v) => {
    const next = toggleArr(step[key] || [], v);
    patch({ [key]: next.length ? next : undefined });
  };

  return (
    <div style={{ border: '1px solid var(--line)', borderRadius: 8, padding: 12, background: 'var(--bg-1)' }}>
      <div className="row" style={{ gap: 8, marginBottom: 10 }}>
        <span className="mono" style={{ color: 'var(--text-3)', fontSize: 11 }}>#{index + 1}</span>
        <input value={step.name || ''} onChange={(e) => patch({ name: e.target.value })}
          placeholder="Nome dello step" style={{ flex: 1, fontWeight: 500 }}/>
        <Btn variant="ghost" size="sm" onClick={() => onMove(-1)} disabled={index === 0}>↑</Btn>
        <Btn variant="ghost" size="sm" onClick={() => onMove(1)} disabled={index === total - 1}>↓</Btn>
        <Btn variant="ghost" size="sm" onClick={onRemove}><Icon name="x" size={10}/></Btn>
      </div>

      <div className="grid grid-3" style={{ gap: 8 }}>
        <div className="field"><label>Approvatore</label>
          <select value={step.approver?.kind || 'role'}
            onChange={(e) => patch({ approver: e.target.value === 'matrix' ? { kind: 'matrix' } : { kind: 'role', roleId: step.approver?.roleId || '' } })}>
            <option value="role">Ruolo specifico</option>
            <option value="matrix">Matrice di approvazione</option>
          </select>
        </div>
        {step.approver?.kind !== 'matrix' ? (
          <div className="field"><label>Ruolo</label>
            <window.Autocomplete value={step.approver?.roleId || ''} onChange={(v) => patch({ approver: { kind: 'role', roleId: v } })}
              options={roles.map((r) => ({ value: r.id, label: r.name, sublabel: r.code }))}
              placeholder="Cerca ruolo… (spazio per lista)" testId="cust-step-approver-role-ac" />
          </div>
        ) : (
          <div className="field"><label>Ruolo</label>
            <div style={{ fontSize: 10.5, color: 'var(--text-3)', padding: '6px 0' }}>Risolto dalla matrice di approvazione.</div>
          </div>
        )}
        <div className="field"><label>Gruppo parallelo</label>
          <input value={step.parallelGroup || ''} placeholder="(opzionale)"
            onChange={(e) => patch({ parallelGroup: e.target.value.trim() || undefined })}/>
        </div>
        <div className="field"><label>SLA (giorni)</label>
          <input type="number" min="0" value={step.slaDays ?? ''}
            onChange={(e) => patch({ slaDays: e.target.value === '' ? undefined : Number(e.target.value) })}/>
        </div>
        <div className="field"><label>Soglia warning SLA (%)</label>
          <input type="number" min="0" max="100" value={step.warnPct ?? ''}
            onChange={(e) => patch({ warnPct: e.target.value === '' ? undefined : Number(e.target.value) })}/>
        </div>
      </div>

      <div style={{ marginTop: 8 }}>
        <div className="eyebrow" style={{ marginBottom: 4 }}>Gate documentali — bloccano l'approvazione</div>
        <label className="row" style={{ gap: 6, fontSize: 11.5 }}>
          <input type="checkbox" checked={gates.includes('documents_signed')}
            onChange={() => setGates(toggleArr(gates, 'documents_signed'))}/>
          Tutti i documenti richiesti devono essere firmati
        </label>
        <div className="row" style={{ gap: 6, marginTop: 4, flexWrap: 'wrap', alignItems: 'center' }}>
          {docGates.map((g) => (
            <span key={g} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10.5, padding: '2px 6px', border: '1px solid var(--line)', borderRadius: 4, background: 'var(--bg-2)' }}>
              🖋 {g.replace('doc_signed:', '')}
              <span onClick={() => setGates(gates.filter((x) => x !== g))} style={{ cursor: 'pointer', color: 'var(--text-3)' }}>×</span>
            </span>
          ))}
          <select value="" style={{ flex: '0 0 230px' }}
            onChange={(e) => { if (e.target.value) setGates([...gates, 'doc_signed:' + e.target.value]); }}>
            <option value="">+ documento specifico da firmare…</option>
            {docCodes.filter((c) => !docGates.includes('doc_signed:' + c)).map((c) => <option key={c} value={c}>{c}</option>)}
          </select>
        </div>
      </div>

      <div style={{ marginTop: 8 }}>
        <label className="row" style={{ gap: 6, fontSize: 11.5 }}>
          <input type="checkbox" checked={!!step.enterCondition}
            onChange={(e) => patch({ enterCondition: e.target.checked ? { field: 'entity.amount', op: 'gte', value: '' } : undefined })}/>
          Condizione di ingresso — lo step si attiva solo se soddisfatta (altrimenti è saltato)
        </label>
        {step.enterCondition && (
          <div style={{ marginTop: 4 }}>
            <WorkflowConditionRow cond={step.enterCondition} seedCustom={seedCustom}
              onChange={(nc) => patch({ enterCondition: nc })}/>
          </div>
        )}
      </div>

      <div className="grid grid-2" style={{ gap: 8, marginTop: 8 }}>
        <div>
          <div className="eyebrow" style={{ marginBottom: 4 }}>Al completamento dello step</div>
          {WF_STEP_ACTIONS.map((a) => (
            <label key={a.v} className="row" style={{ gap: 6, fontSize: 11.5 }}>
              <input type="checkbox" checked={(step.onComplete || []).includes(a.v)}
                onChange={() => toggleAction('onComplete', a.v)}/>
              {a.label}
            </label>
          ))}
        </div>
        <div>
          <div className="eyebrow" style={{ marginBottom: 4 }}>Allo scadere dell'SLA</div>
          {WF_STEP_ACTIONS.map((a) => (
            <label key={a.v} className="row" style={{ gap: 6, fontSize: 11.5 }}>
              <input type="checkbox" checked={(step.onTimeout || []).includes(a.v)}
                onChange={() => toggleAction('onTimeout', a.v)}/>
              {a.label}
            </label>
          ))}
        </div>
      </div>
    </div>
  );
}

// Editor della lista step: add / reorder / remove, renumerazione automatica.
function WorkflowStepsEditor({ steps, seedCustom, onChange }) {
  const list = Array.isArray(steps) ? steps : [];
  const renumber = (arr) => arr.map((s, i) => ({ ...s, order: i + 1 }));
  const update = (i, ns) => onChange(renumber(list.map((s, j) => (j === i ? ns : s))));
  const remove = (i) => onChange(renumber(list.filter((_, j) => j !== i)));
  const move = (i, d) => {
    const j = i + d;
    if (j < 0 || j >= list.length) return;
    const arr = [...list];
    [arr[i], arr[j]] = [arr[j], arr[i]];
    onChange(renumber(arr));
  };
  const add = () =>
    onChange(renumber([...list, { id: wfGenStepId(), order: list.length + 1, name: '', approver: { kind: 'role', roleId: '' } }]));
  return (
    <div className="col" style={{ gap: 8 }}>
      <div className="row" style={{ gap: 8 }}>
        <div className="eyebrow">Step del workflow ({list.length})</div>
        <span className="spacer"/>
        <Btn variant="ghost" size="sm" onClick={add}><Icon name="plus" size={10}/> Aggiungi step</Btn>
      </div>
      {list.length === 0 && (
        <div style={{ fontSize: 11, color: 'var(--text-3)', padding: '8px 10px', background: 'var(--bg-2)', borderRadius: 4 }}>
          Nessuno step — un workflow deve avere almeno uno step per essere avviabile.
        </div>
      )}
      {list.map((s, i) => (
        <WorkflowStepCard key={s.id || i} step={s} index={i} total={list.length} seedCustom={seedCustom}
          onChange={(ns) => update(i, ns)} onRemove={() => remove(i)} onMove={(d) => move(i, d)}/>
      ))}
    </div>
  );
}

// -------------- WORKFLOWS --------------
function CustWorkflows() {
  const { seedCustom, extras, addWorkflow, user, pushToast } = useStore();
  // FASE 9 (s112) — entity_type registrati nel resolver, per il badge orphan.
  const [registered, setRegistered] = React.useState([]);
  React.useEffect(() => {
    let aborted = false;
    fetch('/api/registered-entity-types', { headers: { 'X-Actor-Persona-Id': user?.id || '' } })
      .then((r) => r.json().catch(() => ({})))
      .then((j) => { if (!aborted && Array.isArray(j?.data?.entityTypes)) setRegistered(j.data.entityTypes); })
      .catch(() => { /* badge non mostrato se fetch fallisce */ });
    return () => { aborted = true; };
  }, [user?.id]);
  const isOrphan = React.useCallback(
    (w) => registered.length > 0 && !registered.includes(w.entityType),
    [registered],
  );
  const importInputRef = React.useRef(null);

  // FASE 9 — export workflow come JSON (backup/template).
  const exportWorkflow = (w, e) => {
    if (e) e.stopPropagation();
    const payload = {
      code: w.code, name: w.name, entityType: w.entityType,
      startConditions: w.startConditions || { all: [] }, steps: w.steps || [],
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = `${w.code || 'workflow'}.json`;
    document.body.appendChild(a); a.click(); document.body.removeChild(a);
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };

  // FASE 9 — import workflow da JSON (Zod validate server-side via POST).
  const importWorkflow = async (file) => {
    if (!file) return;
    let parsed;
    try { parsed = JSON.parse(await file.text()); }
    catch { pushToast({ title: 'Import fallito', desc: 'JSON non valido', tone: 'err' }); return; }
    try {
      const res = await fetch('/api/config/approval-workflows', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({
          code: parsed.code, name: parsed.name, entityType: parsed.entityType,
          startConditions: parsed.startConditions || { all: [] }, steps: parsed.steps || [], status: 'draft',
        }),
      });
      const json = await res.json().catch(() => ({}));
      if (!res.ok) {
        const msg = json?.error === 'validation_error'
          ? `Validazione: ${(json.issues || []).map((i) => `${(i.path || []).join('.')} ${i.message}`).join(' · ') || 'campi non validi'}`
          : (json?.detail || json?.error || `HTTP ${res.status}`);
        pushToast({ title: 'Import fallito', desc: msg, tone: 'err' });
        return;
      }
      if (json?.data && addWorkflow) addWorkflow(json.data);
      pushToast({ title: `Workflow ${json?.data?.code || ''} importato`, desc: 'Creato come draft. Audit registrato.', tone: 'ok' });
    } catch (err) {
      pushToast({ title: 'Import fallito', desc: String(err?.message || err), tone: 'err' });
    }
  };

  // FASE 3a.16: dedup extras+seed.
  const wfs = React.useMemo(() => {
    const seedList = seedCustom.APPROVAL_WORKFLOWS || [];
    const extList = extras?.workflowsExt || [];
    const seenIds = new Set();
    const out = [];
    for (const w of [...extList, ...seedList]) {
      if (!w?.id || seenIds.has(w.id)) continue;
      seenIds.add(w.id);
      out.push(w);
    }
    return out;
  }, [seedCustom.APPROVAL_WORKFLOWS, extras?.workflowsExt]);
  const [sel, setSel] = React.useState(null);
  const [showNew, setShowNew] = React.useState(false);
  // Gating scrittura config (FASE — workflow): chi non ha config.update vede i
  // bottoni disabilitati con spiegazione, invece di aprire la modale e becca 403.
  const canWriteConfig = window.can('config.update', user, seedCustom);

  return (
    <>
      <div className="row" style={{ gap: 8, marginBottom: 12 }}>
        <span className="spacer"/>
        <input
          ref={importInputRef}
          type="file"
          accept="application/json,.json"
          style={{ display: 'none' }}
          onChange={(e) => { const f = e.target.files?.[0]; importWorkflow(f); e.target.value = ''; }}
        />
        <Btn
          variant="ghost"
          size="sm"
          disabled={!canWriteConfig}
          title={canWriteConfig ? undefined : window.whyDisabled('config.update')}
          onClick={() => { if (canWriteConfig) importInputRef.current?.click(); }}
          data-action="import-workflow"
        >
          <Icon name="import" size={11}/> Importa JSON
        </Btn>
        <Btn
          variant={canWriteConfig ? 'primary' : 'ghost'}
          size="sm"
          disabled={!canWriteConfig}
          title={canWriteConfig ? undefined : window.whyDisabled('config.update')}
          onClick={() => { if (canWriteConfig) setShowNew(true); }}
        ><Icon name="plus" size={11}/> Nuovo workflow</Btn>
      </div>

      <div className="grid grid-2" style={{ gap: 10 }}>
        {wfs.map(w => (
          <div key={w.id} className="card flush clickable" data-wf-id={w.id} data-wf-orphan={isOrphan(w) ? 'true' : 'false'} onClick={()=>setSel(w)} style={{ border: '1px solid ' + (isOrphan(w) ? 'var(--warn)' : 'var(--line)'), borderRadius: 10, padding: 14, background: 'var(--bg-1)' }}>
            <div className="row" style={{ gap: 8, marginBottom: 8 }}>
              <Icon name="workflow" size={14}/>
              <div style={{ fontWeight: 600, fontSize: 13 }}>{w.name}</div>
              <span className="spacer"/>
              {isOrphan(w) && (
                <Chip kind="warn" title={`Template orfano: l'entity_type «${w.entityType}» non ha ancora una tabella di backing. Nessuna entità può avviare questo workflow finché la tabella non sarà introdotta. Resta come template di riferimento.`}>
                  ⚠ orphan
                </Chip>
              )}
              <Chip>v{w.version}</Chip>
              {w.draftVersion && <Chip kind="warn">draft v{w.draftVersion}</Chip>}
              <button className="btn ghost icon" title="Esporta JSON" data-action="export-workflow" onClick={(e)=>exportWorkflow(w, e)}>
                <Icon name="download" size={12}/>
              </button>
            </div>
            <div className="row" style={{ gap: 10, fontSize: 11.5, color: 'var(--text-2)', marginBottom: 10 }}>
              <span><Icon name="package" size={10}/> {w.entityType}</span>
              <span className="mono">{(w.steps||[]).length} step</span>
              <span>·</span>
              <span>{w.activeInstances} istanze attive</span>
            </div>

            {/* steps strip */}
            <div style={{ display: 'grid', gridAutoFlow: 'column', gridAutoColumns: 'minmax(0, 1fr)', gap: 4, marginTop: 6 }}>
              {(w.steps||[]).map((s, i) => (
                <div key={s.id} style={{
                  padding: '6px 8px', border: '1px solid var(--line)', borderRadius: 4,
                  background: s.approver?.kind === 'matrix' ? 'color-mix(in oklch, var(--accent) 10%, var(--bg-1))' : 'var(--bg-2)',
                  fontSize: 10.5, minWidth: 0,
                }}>
                  <div className="mono" style={{color:'var(--text-3)'}}>#{s.order}</div>
                  <div style={{ fontWeight: 500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{s.name}</div>
                  <div style={{ color: 'var(--text-3)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
                    {s.approver?.kind === 'role' ? s.approver.roleId : s.approver?.kind}
                  </div>
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>

      <ApprovalWorkflowDetailModal selWf={sel} onClose={()=>setSel(null)}/>
      {/* P2 sessione 34 — modal create live */}
      <NewWorkflowModalLive
        open={showNew}
        onClose={() => setShowNew(false)}
        onCreated={(row) => addWorkflow && addWorkflow(row)}
      />
    </>
  );
}

// FASE 3a.16 + sessione 97 Fase C: detail modal editable Approval Workflow
// (PATCH /api/config/approval-workflows/[id]). Editor strutturato — step builder
// + condition builder alimentati dai valori reali di config, niente JSON grezzo.
function ApprovalWorkflowDetailModal({ selWf, onClose }) {
  const { addWorkflow, pushToast, user, seedCustom } = useStore();
  const serverWf = useFetchedDetail(selWf, (s) => `/api/config/approval-workflows/${s.id}`);
  const baseWf = serverWf || selWf;

  const initial = React.useMemo(() => {
    if (!baseWf) return null;
    const sc = baseWf.startConditions;
    return {
      code: baseWf.code || '',
      name: baseWf.name || '',
      entityType: baseWf.entityType || 'rda',
      status: baseWf.status || 'draft',
      startConditions: sc && Array.isArray(sc.all) ? { all: sc.all } : { all: [] },
      steps: Array.isArray(baseWf.steps) ? baseWf.steps : [],
      active: baseWf.active !== false,
    };
  }, [baseWf]);

  const buildBody = React.useCallback((f) => ({
    code: f.code,
    name: (f.name || '').trim(),
    entityType: f.entityType,
    status: f.status,
    startConditions: f.startConditions && Array.isArray(f.startConditions.all) ? f.startConditions : { all: [] },
    steps: Array.isArray(f.steps) ? f.steps : [],
    active: !!f.active,
  }), []);

  const { form, set, isDirty, saving, serverError, save } = useEditableEntity(initial || {}, {
    url: () => selWf ? `/api/config/approval-workflows/${selWf.id}` : null,
    actorId: user?.id,
    buildBody,
    onSaved: (json) => {
      addWorkflow(json.data);
      pushToast({ title: `Workflow ${json.data.code}`, desc: `Aggiornato. ${json.changed ? 'Audit registrato.' : 'Nessuna modifica server.'}`, tone: 'ok' });
      onClose();
    },
  });

  // FASE 9 (s112) — mock-run "valida struttura" via endpoint (no simulazione).
  const [validateResult, setValidateResult] = React.useState(null);
  const [validating, setValidating] = React.useState(false);
  const runValidate = React.useCallback(async () => {
    setValidating(true);
    try {
      const res = await fetch('/api/config/approval-workflows/validate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({ entityType: form.entityType, steps: form.steps || [] }),
      });
      const json = await res.json().catch(() => ({}));
      if (res.ok && json.data) setValidateResult(json.data);
      else setValidateResult({ orphan: false, errors: [json?.error || 'errore validazione'], warnings: [], ok: false });
    } catch (e) {
      setValidateResult({ orphan: false, errors: [String(e?.message || e)], warnings: [], ok: false });
    } finally {
      setValidating(false);
    }
  }, [form.entityType, form.steps, user?.id]);

  if (!selWf) return <Modal open={false} onClose={onClose} title=""/>;

  // Gate preview: risoluzione gate → doc_type label (client-side, da seedCustom).
  const docMap = new Map((seedCustom?.DOC_TYPES || []).map((d) => [d.code, d]));
  const gateLabel = (gate) => {
    if (gate === 'documents_signed') return 'Tutti i documenti richiesti firmati';
    const code = gate.startsWith('doc_signed:') ? gate.slice('doc_signed:'.length) : gate;
    const dt = docMap.get(code);
    return dt ? `${dt.name} (${dt.category}) firmato` : `⚠️ doc_type «${code}» sconosciuto`;
  };
  const stepsWithGates = (form.steps || []).filter((s) => Array.isArray(s.gates) && s.gates.length > 0);

  const codeValid = !form.code || /^[A-Z][A-Z0-9_-]{1,63}$/.test(form.code);
  const nameValid = (form.name || '').trim().length > 0;
  const stepsOk = wfStepsValid(form.steps);
  const condOk = wfStartCondValid(form.startConditions);
  const valid = codeValid && nameValid && stepsOk && condOk;

  const STATUSES = ['draft', 'active', 'deprecated'];
  const ENTITY_TYPES = ['rda', 'progetto', 'vendor', 'contratto', 'sal', 'documento'];

  return (
    <Modal open={!!selWf} onClose={onClose} title={`Workflow · ${selWf.name}`} size="xl"
      footer={<>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
        <Btn variant="primary" size="sm" disabled={!valid || saving || !isDirty} onClick={save}>
          {saving ? 'Salvataggio…' : isDirty ? 'Salva modifiche' : 'Nessuna modifica'}
        </Btn>
      </>}>
      <div className="col" style={{ gap: 14 }}>
        {serverError && (
          <div style={{ padding: '10px 12px', border: '1px solid var(--err, #c0392b)', borderRadius: 6, background: 'rgba(192,57,43,0.08)', color: 'var(--err, #c0392b)', fontSize: 12 }}>
            <strong>Errore salvataggio:</strong> {serverError}
          </div>
        )}

        <div className="grid grid-3">
          <div className="field"><label>Code</label>
            <input value={form.code || ''} onChange={e=>set('code', e.target.value.toUpperCase().replace(/[^A-Z0-9_-]/g,''))} style={{fontFamily:'var(--font-mono)'}}/>
          </div>
          <div className="field"><label>Nome</label>
            <input value={form.name || ''} onChange={e=>set('name', e.target.value)}/>
          </div>
          <div className="field"><label>Entity type</label>
            <select value={form.entityType || 'rda'} onChange={e=>set('entityType', e.target.value)}>
              {ENTITY_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
            </select>
          </div>
          <div className="field"><label>Stato</label>
            <select value={form.status || 'draft'} onChange={e=>set('status', e.target.value)}>
              {STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
            </select>
          </div>
        </div>

        <WorkflowStartConditionsEditor value={form.startConditions || { all: [] }} seedCustom={seedCustom}
          onChange={(v) => set('startConditions', v)}/>

        <WorkflowStepsEditor steps={form.steps || []} seedCustom={seedCustom}
          onChange={(v) => set('steps', v)}/>
        {!stepsOk && (form.steps || []).length > 0 && (
          <div style={{ fontSize: 10.5, color: 'var(--err)' }}>
            Ogni step richiede un nome e un approvatore (ruolo selezionato oppure matrice); le condizioni di ingresso vanno completate con un valore.
          </div>
        )}

        {/* FASE 9 — Valida struttura (mock-run) + anteprima gates risolti */}
        <div style={{ border: '1px solid var(--line)', borderRadius: 8, padding: 12 }} data-section="workflow-validate">
          <div className="row" style={{ gap: 8, alignItems: 'center', marginBottom: 8 }}>
            <div className="eyebrow" style={{ margin: 0 }}>Valida struttura</div>
            <span className="spacer"/>
            <Btn variant="ghost" size="sm" onClick={runValidate} disabled={validating} data-action="validate-workflow">
              {validating ? 'Verifica…' : 'Valida struttura'}
            </Btn>
          </div>

          {validateResult && (
            <div data-testid="validate-result" style={{ marginBottom: stepsWithGates.length > 0 ? 10 : 0 }}>
              {validateResult.ok && validateResult.errors.length === 0 && (
                <div style={{ fontSize: 12, color: 'var(--ok)' }} data-testid="validate-ok">✓ Struttura valida{validateResult.warnings.length > 0 ? ' (con avvisi)' : ''}.</div>
              )}
              {validateResult.errors.map((e, i) => (
                <div key={`e${i}`} style={{ fontSize: 11.5, color: 'var(--err)' }}>✗ {e}</div>
              ))}
              {validateResult.warnings.map((w, i) => (
                <div key={`w${i}`} style={{ fontSize: 11.5, color: 'var(--warn)' }}>⚠ {w}</div>
              ))}
            </div>
          )}

          {/* Anteprima gates: "cosa significa il gate X" senza scavare nei doc_type */}
          {stepsWithGates.length > 0 && (
            <div data-testid="gates-preview">
              <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginBottom: 4 }}>Gate richiesti per step:</div>
              {stepsWithGates.map((s) => (
                <div key={s.id} style={{ fontSize: 11, marginBottom: 3 }}>
                  <span className="mono" style={{ color: 'var(--text-3)' }}>#{s.order}</span>{' '}
                  <strong>{s.name}</strong>:{' '}
                  <span style={{ color: 'var(--text-2)' }}>{s.gates.map((g) => gateLabel(g)).join(' · ')}</span>
                </div>
              ))}
            </div>
          )}
        </div>

        <label className="row" style={{ gap: 6, fontSize: 11.5 }}>
          <input type="checkbox" checked={!!form.active} onChange={e=>set('active', e.target.checked)}/>
          Workflow attivo
        </label>

        {!isDirty && (
          <div style={{ fontSize: 10.5, color: 'var(--text-3)', padding: '6px 10px', background: 'var(--bg-2)', borderRadius: 4 }}>
            Modifica un campo per abilitare "Salva modifiche". <code>PATCH /api/config/approval-workflows/{selWf.id}</code> persisterà con audit automatico.
          </div>
        )}
      </div>
    </Modal>
  );
}

// -------------- APPROVAL MATRIX --------------
// FASE 3a.16: rileva conflitti tra rule della matrice (overlap scope/categoria/classe/range importo).
// Pure function O(N²): due regole sono in conflitto se hanno stesso ruolo + scope (sites/bu)
// sovrapposti + categoria/classe sovrapposte + range importo sovrapposto.
function detectMatrixConflicts(rules) {
  const map = new Map(); // id → array di id in conflitto
  const setOf = (v) => v === '*' || v == null ? '*' : (Array.isArray(v) ? new Set(v) : new Set([v]));
  const intersects = (a, b) => {
    if (a === '*' || b === '*') return true;
    for (const x of a) if (b.has(x)) return true;
    return false;
  };
  const rangeOverlap = (a1, a2, b1, b2) => {
    const aMax = a2 == null ? Infinity : a2;
    const bMax = b2 == null ? Infinity : b2;
    return a1 <= bMax && b1 <= aMax;
  };
  for (let i = 0; i < rules.length; i++) {
    for (let j = i+1; j < rules.length; j++) {
      const a = rules[i], b = rules[j];
      if (a.role !== b.role) continue;
      const aSites = setOf(a.scope?.sites), bSites = setOf(b.scope?.sites);
      const aBus = setOf(a.scope?.bu), bBus = setOf(b.scope?.bu);
      const aCat = a.category === '*' || a.category == null ? '*' : new Set([a.category]);
      const bCat = b.category === '*' || b.category == null ? '*' : new Set([b.category]);
      const aCls = a.capexClass === '*' || a.capexClass == null ? '*' : new Set([a.capexClass]);
      const bCls = b.capexClass === '*' || b.capexClass == null ? '*' : new Set([b.capexClass]);
      if (!intersects(aSites, bSites) || !intersects(aBus, bBus)) continue;
      if (!intersects(aCat, bCat) || !intersects(aCls, bCls)) continue;
      if (!rangeOverlap(a.amountMin || 0, a.amountMax, b.amountMin || 0, b.amountMax)) continue;
      if (!map.has(a.id)) map.set(a.id, []);
      if (!map.has(b.id)) map.set(b.id, []);
      map.get(a.id).push(b.id);
      map.get(b.id).push(a.id);
    }
  }
  return map;
}

function CustMatrix() {
  const { seedCustom, extras, addMatrix } = useStore();
  const matrix = React.useMemo(() => {
    const seedList = seedCustom.APPROVAL_MATRIX || [];
    const extList = extras?.matrixExt || [];
    const seenIds = new Set();
    const out = [];
    for (const m of [...extList, ...seedList]) {
      if (!m?.id || seenIds.has(m.id)) continue;
      seenIds.add(m.id);
      out.push(m);
    }
    return out;
  }, [seedCustom.APPROVAL_MATRIX, extras?.matrixExt]);
  // FASE 3a.16: rileva conflitti scope-overlap.
  const conflicts = React.useMemo(() => detectMatrixConflicts(matrix), [matrix]);
  const conflictsCount = conflicts.size;
  const roles = seedCustom.ROLES || [];
  const categories = seedCustom.CATEGORIES_EXT || [];
  const [sel, setSel] = React.useState(null);
  const [showNew, setShowNew] = React.useState(false);
  const [showSim, setShowSim] = React.useState(false);
  const [filterConflicts, setFilterConflicts] = React.useState(false);

  return (
    <>
      <div className="row" style={{ gap: 8, marginBottom: 12, alignItems:'center' }}>
        <div style={{ fontSize: 12, color: 'var(--text-2)' }}>
          Matrice che determina chi può approvare in base a ruolo, importo, categoria, sito e classe CAPEX. Il workflow consulta questa matrice in ogni step di tipo <Chip kind="ai">matrix</Chip>.
        </div>
        <span className="spacer"/>
        {conflictsCount > 0 && (
          <Btn variant={filterConflicts?'primary':'ghost'} size="sm" onClick={()=>setFilterConflicts(v=>!v)}>
            <Icon name="alert-triangle" size={11}/> {conflictsCount} conflit{conflictsCount === 1 ? 'to' : 'ti'}
          </Btn>
        )}
        <Btn variant="ghost" size="sm" onClick={()=>setShowSim(true)}><Icon name="eye" size={11}/> Simula su entità</Btn>
        <Btn variant="ghost" size="sm"><Icon name="download" size={11}/> Export CSV</Btn>
        <ConfigWriteBtn onClick={()=>setShowNew(true)}><Icon name="plus" size={11}/> Nuova regola</ConfigWriteBtn>
      </div>

      <table className="tbl dense">
        <thead><tr>
          <th style={{width:30,textAlign:'center'}}></th>
          <th style={{width:80}}>ID</th>
          <th>Ruolo</th>
          <th>Sito</th>
          <th>BU</th>
          <th>Categoria</th>
          <th>Classe CAPEX</th>
          <th style={{textAlign:'right'}}>Da</th>
          <th style={{textAlign:'right'}}>A</th>
          <th>Co-firma</th>
          <th>Note</th>
        </tr></thead>
        <tbody>
          {matrix.filter(m => !filterConflicts || conflicts.has(m.id)).map(m => {
            const inConflict = conflicts.has(m.id);
            return (
              <tr key={m.id} className="clickable" onClick={()=>setSel(m)}
                style={inConflict ? { background: 'rgba(192,57,43,0.06)' } : undefined}
                title={inConflict ? `In conflitto con: ${conflicts.get(m.id).join(', ')}` : undefined}
              >
                <td style={{textAlign:'center'}}>
                  {inConflict ? <Icon name="alert-triangle" size={11}/> : <span style={{color:'var(--text-3)'}}>—</span>}
                </td>
                <td className="mono" style={{fontSize:10.5}}>{m.id}</td>
                <td><Chip kind="ai">{m.role}</Chip></td>
                <td style={{fontSize:11.5}}>{m.scope?.sites === '*' ? <span style={{color:'var(--text-3)'}}>tutti</span> : (Array.isArray(m.scope?.sites) ? m.scope.sites.join(', ') : m.scope?.sites)}</td>
                <td style={{fontSize:11.5}}>{m.scope?.bu === '*' ? <span style={{color:'var(--text-3)'}}>tutte</span> : (Array.isArray(m.scope?.bu) ? m.scope.bu.join(', ') : m.scope?.bu)}</td>
                <td style={{fontSize:11.5}}>{(m.category === '*' || !m.category) ? <span style={{color:'var(--text-3)'}}>tutte</span> : m.category}</td>
                <td style={{fontSize:11.5}}>{(m.capexClass === '*' || !m.capexClass) ? <span style={{color:'var(--text-3)'}}>tutte</span> : m.capexClass}</td>
                <td className="num mono">{fmtEUR(m.amountMin || 0, true)}</td>
                <td className="num mono">{m.amountMax == null ? '∞' : fmtEUR(m.amountMax, true)}</td>
                <td>{m.coApproval ? <Chip kind="warn">+ {(m.coApprovers||[]).join(', ')}</Chip> : <span style={{color:'var(--text-3)'}}>—</span>}</td>
                <td style={{fontSize:11, color:'var(--text-2)'}}>{m.notes}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
      {conflictsCount > 0 && !filterConflicts && (
        <div style={{marginTop:8, fontSize:10.5, color:'var(--text-3)', padding:'6px 10px', background:'var(--bg-2)', borderRadius:4, lineHeight:1.5}}>
          <Icon name="alert-triangle" size={10}/> {conflictsCount} regol{conflictsCount === 1 ? 'a' : 'e'} con scope/range sovrapposti — clicca il chip in alto per filtrare.
          Nota: l'overlap implica che più regole potrebbero matchare lo stesso scenario; il workflow engine sceglierà secondo priorità.
        </div>
      )}

      <ApprovalMatrixDetailModal selM={sel} onClose={()=>setSel(null)} roles={roles}/>

      {/* P2 sessione 34 — sostituisce il vecchio NewMatrixRuleModal con versione live (POST /api/config/approval-matrix). */}
      <NewMatrixRuleModalLive
        open={showNew}
        onClose={() => setShowNew(false)}
        roles={roles}
        onCreated={(row) => { if (addMatrix && row) addMatrix(row); }}
      />
      <MatrixSimulateModal open={showSim} onClose={()=>setShowSim(false)} matrix={matrix} categories={categories}/>
    </>
  );
}

// FASE 3a.16: detail modal editable Approval Matrix Entry (PATCH /api/config/approval-matrix/[id]).
function ApprovalMatrixDetailModal({ selM, onClose, roles }) {
  const { addMatrix, pushToast, user } = useStore();
  const serverM = useFetchedDetail(selM, (s) => `/api/config/approval-matrix/${s.id}`);
  const baseM = serverM || selM;

  const initial = React.useMemo(() => {
    if (!baseM) return null;
    const sc = baseM.scope || {};
    return {
      roleCode: baseM.roleCode || baseM.role || '',
      sitesStr: arrayOrStarToString(sc.sites),
      buStr: arrayOrStarToString(sc.bu),
      category: baseM.category && baseM.category !== '*' ? baseM.category : '',
      capexClass: baseM.capexClass && baseM.capexClass !== '*' ? baseM.capexClass : '',
      amountMin: typeof baseM.amountMin === 'number' ? baseM.amountMin : 0,
      amountMax: baseM.amountMax === null || baseM.amountMax === undefined ? '' : String(baseM.amountMax),
      coApproval: !!baseM.coApproval,
      coApproversStr: Array.isArray(baseM.coApprovers) ? baseM.coApprovers.join(', ') : '',
      notes: baseM.notes || '',
      active: baseM.active !== false,
    };
  }, [baseM]);

  const buildBody = React.useCallback((f) => {
    const sites = stringToArrayOrStar(f.sitesStr);
    const bu = stringToArrayOrStar(f.buStr);
    const scopeObj = {};
    if (sites !== undefined) scopeObj.sites = sites;
    if (bu !== undefined) scopeObj.bu = bu;
    return {
      roleCode: f.roleCode,
      scope: Object.keys(scopeObj).length ? scopeObj : null,
      category: (f.category || '').trim() ? (f.category || '').trim() : null,
      capexClass: (f.capexClass || '').trim() ? (f.capexClass || '').trim() : null,
      amountMin: Number(f.amountMin) || 0,
      amountMax: f.amountMax === '' ? null : Number(f.amountMax),
      coApproval: !!f.coApproval,
      coApprovers: (f.coApproversStr || '').split(',').map(x => x.trim()).filter(Boolean),
      notes: (f.notes || '').trim() ? (f.notes || '').trim() : null,
      active: !!f.active,
    };
  }, []);

  const { form, set, isDirty, saving, serverError, save } = useEditableEntity(initial || {}, {
    url: () => selM ? `/api/config/approval-matrix/${selM.id}` : null,
    actorId: user?.id,
    buildBody,
    onSaved: (json) => {
      addMatrix(json.data);
      pushToast({ title: `Regola ${json.data.id}`, desc: `Aggiornata. ${json.changed ? 'Audit registrato.' : 'Nessuna modifica server.'}`, tone: 'ok' });
      onClose();
    },
  });

  if (!selM) return <Modal open={false} onClose={onClose} title=""/>;

  const roleValid = (form.roleCode || '').trim().length > 0;
  const amountValid = form.amountMax === '' || (Number(form.amountMax) >= Number(form.amountMin));
  const valid = roleValid && amountValid;

  return (
    <Modal open={!!selM} onClose={onClose} title={`Regola ${selM.id}`} size="md"
      footer={<>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
        <Btn variant="primary" size="sm" disabled={!valid || saving || !isDirty} onClick={save}>
          {saving ? 'Salvataggio…' : isDirty ? 'Salva modifiche' : 'Nessuna modifica'}
        </Btn>
      </>}>
      <div className="col" style={{gap:14}}>
        {serverError && (
          <div style={{ padding: '10px 12px', border: '1px solid var(--err, #c0392b)', borderRadius: 6, background: 'rgba(192,57,43,0.08)', color: 'var(--err, #c0392b)', fontSize: 12 }}>
            <strong>Errore salvataggio:</strong> {serverError}
          </div>
        )}

        <div className="grid grid-2">
          <div className="field"><label>Ruolo</label>
            <window.Autocomplete value={form.roleCode || ''} onChange={v=>set('roleCode', v)}
              options={(roles || []).map(r => ({ value: r.code, label: r.code, sublabel: r.name }))}
              placeholder="Cerca ruolo… (spazio per lista)" testId="cust-authmatrix-role-ac" />
            {!roleValid && <div style={{fontSize:10.5, color:'var(--err)'}}>Obbligatorio</div>}
          </div>
          <div className="field"><label>Scope siti</label>
            <input value={form.sitesStr || ''} onChange={e=>set('sitesStr', e.target.value)} placeholder="* o id1,id2"/>
          </div>
          <div className="field"><label>Scope BU</label>
            <input value={form.buStr || ''} onChange={e=>set('buStr', e.target.value)} placeholder="* o id1,id2"/>
          </div>
          <div className="field"><label>Categoria</label>
            <input value={form.category || ''} onChange={e=>set('category', e.target.value)} placeholder="* o codice"/>
          </div>
          <div className="field"><label>Classe CAPEX</label>
            <input value={form.capexClass || ''} onChange={e=>set('capexClass', e.target.value)} placeholder="* o codice"/>
          </div>
          <div className="field"><label>Da (€)</label>
            <input type="number" min={0} value={form.amountMin} onChange={e=>set('amountMin', Number(e.target.value))}/>
          </div>
          <div className="field"><label>A (€) — vuoto = ∞</label>
            <input type="number" min={0} value={form.amountMax} onChange={e=>set('amountMax', e.target.value)}/>
            {!amountValid && <div style={{fontSize:10.5, color:'var(--err)'}}>A deve essere ≥ Da</div>}
          </div>
          <div className="field"><label className="row" style={{gap:6}}>
            <input type="checkbox" checked={!!form.coApproval} onChange={e=>set('coApproval', e.target.checked)}/>
            Richiede co-firma
          </label></div>
          {form.coApproval && (
            <div className="field"><label>Co-approvatori (csv ruoli)</label>
              <input value={form.coApproversStr || ''} onChange={e=>set('coApproversStr', e.target.value)} placeholder="CFO, BOARD"/>
            </div>
          )}
          <div className="field" style={{gridColumn:'1 / -1'}}><label>Note</label>
            <textarea rows={2} value={form.notes || ''} onChange={e=>set('notes', e.target.value)}/>
          </div>
        </div>

        <div>
          <label className="row" style={{gap:6, fontSize:11.5}}>
            <input type="checkbox" checked={!!form.active} onChange={e=>set('active', e.target.checked)}/>
            Regola attiva
          </label>
        </div>

        {!isDirty && (
          <div style={{fontSize:10.5, color:'var(--text-3)', padding:'6px 10px', background:'var(--bg-2)', borderRadius:4}}>
            Modifica un campo per abilitare "Salva modifiche". <code>PATCH /api/config/approval-matrix/{selM.id}</code> persisterà con audit automatico.
          </div>
        )}
      </div>
    </Modal>
  );
}

function NewMatrixRuleModal({ open, onClose, roles, categories, matrix }) {
  const roleCodes = roles.map(r => r.code);
  const siteCodes = ['Cameri (NO)','Nola (NA)','Torino','Brescia','Milano HQ'];
  const buCodes = ['Mechatronics','Aerospace','Powertrain','Corporate'];
  const capexClasses = ['NEW_CAPACITY','REPLACEMENT','COMPLIANCE','IT','R_AND_D'];

  const nextId = React.useMemo(() => {
    const nums = matrix.map(m => parseInt(String(m.id).replace(/\D/g,''), 10)).filter(Number.isFinite);
    const max = nums.length ? Math.max(...nums) : 0;
    return 'AM' + String(max + 1).padStart(3, '0');
  }, [matrix, open]);

  const [form, setForm] = React.useState({
    role: roleCodes[0] || 'BUYER',
    sites: '*', bu: '*', category: '*', capexClass: '*',
    amountMin: 0, amountMax: '',
    coApproval: false, coApprovers: [],
    notes: '',
  });
  React.useEffect(() => {
    if (open) setForm({
      role: roleCodes[0] || 'BUYER',
      sites: '*', bu: '*', category: '*', capexClass: '*',
      amountMin: 0, amountMax: '',
      coApproval: false, coApprovers: [],
      notes: '',
    });
  }, [open]);
  const set = (k, v) => setForm(f => ({...f, [k]: v}));

  const amountMinN = Number(form.amountMin) || 0;
  const amountMaxN = form.amountMax === '' || form.amountMax == null ? null : Number(form.amountMax);
  const amountValid = amountMaxN == null || amountMaxN > amountMinN;
  const coValid = !form.coApproval || form.coApprovers.length > 0;
  const valid = form.role && amountValid && coValid;

  // Detect overlaps: same role + same scope + intersecting amount range
  const overlaps = React.useMemo(() => {
    if (!form.role) return [];
    return matrix.filter(m => {
      if (m.role !== form.role) return false;
      if (m.scope?.sites !== form.sites && m.scope?.sites !== '*' && form.sites !== '*') return false;
      if (m.scope?.bu !== form.bu && m.scope?.bu !== '*' && form.bu !== '*') return false;
      if (m.category !== form.category && m.category !== '*' && form.category !== '*') return false;
      if (m.capexClass !== form.capexClass && m.capexClass !== '*' && form.capexClass !== '*') return false;
      const aMin = m.amountMin || 0;
      const aMax = m.amountMax == null ? Infinity : m.amountMax;
      const bMin = amountMinN;
      const bMax = amountMaxN == null ? Infinity : amountMaxN;
      return aMin < bMax && bMin < aMax;
    });
  }, [form, matrix, amountMinN, amountMaxN]);

  return (
    <Modal open={open} onClose={onClose} title="Nuova regola autorizzativa" size="lg"
      footer={<>
        <Btn variant="ghost" size="sm" onClick={onClose}>Annulla</Btn>
        <Btn variant="ghost" size="sm" disabled={!valid}><Icon name="eye" size={11}/> Simula</Btn>
        <Btn variant="primary" size="sm" disabled={!valid} onClick={onClose}>Crea regola</Btn>
      </>}>
      <div className="col" style={{gap:14}}>
        <div style={{fontSize:11.5, color:'var(--text-2)', lineHeight:1.5}}>
          Definisce chi può approvare un'entità (tipicamente RdA o variante) quando il workflow raggiunge uno step di tipo <Chip kind="ai">matrix</Chip>. La prima riga matchante in ordine di specificità vince. Vedi <code>docs/approval-matrix.md</code>.
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Identificativo</div>
          <div className="grid grid-2">
            <div className="field"><label>ID regola</label><input disabled value={nextId} style={{fontFamily:'var(--font-mono)'}}/></div>
            <div className="field"><label>Ruolo <span style={{color:'var(--err)'}}>*</span></label>
              <window.Autocomplete value={form.role} onChange={v=>set('role', v)} allowClear={false}
                options={roleCodes.map(r => ({ value: r, label: r }))}
                placeholder="Cerca ruolo… (spazio per lista)" testId="cust-newmatrix-role-ac" />
            </div>
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Scope</div>
          <div className="grid grid-2">
            <div className="field"><label>Sito</label>
              <select value={form.sites} onChange={e=>set('sites', e.target.value)}>
                <option value="*">Tutti i siti</option>
                {siteCodes.map(s => <option key={s} value={s}>{s}</option>)}
              </select>
            </div>
            <div className="field"><label>Business Unit</label>
              <select value={form.bu} onChange={e=>set('bu', e.target.value)}>
                <option value="*">Tutte le BU</option>
                {buCodes.map(b => <option key={b} value={b}>{b}</option>)}
              </select>
            </div>
            <div className="field"><label>Categoria</label>
              <select value={form.category} onChange={e=>set('category', e.target.value)}>
                <option value="*">Tutte le categorie</option>
                {categories.map(c => <option key={c.id} value={c.name}>{c.name}</option>)}
              </select>
            </div>
            <div className="field"><label>Classe CAPEX</label>
              <select value={form.capexClass} onChange={e=>set('capexClass', e.target.value)}>
                <option value="*">Tutte</option>
                {capexClasses.map(c => <option key={c} value={c}>{c}</option>)}
              </select>
            </div>
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Fascia di importo <span style={{color:'var(--err)'}}>*</span></div>
          <div className="grid grid-2">
            <div className="field"><label>Da (€)</label>
              <input type="number" min={0} step={1000} value={form.amountMin} onChange={e=>set('amountMin', e.target.value)}/>
            </div>
            <div className="field"><label>A (€) — lascia vuoto per ∞</label>
              <input type="number" min={0} step={1000} value={form.amountMax} onChange={e=>set('amountMax', e.target.value)} placeholder="∞"/>
            </div>
          </div>
          {!amountValid && (
            <div style={{fontSize:10.5, color:'var(--err)', marginTop:4}}>
              <Icon name="alert-triangle" size={10}/> L'importo massimo deve essere maggiore del minimo (o vuoto per illimitato).
            </div>
          )}
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Co-approvazione</div>
          <label className="row" style={{gap:6, fontSize:11.5, cursor:'pointer'}}>
            <input type="checkbox" checked={form.coApproval} onChange={e=>set('coApproval', e.target.checked)}/>
            Richiede co-firma di uno o più ruoli aggiuntivi
          </label>
          {form.coApproval && (
            <div style={{marginTop:8, padding:8, border:'1px solid var(--line)', borderRadius:4}}>
              <div style={{fontSize:10.5, color:'var(--text-3)', marginBottom:6}}>Ruoli co-approvatori (tutti devono firmare)</div>
              <div style={{display:'flex', gap:4, flexWrap:'wrap'}}>
                {form.coApprovers.map(r => (
                  <Chip key={r} kind="warn">{r} <span style={{marginLeft:4, cursor:'pointer'}} onClick={()=>set('coApprovers', form.coApprovers.filter(x=>x!==r))}>×</span></Chip>
                ))}
                {roleCodes.filter(r => !form.coApprovers.includes(r) && r !== form.role).map(r => (
                  <button key={r} className="btn sm ghost" style={{fontSize:10, padding:'2px 6px'}} onClick={()=>set('coApprovers', [...form.coApprovers, r])}>+ {r}</button>
                ))}
              </div>
              {form.coApproval && form.coApprovers.length === 0 && (
                <div style={{fontSize:10.5, color:'var(--err)', marginTop:6}}><Icon name="alert-triangle" size={10}/> Seleziona almeno un co-approvatore.</div>
              )}
            </div>
          )}
        </div>

        <div className="field"><label>Note</label>
          <textarea rows={2} value={form.notes} onChange={e=>set('notes', e.target.value)} placeholder="Contesto, link a policy, eccezioni"/>
        </div>

        {overlaps.length > 0 && (
          <div style={{fontSize:10.5, color:'var(--warn)', padding:'8px 10px', background:'color-mix(in oklch, var(--warn) 8%, var(--bg-1))', borderRadius:4, lineHeight:1.5}}>
            <Icon name="alert-triangle" size={10}/> <strong>{overlaps.length} regol{overlaps.length===1?'a sovrapposta':'e sovrapposte'}</strong> con stesso ruolo/scope e range di importo intersecante: {overlaps.map(o=>o.id).join(', ')}. Verifica la priorità prima di salvare.
          </div>
        )}

        <div style={{fontSize:10.5, color:'var(--text-3)', padding:'8px 10px', background:'var(--bg-2)', borderRadius:4, lineHeight:1.5}}>
          <Icon name="info" size={10}/> Mock: il salvataggio usa <code>POST /api/config/approval-matrix</code> e crea una nuova versione di config in draft. Attiva dopo publish.
        </div>
      </div>
    </Modal>
  );
}

function MatrixSimulateModal({ open, onClose, matrix, categories }) {
  const [input, setInput] = React.useState({
    amount: 150000, site: 'Cameri (NO)', bu: 'Mechatronics',
    category: categories[0]?.name || '*', capexClass: 'NEW_CAPACITY',
  });
  const set = (k, v) => setInput(i => ({...i, [k]: v}));

  // Find matching rules; specificity = count of non-'*' fields
  const matches = React.useMemo(() => {
    const amt = Number(input.amount) || 0;
    const result = matrix.map(m => {
      const sMatch = m.scope?.sites === '*' || m.scope?.sites === input.site;
      const bMatch = m.scope?.bu === '*' || m.scope?.bu === input.bu;
      const cMatch = m.category === '*' || m.category === input.category;
      const xMatch = m.capexClass === '*' || m.capexClass === input.capexClass;
      const aMin = m.amountMin || 0;
      const aMax = m.amountMax == null ? Infinity : m.amountMax;
      const aMatch = amt >= aMin && amt <= aMax;
      const ok = sMatch && bMatch && cMatch && xMatch && aMatch;
      const specificity = [m.scope?.sites, m.scope?.bu, m.category, m.capexClass].filter(x => x && x !== '*').length;
      return { rule: m, ok, specificity };
    }).filter(r => r.ok).sort((a,b) => b.specificity - a.specificity);
    return result;
  }, [input, matrix]);

  const winner = matches[0];

  return (
    <Modal open={open} onClose={onClose} title="Simulazione matrice autorizzativa" size="lg"
      footer={<Btn variant="ghost" size="sm" onClick={onClose}>Chiudi</Btn>}>
      <div className="col" style={{gap:14}}>
        <div style={{fontSize:11.5, color:'var(--text-2)'}}>
          Inserisci i parametri di un'entità (es. una RdA) per vedere quale riga della matrice la approverà.
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Parametri entità</div>
          <div className="grid grid-3">
            <div className="field"><label>Importo (€)</label>
              <input type="number" value={input.amount} onChange={e=>set('amount', e.target.value)}/>
            </div>
            <div className="field"><label>Sito</label>
              <select value={input.site} onChange={e=>set('site', e.target.value)}>
                {['Cameri (NO)','Nola (NA)','Torino','Brescia','Milano HQ'].map(s => <option key={s}>{s}</option>)}
              </select>
            </div>
            <div className="field"><label>BU</label>
              <select value={input.bu} onChange={e=>set('bu', e.target.value)}>
                {['Mechatronics','Aerospace','Powertrain','Corporate'].map(b => <option key={b}>{b}</option>)}
              </select>
            </div>
            <div className="field"><label>Categoria</label>
              <select value={input.category} onChange={e=>set('category', e.target.value)}>
                {categories.map(c => <option key={c.id} value={c.name}>{c.name}</option>)}
              </select>
            </div>
            <div className="field"><label>Classe CAPEX</label>
              <select value={input.capexClass} onChange={e=>set('capexClass', e.target.value)}>
                {['NEW_CAPACITY','REPLACEMENT','COMPLIANCE','IT','R_AND_D'].map(c => <option key={c}>{c}</option>)}
              </select>
            </div>
          </div>
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Risultato</div>
          {!winner ? (
            <div style={{padding:12, border:'1px solid var(--err)', borderRadius:4, color:'var(--err)', fontSize:11.5}}>
              <Icon name="alert-triangle" size={11}/> Nessuna riga della matrice copre questa combinazione. Lo step di approvazione fallirebbe o sarebbe escalato al ruolo di default.
            </div>
          ) : (
            <div style={{padding:12, border:'1px solid var(--ok)', borderRadius:4, background:'color-mix(in oklch, var(--ok) 8%, var(--bg-1))'}}>
              <div style={{display:'flex', alignItems:'center', gap:8, marginBottom:8}}>
                <Chip kind="ok" dot>Winner</Chip>
                <span className="mono" style={{fontSize:11}}>{winner.rule.id}</span>
                <span style={{fontSize:11, color:'var(--text-3)'}}>specificità: {winner.specificity}/4</span>
              </div>
              <div style={{fontSize:12}}>
                Approvatore: <Chip kind="ai">{winner.rule.role}</Chip>
                {winner.rule.coApproval && <> + co-firma: {(winner.rule.coApprovers||[]).map(c => <Chip key={c} kind="warn">{c}</Chip>)}</>}
              </div>
            </div>
          )}
        </div>

        {matches.length > 1 && (
          <div>
            <div className="eyebrow" style={{marginBottom:6}}>Altre regole matchanti ({matches.length - 1})</div>
            <table className="tbl dense">
              <thead><tr><th>ID</th><th>Ruolo</th><th style={{textAlign:'right'}}>Range</th><th>Specificità</th></tr></thead>
              <tbody>{matches.slice(1).map(m => (
                <tr key={m.rule.id}>
                  <td className="mono" style={{fontSize:10.5}}>{m.rule.id}</td>
                  <td><Chip>{m.rule.role}</Chip></td>
                  <td className="num mono">{fmtEUR(m.rule.amountMin, true)} – {m.rule.amountMax == null ? '∞' : fmtEUR(m.rule.amountMax, true)}</td>
                  <td style={{fontSize:11}}>{m.specificity}/4</td>
                </tr>
              ))}</tbody>
            </table>
          </div>
        )}
      </div>
    </Modal>
  );
}

// -------------- ROLES --------------
function CustRoles() {
  const { seedCustom, extras } = useStore();
  // FASE 3a.7: merge extras + seed con dedup per id (extras vince).
  const roles = React.useMemo(() => {
    const seedList = seedCustom.ROLES || [];
    const extList = extras?.rolesExt || [];
    const seenIds = new Set();
    const out = [];
    for (const r of [...extList, ...seedList]) {
      if (!r?.id || seenIds.has(r.id)) continue;
      seenIds.add(r.id);
      out.push(r);
    }
    return out;
  }, [seedCustom.ROLES, extras?.rolesExt]);
  const [sel, setSel] = React.useState(null);
  const [showNew, setShowNew] = React.useState(false);

  const permSummary = (r) => {
    const p = Array.isArray(r.permissions) ? r.permissions : [];
    if (p.includes('*')) return 'Completo';
    if (p.length === 0) return '—';
    return `${p.length} permess${p.length === 1 ? 'o' : 'i'}`;
  };
  const capexSummary = (r) => {
    const c = r.delegationScope?.capex_classes;
    if (c == null || c === '*') return 'tutte';
    if (Array.isArray(c)) return c.length ? c.join(', ') : '—';
    return String(c);
  };

  return (
    <>
      <div className="row" style={{ gap: 8, marginBottom: 12 }}>
        <Chip>{roles.length} ruoli · {roles.reduce((a,r)=>a+(r.personaCount||0),0)} persone</Chip>
        <span className="spacer"/>
        <Btn variant="ghost" size="sm"><Icon name="download" size={11}/> Export RACI</Btn>
        <ConfigWriteBtn onClick={()=>setShowNew(true)}><Icon name="plus" size={11}/> Nuovo ruolo</ConfigWriteBtn>
      </div>

      <NewRoleModal open={showNew} onClose={()=>setShowNew(false)} roles={roles}/>

      <table className="tbl dense">
        <thead><tr>
          <th style={{width:170}}>Codice</th>
          <th>Nome</th>
          <th style={{width:110}}>Permessi</th>
          <th style={{width:110,textAlign:'right'}}>Limite €</th>
          <th style={{width:150}}>Classi CAPEX</th>
          <th style={{width:130}}>Sostituto</th>
          <th style={{width:70,textAlign:'right'}}>Persone</th>
        </tr></thead>
        <tbody>
          {roles.map(r => (
            <tr key={r.id} className="clickable" onClick={()=>setSel(r)}>
              <td><Chip kind="ai">{r.code}</Chip></td>
              <td style={{fontWeight:500}}>{r.name}</td>
              <td style={{fontSize:11.5, color:'var(--text-2)'}}>{permSummary(r)}</td>
              <td className="num mono">{r.delegationScope?.amount_max_eur == null ? '∞' : fmtEUR(r.delegationScope.amount_max_eur, true)}</td>
              <td style={{fontSize:11, color:'var(--text-2)'}}>{capexSummary(r)}</td>
              <td style={{fontSize:11}}>{r.substituteRole || <span style={{color:'var(--text-3)'}}>—</span>}</td>
              <td className="num mono">{r.personaCount}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <RoleDetailModal sel={sel} onClose={()=>setSel(null)} roles={roles} />
    </>
  );
}

// ============================================================
// FASE 16 (sessione 94) — Editor Ruoli strutturato
// ============================================================
// Sostituisce la textarea free-text dei permessi con una matrice namespace ×
// azioni (catalogo in lib/permissions.jsx), un'anteprima live delle voci di
// menu visibili, e uno scope operativo enforçato a runtime (limite importo +
// classi CAPEX). Componenti condivisi tra RoleDetailModal e NewRoleModal.

// I 9 codici CAPEX class canonical (enum DB capex_class_code) — allineati a
// CAPEX_CLASS_CODES in apps/web/src/lib/capex-class.ts e all'enum project.capexClass.
const CAPEX_CLASS_OPTIONS = [
  { v: 'growth', label: 'Crescita' },
  { v: 'maintenance', label: 'Mantenimento' },
  { v: 'compliance', label: 'Compliance' },
  { v: 'efficiency', label: 'Efficienza' },
  { v: 'hse', label: 'HSE' },
  { v: 'it_ot', label: 'IT / OT' },
  { v: 'replacement', label: 'Sostituzione' },
  { v: 'new_capacity', label: 'Nuova capacità' },
  { v: 'strategic', label: 'Strategico / R&D' },
];

function navLabelById(id) {
  for (const g of (window.NAV_PREVIEW || [])) {
    for (const it of g.items) if (it.id === id) return it.label;
  }
  return id;
}

// Matrice permessi: namespace × azioni, niente free-text. I permessi non
// modellati dal catalogo (rari/avanzati) sono preservati come chip rimuovibili.
function RolePermissionEditor({ value, onChange }) {
  const perms = Array.isArray(value) ? value : [];
  const catalog = window.PERMISSION_CATALOG || [];
  const known = window.CATALOG_KNOWN_PERMS || new Set();
  const isSuper = perms.includes('*');
  const isReader = perms.includes('*.read');
  const has = (p) => perms.includes(p);
  const commit = (next) => onChange(Array.from(new Set(next)));

  const toggle = (p) => commit(has(p) ? perms.filter((x) => x !== p) : [...perms, p]);
  const toggleSuper = () => commit(isSuper ? [] : ['*']);
  const toggleReader = () => commit(isReader
    ? perms.filter((x) => x !== '*.read')
    : [...perms.filter((x) => x !== '*'), '*.read']);
  const toggleGroupAll = (g) => {
    if (has(g.wildcard)) commit(perms.filter((x) => x !== g.wildcard));
    else commit([...perms.filter((x) => !x.startsWith(g.key + '.')), g.wildcard]);
  };

  const extras = perms.filter((p) => p !== '*' && p !== '*.read' && !known.has(p));

  return (
    <div className="col" style={{ gap: 8 }}>
      <div className="col" style={{ gap: 5, padding: '8px 10px', background: 'var(--bg-2)', borderRadius: 6 }}>
        <label className="row" style={{ gap: 8, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
          <input type="checkbox" checked={isSuper} onChange={toggleSuper} />
          Accesso completo — Super-admin (<code>*</code>)
        </label>
        {!isSuper && (
          <label className="row" style={{ gap: 8, fontSize: 11.5, cursor: 'pointer', color: 'var(--text-2)' }}>
            <input type="checkbox" checked={isReader} onChange={toggleReader} />
            Lettore globale (<code>*.read</code>) — vede ogni sezione tranne il Customizing
          </label>
        )}
      </div>

      {isSuper ? (
        <div style={{ fontSize: 11.5, color: 'var(--text-3)', padding: '10px 12px', border: '1px dashed var(--line)', borderRadius: 6 }}>
          <code>*</code> concede ogni azione su ogni sezione. Disattiva "Super-admin" per scegliere i permessi nel dettaglio.
        </div>
      ) : catalog.map((g) => {
        const groupAll = !!g.wildcard && has(g.wildcard);
        return (
          <div key={g.key} style={{ border: '1px solid var(--line)', borderRadius: 6, padding: '8px 10px' }}>
            <div className="row" style={{ gap: 6, marginBottom: (g.unlocks && g.unlocks.length) ? 3 : 6 }}>
              <Icon name={g.icon} size={13} />
              <strong style={{ fontSize: 12 }}>{g.label}</strong>
              {g.sensitive && (
                <span style={{ fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'rgba(234,179,8,0.18)', color: '#a16207' }}>sensibile</span>
              )}
              <span className="spacer" />
              {g.wildcard && (
                <label className="row" style={{ gap: 4, fontSize: 10.5, cursor: 'pointer', color: 'var(--text-2)' }}>
                  <input type="checkbox" checked={groupAll} onChange={() => toggleGroupAll(g)} /> tutto il gruppo
                </label>
              )}
            </div>
            {g.unlocks && g.unlocks.length > 0 && (
              <div style={{ fontSize: 10, color: 'var(--text-3)', marginBottom: 6 }}>
                Sblocca i menu: {g.unlocks.map(navLabelById).join(' · ')}
              </div>
            )}
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px 14px' }}>
              {g.actions.map((a) => (
                <label key={a.perm} className="row" style={{ gap: 6, fontSize: 11, cursor: groupAll ? 'default' : 'pointer', opacity: groupAll ? 0.55 : 1 }}>
                  <input type="checkbox" checked={groupAll || has(a.perm)} disabled={groupAll} onChange={() => toggle(a.perm)} />
                  <span>{a.label} <code style={{ fontSize: 9, color: 'var(--text-3)' }}>{a.perm}</code></span>
                </label>
              ))}
            </div>
          </div>
        );
      })}

      {!isSuper && extras.length > 0 && (
        <div style={{ padding: '6px 10px', background: 'var(--bg-2)', borderRadius: 6 }}>
          <div style={{ fontSize: 10, color: 'var(--text-3)', marginBottom: 4 }}>Permessi avanzati non in catalogo — clic per rimuovere:</div>
          <div className="row" style={{ flexWrap: 'wrap', gap: 4 }}>
            {extras.map((p) => (
              <button key={p} className="btn ghost sm" style={{ fontSize: 9.5, fontFamily: 'var(--font-mono)', padding: '2px 6px' }} onClick={() => toggle(p)}>
                {p} ✕
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// Anteprima live: quali voci della Sidebar vede chi ha questo ruolo.
function RoleMenuPreview({ permissions }) {
  const nav = window.navVisibilityForPerms ? window.navVisibilityForPerms(permissions || []) : [];
  const visible = nav.filter((n) => n.visible).length;
  const groups = [];
  for (const n of nav) {
    let g = groups.find((x) => x.label === n.group);
    if (!g) { g = { label: n.group, items: [] }; groups.push(g); }
    g.items.push(n);
  }
  return (
    <div>
      <div style={{ fontSize: 10.5, color: 'var(--text-3)', marginBottom: 6 }}>
        {visible} di {nav.length} voci di menu visibili con questi permessi.
      </div>
      <div className="col" style={{ gap: 6 }}>
        {groups.map((g) => (
          <div key={g.label} className="row" style={{ gap: 6, flexWrap: 'wrap', alignItems: 'baseline' }}>
            <span className="eyebrow" style={{ fontSize: 9, minWidth: 78 }}>{g.label}</span>
            {g.items.map((it) => (
              <span key={it.id} style={{
                fontSize: 10.5, padding: '2px 7px', borderRadius: 4,
                background: it.visible ? 'rgba(34,197,94,0.13)' : 'var(--bg-2)',
                color: it.visible ? '#15803d' : 'var(--text-3)',
                textDecoration: it.visible ? 'none' : 'line-through',
              }}>{it.visible ? '✓' : '✗'} {it.label}</span>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}

// Scope operativo enforçato a runtime: limite importo + classi CAPEX consentite.
function RoleScopeEditor({ amountMaxEur, capexClasses, setAmount, setCapex }) {
  const allClasses = capexClasses === '*' || capexClasses == null;
  const arr = Array.isArray(capexClasses) ? capexClasses : [];
  const toggleClass = (v) => setCapex(arr.includes(v) ? arr.filter((x) => x !== v) : [...arr, v]);
  return (
    <div className="col" style={{ gap: 8 }}>
      <div style={{ fontSize: 11, color: 'var(--text-2)', lineHeight: 1.5 }}>
        Limita le operazioni di chi ha il ruolo. Entrambe le dimensioni sono <strong>applicate a runtime</strong> dal motore alla creazione di progetti e RdA.
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
        <div className="field">
          <label>Limite di importo €</label>
          <input type="number" min={0} step={1000} value={amountMaxEur ?? ''}
            onChange={(e) => setAmount(e.target.value === '' ? null : Number(e.target.value))}
            placeholder="∞ — nessun limite" />
          <div style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 2 }}>
            Budget progetto / importo RdA oltre la soglia → bloccati. Vuoto = illimitato.
          </div>
        </div>
        <div className="field">
          <label>Classi CAPEX consentite</label>
          <label className="row" style={{ gap: 6, fontSize: 11, cursor: 'pointer' }}>
            <input type="checkbox" checked={allClasses} onChange={() => setCapex(allClasses ? [] : '*')} /> Tutte le classi
          </label>
          {!allClasses && (
            <div className="row" style={{ flexWrap: 'wrap', gap: 10, marginTop: 4 }}>
              {CAPEX_CLASS_OPTIONS.map((c) => (
                <label key={c.v} className="row" style={{ gap: 4, fontSize: 11, cursor: 'pointer' }}>
                  <input type="checkbox" checked={arr.includes(c.v)} onChange={() => toggleClass(c.v)} /> {c.label}
                </label>
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// FASE 3a.15 → ridisegnato FASE 16 (sessione 94): detail modal editable per Role.
function RoleDetailModal({ sel, onClose, roles }) {
  const { addRole, pushToast, user } = useStore();
  const initial = React.useMemo(() => {
    if (!sel) return null;
    const ds = sel.delegationScope || {};
    return {
      code: sel.code || '',
      name: sel.name || '',
      description: sel.description || null,
      permissions: Array.isArray(sel.permissions) ? sel.permissions : [],
      amountMaxEur: ds.amount_max_eur ?? null,
      capexClasses: ds.capex_classes ?? '*',
      defaultSubstituteRoleId: sel.defaultSubstituteRoleId || sel.substituteRole || '',
      active: sel.active !== false,
    };
  }, [sel]);

  const { form, set, isDirty, saving, serverError, save } = useEditableEntity(initial || {}, {
    url: () => sel ? `/api/config/roles/${sel.id}` : null,
    actorId: user?.id,
    buildBody: (f) => ({
      code: f.code,
      name: f.name?.trim(),
      description: f.description?.trim() || null,
      permissions: f.permissions,
      delegationScope: {
        amount_max_eur: f.amountMaxEur == null || f.amountMaxEur === '' ? null : Number(f.amountMaxEur),
        capex_classes: f.capexClasses === '*' ? '*' : (Array.isArray(f.capexClasses) ? f.capexClasses : '*'),
      },
      defaultSubstituteRoleId: f.defaultSubstituteRoleId || null,
      active: !!f.active,
    }),
    onSaved: (json) => {
      addRole(json.data);
      pushToast({ title: 'Ruolo aggiornato', desc: `${json.data.code} · ${json.data.name} salvato. ${json.changed ? 'Audit registrato.' : 'Nessuna modifica server.'}`, tone: 'ok' });
      onClose();
    },
  });

  if (!sel) return <Modal open={false} onClose={onClose} title="" />;

  const codeValid = !form.code || /^[A-Z][A-Z0-9_-]{1,31}$/.test(form.code);
  const valid = form.name?.trim() && codeValid;

  return (
    <Modal open={!!sel} onClose={onClose} title={`${sel.code} · ${sel.name}`} size="lg"
      footer={<>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
        <Btn variant="primary" size="sm" disabled={!valid || saving || !isDirty} onClick={save}>
          {saving ? 'Salvataggio…' : isDirty ? 'Salva modifiche' : 'Nessuna modifica'}
        </Btn>
      </>}>
      <div className="col" style={{ gap: 14 }}>
        {serverError && (
          <div style={{ padding: '10px 12px', border: '1px solid var(--err, #c0392b)', borderRadius: 6, background: 'rgba(192,57,43,0.08)', color: 'var(--err, #c0392b)', fontSize: 12 }}>
            <strong>Errore salvataggio:</strong> {serverError}
          </div>
        )}
        <div className="grid grid-3">
          <div className="field"><label>Codice</label>
            <input value={form.code ?? ''} onChange={e=>set('code', e.target.value.toUpperCase().replace(/[^A-Z0-9_-]/g,''))} style={{fontFamily:'var(--font-mono)'}}/>
            {form.code && !codeValid && <div style={{fontSize:10.5, color:'var(--err)'}}>Code: maiuscola + A-Z/0-9/_/- (max 32)</div>}
          </div>
          <div className="field"><label>Nome</label>
            <input value={form.name ?? ''} onChange={e=>set('name', e.target.value)}/>
          </div>
          <div className="field"><label>Sostituto default</label>
            <window.Autocomplete value={form.defaultSubstituteRoleId || ''} onChange={v=>set('defaultSubstituteRoleId', v)}
              options={roles.filter(r => r.id !== sel.id).map(r => ({ value: r.id, label: r.name, sublabel: r.code }))}
              placeholder="Nessuno · cerca ruolo…" testId="cust-role-substitute-edit-ac" />
          </div>
        </div>
        <div className="field"><label>Descrizione</label>
          <textarea rows={2} value={form.description || ''} onChange={e=>set('description', e.target.value || null)}/>
        </div>
        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Permessi & navigazione</div>
          <RolePermissionEditor value={form.permissions} onChange={(v)=>set('permissions', v)} />
        </div>
        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Anteprima menu laterale</div>
          <RoleMenuPreview permissions={form.permissions} />
        </div>
        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Scope operativo</div>
          <RoleScopeEditor amountMaxEur={form.amountMaxEur} capexClasses={form.capexClasses}
            setAmount={(v)=>set('amountMaxEur', v)} setCapex={(v)=>set('capexClasses', v)} />
        </div>
        <div className="field"><label className="row" style={{gap:6}}>
          <input type="checkbox" checked={!!form.active} onChange={e=>set('active', e.target.checked)}/> Ruolo attivo
        </label></div>
      </div>
    </Modal>
  );
}

// -------------- NEW ROLE MODAL — ridisegnato FASE 16 (sessione 94) --------------
function NewRoleModal({ open, onClose, roles }) {
  const { addRole, pushToast, user } = useStore();
  const initial = () => ({
    code: '', name: '', description: '',
    permissions: [],
    amountMaxEur: null, capexClasses: '*',
    defaultSubstituteRoleId: '',
    active: true,
  });
  const [form, setForm] = React.useState(initial);
  const [saving, setSaving] = React.useState(false);
  const [serverError, setServerError] = React.useState(null);
  React.useEffect(() => { if (open) { setForm(initial()); setServerError(null); } }, [open]);
  const set = (k, v) => setForm(f => ({...f, [k]: v}));

  const codeValid = /^[A-Z][A-Z0-9_-]{1,31}$/.test(form.code);
  const codeUnique = !roles.some(r => r.code === form.code);
  const valid = form.name.trim() && codeValid && codeUnique;

  async function handleSubmit() {
    if (!valid || saving) return;
    setSaving(true); setServerError(null);
    try {
      const res = await fetch('/api/config/roles', {
        method: 'POST',
        headers: { 'content-type': 'application/json', ...(user?.id ? { 'X-Actor-Persona-Id': user.id } : {}) },
        body: JSON.stringify({
          code: form.code,
          name: form.name.trim(),
          description: form.description.trim() || null,
          permissions: form.permissions,
          delegationScope: {
            amount_max_eur: form.amountMaxEur == null || form.amountMaxEur === '' ? null : Number(form.amountMaxEur),
            capex_classes: form.capexClasses === '*' ? '*' : (Array.isArray(form.capexClasses) ? form.capexClasses : '*'),
          },
          defaultSubstituteRoleId: form.defaultSubstituteRoleId || null,
          active: !!form.active,
        }),
      });
      const json = await res.json().catch(()=>({}));
      if (!res.ok) {
        setServerError(json?.error === 'validation_error' ? `Validazione fallita: ${(json.issues||[]).map(i=>i.message).join(' · ')}` : (json?.error || `HTTP ${res.status}`));
        return;
      }
      if (json?.data) { addRole(json.data); pushToast({ title: `${json.data.code} · ${json.data.name}`, desc: 'Ruolo salvato in DB. Audit registrato.', tone: 'ok' }); }
      onClose();
    } catch (e) { setServerError(String(e?.message||e)); }
    finally { setSaving(false); }
  }

  return (
    <Modal open={open} onClose={onClose} title="Nuovo ruolo" size="lg"
      footer={<>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
        <Btn variant="primary" size="sm" disabled={!valid || saving} onClick={handleSubmit}>{saving ? 'Salvataggio…' : 'Crea ruolo'}</Btn>
      </>}>
      <div className="col" style={{gap:14}}>
        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Identificativo</div>
          <div className="grid grid-3">
            <div className="field"><label>Code <span style={{color:'var(--err)'}}>*</span></label>
              <input value={form.code} onChange={e=>set('code', e.target.value.toUpperCase().replace(/[^A-Z0-9_-]/g,''))} placeholder="PM_CAPEX" style={{fontFamily:'var(--font-mono)'}}/>
            </div>
            <div className="field" style={{gridColumn:'span 2'}}><label>Nome <span style={{color:'var(--err)'}}>*</span></label>
              <input value={form.name} onChange={e=>set('name', e.target.value)} placeholder="es. Project Manager CAPEX"/>
            </div>
          </div>
          <div className="field" style={{marginTop:8}}><label>Descrizione</label>
            <textarea rows={2} value={form.description} onChange={e=>set('description', e.target.value)}/>
          </div>
          {form.code && !codeValid && <div style={{fontSize:10.5, color:'var(--err)'}}><Icon name="alert-triangle" size={10}/> Code: maiuscola iniziale + A-Z/0-9/_/-</div>}
          {codeValid && !codeUnique && <div style={{fontSize:10.5, color:'var(--err)'}}><Icon name="alert-triangle" size={10}/> Code già esistente</div>}
        </div>

        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Permessi & navigazione</div>
          <RolePermissionEditor value={form.permissions} onChange={(v)=>set('permissions', v)} />
        </div>
        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Anteprima menu laterale</div>
          <RoleMenuPreview permissions={form.permissions} />
        </div>
        <div>
          <div className="eyebrow" style={{marginBottom:6}}>Scope operativo</div>
          <RoleScopeEditor amountMaxEur={form.amountMaxEur} capexClasses={form.capexClasses}
            setAmount={(v)=>set('amountMaxEur', v)} setCapex={(v)=>set('capexClasses', v)} />
        </div>

        <div className="field"><label>Sostituto default</label>
          <window.Autocomplete value={form.defaultSubstituteRoleId} onChange={v=>set('defaultSubstituteRoleId', v)}
            options={roles.map(r => ({ value: r.id, label: r.name, sublabel: r.code }))}
            placeholder="Nessuno · cerca ruolo…" testId="cust-role-substitute-ac" />
        </div>

        <div className="field"><label className="row" style={{gap:6}}>
          <input type="checkbox" checked={!!form.active} onChange={e=>set('active', e.target.checked)}/> Ruolo attivo
        </label></div>

        {serverError && (
          <div style={{fontSize:11, color:'var(--err)', padding:'8px 10px', background:'rgba(239,68,68,0.06)', border:'1px solid var(--err)', borderRadius:4}}>
            <Icon name="alert-triangle" size={10}/> {serverError}
          </div>
        )}
      </div>
    </Modal>
  );
}

// ============================================================
// FASE 1 Project Cockpit (s104) — CustProjectRoles
// CRUD del catalogo `project_role` (PM, Sponsor, Eng Lead, ...). Diverso
// dai `role` RBAC: questi sono ruoli "nel progetto", capabilities sono
// suggerimenti UX. DELETE è bloccato a 409 se ci sono membri attivi.
// ============================================================
function CustProjectRoles() {
  const { user, pushToast } = useStore();
  const [items, setItems] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [showNew, setShowNew] = React.useState(false);
  const [sel, setSel] = React.useState(null);

  const reload = React.useCallback(async () => {
    setLoading(true);
    try {
      const r = await fetch('/api/config/project-roles', {
        headers: { 'X-Actor-Persona-Id': user?.id || '' },
      });
      const j = await r.json().catch(() => ({}));
      setItems(Array.isArray(j?.data) ? j.data : []);
    } catch { setItems([]); }
    finally { setLoading(false); }
  }, [user?.id]);
  React.useEffect(() => { reload(); }, [reload]);

  const onDelete = async (item) => {
    if ((item.usageCount ?? 0) > 0) {
      pushToast({ title: 'Impossibile eliminare', desc: `Il ruolo è usato da ${item.usageCount} membri attivi. Riassegnarli prima.`, tone: 'warn' });
      return;
    }
    if (!window.confirm(`Eliminare il ruolo "${item.code}"?`)) return;
    try {
      const r = await fetch(`/api/config/project-roles/${item.id}`, {
        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: 'Ruolo eliminato', desc: `${item.code} rimosso dal catalogo`, tone: 'ok' });
      reload();
    } catch (err) {
      pushToast({ title: 'Errore di rete', desc: err?.message || 'fail', tone: 'err' });
    }
  };

  if (items === null) return <div style={{ fontSize: 12, color: 'var(--text-3)' }}>Caricamento…</div>;

  return (
    <>
      <div className="row" style={{ gap: 8, marginBottom: 12 }}>
        <Chip>{items.length} ruoli</Chip>
        <span className="spacer"/>
        <ConfigWriteBtn onClick={() => setShowNew(true)}><Icon name="plus" size={11}/> Nuovo ruolo</ConfigWriteBtn>
      </div>
      <div style={{ fontSize: 11.5, color: 'var(--text-2)', marginBottom: 12, padding: 10, background: 'var(--bg-2)', borderRadius: 6, border: '1px solid var(--line)' }}>
        <strong>Ruoli di progetto</strong> definiscono "chi fa cosa" dentro un progetto specifico (PM, Sponsor, ENG Lead, …). Sono diversi dai <em>ruoli organizzativi</em> (RBAC) — le capabilities qui sono <em>suggerimenti UX</em>, NON permessi di sicurezza. Un ruolo non può essere cancellato se è ancora assegnato a uno o più membri attivi.
      </div>
      <table className="tbl dense">
        <thead><tr>
          <th style={{ width: 130 }}>Code</th>
          <th>Nome</th>
          <th style={{ width: 250 }}>Capabilities (suggerimenti UX)</th>
          <th style={{ width: 90, textAlign: 'center' }}>Suggeriti</th>
          <th style={{ width: 80, textAlign: 'center' }}>Default</th>
          <th style={{ width: 80, textAlign: 'right' }}>Ordine</th>
          <th style={{ width: 90, textAlign: 'right' }}>In uso</th>
          <th style={{ width: 80 }}></th>
        </tr></thead>
        <tbody>
          {items.map((r) => (
            <tr key={r.id} className="clickable" onClick={() => setSel(r)}>
              <td className="mono" style={{ fontSize: 10.5 }}>{r.code}</td>
              <td style={{ fontWeight: 500 }}>{r.name}</td>
              <td style={{ fontSize: 10.5, color: 'var(--text-2)' }}>
                {Array.isArray(r.capabilities) && r.capabilities.length > 0
                  ? r.capabilities.join(', ')
                  : <span style={{ color: 'var(--text-3)' }}>—</span>}
              </td>
              <td style={{ textAlign: 'center' }}>
                {Array.isArray(r.suggestedRoleIds) && r.suggestedRoleIds.length > 0
                  ? <Chip kind="info">{r.suggestedRoleIds.length}</Chip>
                  : <span style={{ color: 'var(--text-3)' }}>—</span>}
              </td>
              <td style={{ textAlign: 'center' }}>{r.isDefault ? <Chip kind="info" dot>sì</Chip> : <span style={{ color: 'var(--text-3)' }}>—</span>}</td>
              <td className="num mono">{r.displayOrder}</td>
              <td className="num mono" style={{ color: (r.usageCount ?? 0) > 0 ? 'var(--text-1)' : 'var(--text-3)' }}>
                {r.usageCount ?? 0}
              </td>
              <td onClick={(e) => e.stopPropagation()} style={{ textAlign: 'right' }}>
                <button
                  className="btn ghost icon"
                  onClick={() => onDelete(r)}
                  disabled={(r.usageCount ?? 0) > 0}
                  title={(r.usageCount ?? 0) > 0 ? `Non eliminabile: ${r.usageCount} membri attivi` : 'Elimina'}
                  style={{ opacity: (r.usageCount ?? 0) > 0 ? 0.4 : 1 }}
                >
                  <Icon name="x" size={11}/>
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      {showNew && <NewProjectRoleModal open={showNew} onClose={() => { setShowNew(false); reload(); }} existingCodes={items.map(i => i.code)} />}
      {sel && <ProjectRoleDetailModal item={sel} onClose={() => { setSel(null); reload(); }} />}
    </>
  );
}

function NewProjectRoleModal({ open, onClose, existingCodes = [] }) {
  const { user, pushToast, seedCustom } = useStore();
  const initial = () => ({ code: '', name: '', description: '', capabilities: [], suggestedRoleIds: [], isDefault: false, displayOrder: 100 });
  const [form, setForm] = React.useState(initial);
  const [saving, setSaving] = React.useState(false);
  const [err, setErr] = React.useState(null);
  React.useEffect(() => { if (open) { setForm(initial()); setSaving(false); setErr(null); } }, [open]);
  const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
  const codeValid = /^[A-Z][A-Z0-9_]{1,31}$/.test(form.code);
  const codeUnique = !existingCodes.includes(form.code);
  const valid = form.name.trim() && codeValid && codeUnique;

  const submit = async () => {
    if (!valid || saving) return;
    setSaving(true); setErr(null);
    try {
      const r = await fetch('/api/config/project-roles', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({
          code: form.code,
          name: form.name.trim(),
          description: form.description.trim() || null,
          capabilities: form.capabilities.filter(Boolean),
          suggestedRoleIds: form.suggestedRoleIds.filter(Boolean),
          isDefault: !!form.isDefault,
          displayOrder: Number(form.displayOrder) || 0,
        }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { setErr(j?.detail || j?.error || `HTTP ${r.status}`); setSaving(false); return; }
      pushToast({ title: 'Ruolo creato', desc: `${form.code} aggiunto al catalogo`, tone: 'ok' });
      onClose();
    } catch (e) { setErr(e?.message || 'rete'); setSaving(false); }
  };

  return (
    <Modal open={open} onClose={onClose} size="md" title="Nuovo ruolo di progetto" footer={
      <>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
        <span className="spacer"/>
        <Btn variant="primary" size="sm" onClick={submit} disabled={!valid || saving}>{saving ? 'Salvataggio…' : 'Crea'}</Btn>
      </>
    }>
      {err && <div style={{ marginBottom: 10, padding: 8, color: 'var(--err)', border: '1px solid var(--err)', borderRadius: 4, fontSize: 11.5 }}>{err}</div>}
      <FormGrid cols={2}>
        <FormField label="Code" required hint="maiuscolo, A-Z/0-9/_">
          <input className="mono" value={form.code} onChange={(e) => set('code', e.target.value.toUpperCase())} placeholder="QA_INTERNAL"/>
          {!codeValid && form.code && <div style={{ fontSize: 10.5, color: 'var(--err)' }}>Formato non valido</div>}
          {codeValid && !codeUnique && <div style={{ fontSize: 10.5, color: 'var(--err)' }}>Code già usato</div>}
        </FormField>
        <FormField label="Nome" required>
          <input value={form.name} onChange={(e) => set('name', e.target.value)} placeholder="QA Interno"/>
        </FormField>
        <FormField label="Descrizione" cols={2}>
          <textarea rows={2} value={form.description} onChange={(e) => set('description', e.target.value)} placeholder="A cosa serve questo ruolo nel progetto…"/>
        </FormField>
        <FormField label="Capabilities (csv)" hint="suggerimenti UX, non permessi RBAC" cols={2}>
          <input
            value={form.capabilities.join(',')}
            onChange={(e) => set('capabilities', e.target.value.split(',').map(s => s.trim()).filter(Boolean))}
            placeholder="review_doc,upload_doc"
          />
        </FormField>
        <FormField label="Ruoli di sistema suggeriti" hint="pre-rank: chi ha questi ruoli sale in cima nella ricerca persona del Team (mai un filtro rigido)" cols={2}>
          <DocTypeSignerRolesPicker
            value={form.suggestedRoleIds}
            seedCustom={seedCustom}
            onChange={(v) => set('suggestedRoleIds', v)}
            placeholder="+ aggiungi ruolo suggerito…"
          />
        </FormField>
        <FormField label="Default in UI">
          <label className="row" style={{ gap: 6, fontSize: 12 }}>
            <input type="checkbox" checked={form.isDefault} onChange={(e) => set('isDefault', e.target.checked)}/>
            Pre-selezionato in "Aggiungi membro"
          </label>
        </FormField>
        <FormField label="Display order" hint="minore = più in alto">
          <input type="number" min="0" max="9999" value={form.displayOrder} onChange={(e) => set('displayOrder', e.target.value)}/>
        </FormField>
      </FormGrid>
    </Modal>
  );
}

function ProjectRoleDetailModal({ item, onClose }) {
  const { user, pushToast, seedCustom } = useStore();
  const [form, setForm] = React.useState({
    name: item.name,
    description: item.description || '',
    capabilities: Array.isArray(item.capabilities) ? item.capabilities : [],
    suggestedRoleIds: Array.isArray(item.suggestedRoleIds) ? item.suggestedRoleIds : [],
    isDefault: !!item.isDefault,
    displayOrder: item.displayOrder ?? 0,
  });
  const [saving, setSaving] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));

  const submit = async () => {
    setSaving(true); setErr(null);
    try {
      const r = await fetch(`/api/config/project-roles/${item.id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', 'X-Actor-Persona-Id': user?.id || '' },
        body: JSON.stringify({
          name: form.name.trim(),
          description: form.description.trim() || null,
          capabilities: form.capabilities.filter(Boolean),
          suggestedRoleIds: form.suggestedRoleIds.filter(Boolean),
          isDefault: !!form.isDefault,
          displayOrder: Number(form.displayOrder) || 0,
        }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { setErr(j?.detail || j?.error || `HTTP ${r.status}`); setSaving(false); return; }
      pushToast({ title: 'Ruolo aggiornato', desc: `${item.code} salvato`, tone: 'ok' });
      onClose();
    } catch (e) { setErr(e?.message || 'rete'); setSaving(false); }
  };

  return (
    <Modal open={!!item} onClose={onClose} size="md" title={`Ruolo · ${item.code}`} footer={
      <>
        <Btn variant="ghost" size="sm" onClick={onClose} disabled={saving}>Annulla</Btn>
        <span className="spacer"/>
        <Btn variant="primary" size="sm" onClick={submit} disabled={saving}>{saving ? 'Salvataggio…' : 'Salva'}</Btn>
      </>
    }>
      {err && <div style={{ marginBottom: 10, padding: 8, color: 'var(--err)', border: '1px solid var(--err)', borderRadius: 4, fontSize: 11.5 }}>{err}</div>}
      <FormGrid cols={2}>
        <FormField label="Code" cols={2}>
          <input className="mono" value={item.code} disabled style={{ opacity: 0.6 }}/>
          <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>Il code non si modifica (audit trail).</div>
        </FormField>
        <FormField label="Nome" required cols={2}>
          <input value={form.name} onChange={(e) => set('name', e.target.value)}/>
        </FormField>
        <FormField label="Descrizione" cols={2}>
          <textarea rows={2} value={form.description} onChange={(e) => set('description', e.target.value)}/>
        </FormField>
        <FormField label="Capabilities (csv)" hint="suggerimenti UX" cols={2}>
          <input
            value={form.capabilities.join(',')}
            onChange={(e) => set('capabilities', e.target.value.split(',').map(s => s.trim()).filter(Boolean))}
          />
        </FormField>
        <FormField label="Ruoli di sistema suggeriti" hint="pre-rank: chi ha questi ruoli sale in cima nella ricerca persona del Team (mai un filtro rigido)" cols={2}>
          <DocTypeSignerRolesPicker
            value={form.suggestedRoleIds}
            seedCustom={seedCustom}
            onChange={(v) => set('suggestedRoleIds', v)}
            placeholder="+ aggiungi ruolo suggerito…"
          />
        </FormField>
        <FormField label="Default in UI">
          <label className="row" style={{ gap: 6, fontSize: 12 }}>
            <input type="checkbox" checked={form.isDefault} onChange={(e) => set('isDefault', e.target.checked)}/>
            Pre-selezionato
          </label>
        </FormField>
        <FormField label="Display order">
          <input type="number" min="0" max="9999" value={form.displayOrder} onChange={(e) => set('displayOrder', e.target.value)}/>
        </FormField>
        <FormField label="In uso da" cols={2}>
          <div style={{ fontSize: 12 }}>{item.usageCount ?? 0} membri attivi</div>
        </FormField>
      </FormGrid>
    </Modal>
  );
}

Object.assign(window, {
  CustDocTypes, CustChecklists, CustWorkflows, CustMatrix, CustRoles, NewRoleModal,
  CustProjectRoles, NewProjectRoleModal, ProjectRoleDetailModal,
  formatCondition, renderConditionTree,
});
