/* ============================================================
   COMPONENTS MODULE
   Three tabs: the component list itself, the architecture map (moved in
   from its own top-level nav entry), and configuration (the classification
   lists moved in from the Directory module).
   ============================================================ */
function ComponentsModule({state, idx, go, canCreate, onNew, act, toast, route}){
  const [tab, setTab] = useState('list');
  useEffect(()=>{ if (route && route.tab) setTab(route.tab); }, [route]);

  return (
    <div className="mod">
      <div className="mod-head">
        <div className="tabs" style={{margin:0}}>
          <button className={'tab' + (tab==='list'?' active':'')} onClick={()=>setTab('list')}>
            <I.grid size={15}/>Components <span className="n">{state.components.length}</span></button>
          <button className={'tab' + (tab==='architecture'?' active':'')} onClick={()=>setTab('architecture')}>
            <I.layers size={15}/>Architecture map</button>
          <button className={'tab' + (tab==='configuration'?' active':'')} onClick={()=>setTab('configuration')}>
            <I.gear size={15}/>Configuration</button>
        </div>
      </div>

      {tab === 'list' &&
        <ComponentList state={state} idx={idx} go={go} canCreate={canCreate} onNew={onNew} act={act} toast={toast}/>}
      {tab === 'architecture' &&
        <div className="mod-body"><div className="mod-scroll">
          <ArchitectureMap state={state} idx={idx} go={go}/>
        </div></div>}
      {tab === 'configuration' &&
        <div className="mod-body"><div className="mod-scroll">
          <ComponentsConfiguration state={state} idx={idx} act={act} toast={toast}/>
        </div></div>}
    </div>
  );
}

/* ============================================================
   COMPONENT LIST
   ============================================================ */
function ComponentList({state, idx, go, onNew, canCreate, act, toast}){
  const [q, setQ] = useState(()=>urlParam('q'));
  const [fType, setFType] = useState(()=>urlParam('type'));
  const [fStatus, setFStatus] = useState(()=>urlParam('status'));
  const [fTier, setFTier] = useState(()=>urlParam('tier'));
  const [fOwner, setFOwner] = useState(()=>urlParam('owner'));
  const {out, inc} = useMemo(()=>depGraph(state), [state]);
  const {menu, openMenu, closeMenu} = useRecordMenu();
  const {sort, sorted, Th} = useSort('name', {
    identifier: c => c.identifier || '',
    name: c => c.name,
    type: c => (idx.type[c.typeId]||{}).name || '',
    tier: c => c.tier,
    status: c => c.status,
    owner: c => { const r = resolveParty(c.owners && c.owners.business, idx); return r ? r.name : 'zzz'; },
    deps: c => (out[c.id]||[]).length,
    dependants: c => (inc[c.id]||[]).length,
    updated: c => c.updatedAt || ''
  });
  useFilterUrlSync({q:[q,''], type:[fType,''], status:[fStatus,''], tier:[fTier,''], owner:[fOwner,'']});

  const rows = useMemo(()=>{
    const term = q.trim().toLowerCase();
    const list = state.components.filter(c => {
      if (fType && c.typeId !== fType) return false;
      if (fStatus && c.status !== fStatus) return false;
      if (fTier && String(c.tier) !== fTier) return false;
      if (fOwner){
        const has = OWNER_ROLES.some(o => partyKey(c.owners && c.owners[o.key]) === fOwner)
          || (c.stakeholders||[]).some(s => partyKey(s.party) === fOwner)
          || (c.raci||[]).some(r => partyKey(r.party) === fOwner);
        if (!has) return false;
      }
      if (!term) return true;
      const hay = [c.name, c.identifier, c.description, c.vendor, c.businessCapability,
        (c.tags||[]).join(' '), (idx.type[c.typeId]||{}).name,
        ...OWNER_ROLES.map(o => { const r = resolveParty(c.owners && c.owners[o.key], idx); return r ? r.name : ''; })
      ].join(' ').toLowerCase();
      return hay.includes(term);
    });
    return sorted(list);
  }, [state, q, fType, fStatus, fTier, fOwner, sort, idx, out, inc]);

  const exportCsv = () => {
    const head = ['Identifier','Name','Type','Status','Tier','Executive Owner','Business Owner',
      'Technical Owner','System Administrator','Vendor','Environment','Depends on','Depended on by',
      'Links','Documents','Notes','Attachments','Last reviewed','Updated'];
    const lines = [head.join(',')].concat(rows.map(c => [
      c.identifier, c.name, (idx.type[c.typeId]||{}).name, c.status, c.tier,
      ...OWNER_ROLES.map(o => { const r = resolveParty(c.owners && c.owners[o.key], idx); return r ? r.name : ''; }),
      c.vendor, c.environment, (out[c.id]||[]).length, (inc[c.id]||[]).length,
      (c.toolIds||[]).length, (c.documentIds||[]).length, (c.componentNotes||[]).length, (c.attachments||[]).length,
      c.lastReviewed, c.updatedAt
    ].map(csvEscape).join(',')));
    download('fuse-components.csv', lines.join('\n'), 'text/csv');
  };

  const ownerOptions = [
    ...state.people.map(p=>({k:'person:'+p.id, label:p.name})),
    ...state.roles.map(r=>({k:'role:'+r.id, label:r.name + ' (role)'}))
  ].sort((a,b)=>a.label.localeCompare(b.label));

  const active = state.components.filter(c => c.status !== 'Archived');
  const tier1 = active.filter(c => c.tier === 1).length;
  const gaps = active.filter(c => OWNER_ROLES.some(o => !c.owners[o.key])).length;
  const neverReviewed = active.filter(c => !c.lastReviewed).length;

  return (
    <>
      <div className="mod-head">
        <div className="stats-row">
          <div className="stat"><div className="n">{state.components.length}</div><div className="l">Components</div></div>
          <div className="stat"><div className="n" style={{color: tier1 ? 'var(--red)' : undefined}}>{tier1}</div>
            <div className="l">Tier 1 active</div></div>
          <div className="stat"><div className="n" style={{color: gaps ? 'var(--amber)' : undefined}}>{gaps}</div>
            <div className="l">Ownership gaps</div></div>
          <div className="stat"><div className="n" style={{color: neverReviewed ? 'var(--amber)' : undefined}}>{neverReviewed}</div>
            <div className="l">Never reviewed</div></div>
          <div className="stat"><div className="n">{state.components.length - active.length}</div>
            <div className="l">Archived</div></div>
        </div>

        <div className="row wrap">
          <div className="spacer"/>
          <button className="btn" onClick={exportCsv}><I.down size={14}/>CSV</button>
        </div>

        <div className="row wrap">
          <SearchBox value={q} onChange={setQ} aria="Search components"
                     title="Search name, identifier, owner, vendor or tag"/>
          <select style={{width:'auto'}} value={fType} onChange={e=>setFType(e.target.value)}>
            <option value="">All types</option>
            {state.componentTypes.map(t=><option key={t.id} value={t.id}>{t.name}</option>)}
          </select>
          <select style={{width:'auto'}} value={fStatus} onChange={e=>setFStatus(e.target.value)}>
            <option value="">All statuses</option>{STATUSES.map(s=><option key={s}>{s}</option>)}
          </select>
          <select style={{width:'auto'}} value={fTier} onChange={e=>setFTier(e.target.value)}>
            <option value="">All tiers</option>{TIERS.map(t=><option key={t} value={t}>Tier {t}</option>)}
          </select>
          <select style={{width:'auto',maxWidth:210}} value={fOwner} onChange={e=>setFOwner(e.target.value)}>
            <option value="">Anyone involved</option>
            {ownerOptions.map(o=><option key={o.k} value={o.k}>{o.label}</option>)}
          </select>
          {(q||fType||fStatus||fTier||fOwner) &&
            <button className="btn ghost sm" onClick={()=>{setQ('');setFType('');setFStatus('');setFTier('');setFOwner('');}}>Clear</button>}
          <div className="spacer"/>
          <span className="xsmall dim">{rows.length} shown</span>
        </div>
      </div>

      <div className="mod-body">
      <div className="card">
        <div className="card-head">
          <span className="xsmall dim">Click any row to open · right-click to copy a link</span>
        </div>
        <div className="tbl-wrap">
          <table>
            <thead><tr>
              <Th k="identifier" label="ID"/>
              <Th k="name" label="Component"/>
              <Th k="type" label="Type"/>
              <Th k="tier" label="Tier"/>
              <Th k="status" label="Status"/>
              <Th k="owner" label="Business owner"/>
              <th>Technical owner</th>
              <Th k="deps" label="Depends on"/>
              <Th k="dependants" label="Dependants"/>
              <th>Content</th>
            </tr></thead>
            <tbody>
              {rows.map(c => {
                const t = idx.type[c.typeId];
                const TIcon = typeIconFor(t);
                const bo = resolveParty(c.owners && c.owners.business, idx);
                const to = resolveParty(c.owners && c.owners.technical, idx);
                return (
                  <tr key={c.id} className="clickable" onClick={()=>go({view:'component', id:c.id})}
                      onContextMenu={(e)=>openMenu(e, recordMenuItem('component', c))}>
                    <td className="mono dim">{c.identifier}</td>
                    <td>
                      <div className="row" style={{gap:9}}>
                        <span style={{color:'var(--text-3)'}}><TIcon size={15}/></span>
                        <span style={{minWidth:0}}>
                          <span className="cell-name" style={{display:'block'}}>{c.name}</span>
                          {c.vendor && <span className="cell-sub">{c.vendor}</span>}
                        </span>
                      </div>
                    </td>
                    <td className="small muted">{t ? t.name : '—'}</td>
                    <td><TierBadge tier={c.tier}/></td>
                    <td><Badge tone={statusTone(c.status)} dot>{c.status}</Badge></td>
                    <td className="small">{bo ? bo.name : <span className="dim">Not assigned</span>}</td>
                    <td className="small">{to ? to.name : <span className="dim">Not assigned</span>}</td>
                    <td className="small mono">{(out[c.id]||[]).length}</td>
                    <td className="small mono">{(inc[c.id]||[]).length}</td>
                    <td>
                      <div className="row" style={{gap:9,color:'var(--text-3)',fontSize:11.5}}>
                        <span title={`${(c.toolIds||[]).length} tools & links`}><I.link size={12}/> {(c.toolIds||[]).length}</span>
                        <span title={`${(c.documentIds||[]).length} documents`}><I.book size={12}/> {(c.documentIds||[]).length}</span>
                        <span title={`${(c.componentNotes||[]).length} notes`}><I.note size={12}/> {(c.componentNotes||[]).length}</span>
                        <span title={`${(c.attachments||[]).length} attachments`}><I.paper size={12}/> {(c.attachments||[]).length}</span>
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
          {!rows.length &&
            <EmptyState icon={<I.search size={28}/>} title="No components match"
              body={(q||fType||fStatus||fTier||fOwner) ? 'Try clearing the filters.' : 'Nothing here yet.'}
              action={(q||fType||fStatus||fTier||fOwner)
                ? <button className="btn sm" onClick={()=>{setQ('');setFType('');setFStatus('');setFTier('');setFOwner('');}}>
                    Clear filters</button>
                : canCreate ? <button className="btn primary sm" onClick={onNew}>Add the first component</button> : null}/>}
        </div>
      </div>
      </div>
      <RecordContextMenu menu={menu} onClose={closeMenu} toast={toast}/>
    </>
  );
}


/* ============================================================
   ARCHITECTURE MAP
   ============================================================ */
function ArchitectureMap({state, idx, go}){
  const [fTier, setFTier] = useState('');
  const [hideArchived, setHideArchived] = useState(true);
  const [fType, setFType] = useState('');
  const filterFn = useCallback((c)=>{
    if (hideArchived && c.status === 'Archived') return false;
    if (fTier && String(c.tier) !== fTier) return false;
    if (fType && c.typeId !== fType) return false;
    return true;
  }, [fTier, hideArchived, fType]);

  const {nodes, links} = useMemo(()=>buildGraphData(state, idx, null, 0, filterFn), [state, idx, filterFn]);
  const maxLayer = nodes.length ? Math.max(...nodes.map(n=>n.layer)) : 0;
  const layerGroups = [];
  for (let l = maxLayer; l >= 0; l--){
    const g = nodes.filter(n=>n.layer===l);
    if (g.length) layerGroups.push({l, g});
  }

  return (
    <div className="stack">
      <div className="row wrap">
        <select style={{width:'auto'}} value={fTier} onChange={e=>setFTier(e.target.value)}>
          <option value="">All tiers</option>{TIERS.map(t=><option key={t} value={t}>Tier {t} only</option>)}
        </select>
        <select style={{width:'auto'}} value={fType} onChange={e=>setFType(e.target.value)}>
          <option value="">All types</option>
          {state.componentTypes.map(t=><option key={t.id} value={t.id}>{t.name}</option>)}
        </select>
        <button className={'chip-toggle' + (hideArchived?' on':'')} onClick={()=>setHideArchived(v=>!v)}>
          Hide archived</button>
        <div className="spacer"/>
        <span className="xsmall dim">{nodes.length} components · {links.length} relationships</span>
      </div>

      <DependencyGraph nodes={nodes} links={links} height={540} onSelect={(id)=>go({view:'component',id})}/>

      <div className="card">
        <div className="card-head"><h3>Dependency layers</h3>
          <span className="xsmall dim">Layer 0 depends on nothing else in the model</span></div>
        <div className="card-body">
          {layerGroups.map(({l,g}) =>
            <div key={l} style={{marginBottom:14}}>
              <div className="sub-head" style={{margin:'0 0 7px'}}>Layer {l} · {g.length}</div>
              <div className="pill-list">
                {[...g].sort(byName).map(n =>
                  <span key={n.id} className="chip-toggle" onClick={()=>go({view:'component',id:n.id})}
                        style={{cursor:'pointer'}}>
                    <TierBadge tier={n.tier}/> <span style={{marginLeft:5}}>{n.name}</span>
                  </span>)}
              </div>
            </div>)}
          {!layerGroups.length && <EmptyState icon={<I.layers size={28}/>} title="Nothing to show"/>}
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   COMPONENT DETAIL
   ============================================================ */
function ComponentDetail({id, state, idx, go, act, toast, onEdit, canEdit}){
  const [tab, setTab] = useState('overview');
  const [depth, setDepth] = useState(1);
  const [dlg, setDlg] = useState(null);
  const [detailTargetId, setDetailTargetId] = useState(null);
  const comp = idx.comp[id];
  const {out, inc} = useMemo(()=>depGraph(state), [state]);
  const graph = useMemo(
    ()=> comp ? buildGraphData(state, idx, id, depth) : {nodes:[], links:[]},
    [state, idx, id, depth, !!comp]);

  useEffect(()=>{ setTab('overview'); }, [id]);

  if (!comp) return <EmptyState icon={<I.warn size={30}/>} title="Component not found"
    body="It may have been deleted by someone else."
    action={<button className="btn" onClick={()=>go({view:'components'})}>Back to the list</button>}/>;

  const editable = canEdit(comp.id);
  const type = idx.type[comp.typeId];
  const TIcon = typeIconFor(type);
  const stakeholders = allStakeholders(comp, idx);
  const namedStakeholders = stakeholders.filter(s => s.resolved);
  const contacts = (comp.contactIds||[]).map(cid => idx.contact[cid]).filter(Boolean);
  const dependsOn = out[comp.id] || [];
  const dependants = inc[comp.id] || [];
  const base = `/api/components/${comp.id}`;
  // Documents and requests live in their own modules and point back here.
  const documents = state.documents.filter(d => (d.componentIds||[]).includes(comp.id));
  const tools = state.tools.filter(t => (t.componentIds||[]).includes(comp.id));
  const requests = state.requests.filter(r => (r.componentIds||[]).includes(comp.id));
  const changes = state.changes.filter(ch => (ch.componentIds||[]).includes(comp.id));
  const openRequests = requests.filter(r => !['Done','Rejected'].includes(r.status));
  const monitorTargets = (state.monitorTargets||[]).filter(t => t.componentId === comp.id);

  const sideRows = [
    ['Identifier', <span className="mono">{comp.identifier || '—'}</span>],
    ['Type', type ? type.name : '—'],
    ['Status', <Badge tone={statusTone(comp.status)} dot>{comp.status}</Badge>],
    ['Tier', <span className="row" style={{gap:6,justifyContent:'flex-end'}}>
      <TierBadge tier={comp.tier}/><span className="xsmall dim">{TIER_LABEL[comp.tier]}</span></span>],
    ['Environment', comp.environment || '—'],
    ['Vendor', comp.vendor || '—'],
    ['Capability', comp.businessCapability || '—'],
    ['Hosting', comp.hostingLocation || '—'],
    ['Recovery', comp.recoveryTier || '—'],
    ['Cost centre', comp.costCentre || '—'],
    ['Live since', fmtDate(comp.goLiveDate) || '—'],
    ['Last reviewed', fmtDate(comp.lastReviewed) || <span style={{color:'var(--red)'}}>Never</span>]
  ];

  const TABS = [
    {k:'overview',      label:'Overview',      icon:<I.book size={15}/>},
    {k:'stakeholders',  label:'Stakeholders',  icon:<I.users size={15}/>, n:namedStakeholders.length},
    {k:'raci',          label:'RACI',          icon:<I.raci size={15}/>,  n:(comp.raci||[]).length},
    {k:'contacts',      label:'Contacts',      icon:<I.phone size={15}/>, n:contacts.length},
    {k:'links',         label:'Links',         icon:<I.link size={15}/>,  n:tools.length},
    {k:'documents',     label:'Documentation', icon:<I.book size={15}/>,  n:documents.length},
    {k:'versions',      label:'Versions',      icon:<I.tag size={15}/>,  n:(comp.versions||[]).length},
    {k:'requests',      label:'Requests',      icon:<I.inbox size={15}/>, n:requests.length},
    {k:'changes',       label:'Changes',       icon:<I.change size={15}/>, n:changes.length},
    {k:'notes',         label:'Notes',         icon:<I.note size={15}/>,  n:(comp.componentNotes||[]).length},
    {k:'attachments',   label:'Attachments',   icon:<I.paper size={15}/>, n:(comp.attachments||[]).length},
    {k:'architecture',  label:'Architecture',  icon:<I.graph size={15}/>, n:dependsOn.length + dependants.length}
  ];

  const shared = {comp, state, idx, act, toast, base, editable, go};

  return (
    <div>
      <div className="page-head">
        <div className="type-icon"><TIcon size={20}/></div>
        <div style={{minWidth:0,flex:1}}>
          <div className="row wrap" style={{gap:9,marginBottom:2}}>
            <h1>{comp.name}</h1>
            <TierBadge tier={comp.tier} showLabel/>
            <Badge tone={statusTone(comp.status)} dot>{comp.status}</Badge>
            {!editable && <Badge tone="slate">Read only</Badge>}
          </div>
          <div className="small muted">
            <span className="mono">{comp.identifier}</span>
            {type && <> · {type.name}</>}
            {comp.businessCapability && <> · {comp.businessCapability}</>}
            {comp.updatedAt && <> · updated {relative(comp.updatedAt)} by {comp.updatedBy}</>}
          </div>
        </div>
        {editable && <button className="btn" onClick={()=>onEdit(comp)}><I.edit size={14}/>Edit</button>}
        {state.permissions.canCreateComponents &&
          <button className="btn danger" onClick={()=>setDlg({t:'delete'})} title="Delete component"><I.trash size={14}/></button>}
      </div>

      <div className="detail">
        <div>
          <div className="tabs">
            {TABS.map(t =>
              <button key={t.k} className={'tab'+(tab===t.k?' active':'')} onClick={()=>setTab(t.k)}>
                {t.icon}{t.label}{t.n != null && <span className="n">{t.n}</span>}
              </button>)}
          </div>

          {tab === 'overview' && <OverviewTab comp={comp} idx={idx} out={out} inc={inc}/>}
          {tab === 'stakeholders' && <StakeholdersTab {...shared} rows={stakeholders} onEditOwners={()=>onEdit(comp)}/>}
          {tab === 'raci' && <RaciTab {...shared}/>}
          {tab === 'contacts' && <ContactsTab {...shared} contacts={contacts}/>}
          {tab === 'links' && <ComponentToolsTab {...shared} tools={tools}/>}
          {tab === 'documents' && <ComponentDocsTab {...shared} documents={documents}/>}
          {tab === 'versions' && <VersionsTab {...shared}/>}
          {tab === 'requests' && <ComponentRequestsTab {...shared} requests={requests}/>}
          {tab === 'changes' && <ComponentChangesTab {...shared} changes={changes}/>}
          {tab === 'notes' && <NotesTab {...shared}/>}
          {tab === 'attachments' &&
            <AttachmentPanel attachments={comp.attachments} base={base} editable={editable}
              act={act} toast={toast}/>}
          {tab === 'architecture' &&
            <ArchitectureTab {...shared} graph={graph} depth={depth} setDepth={setDepth}
              dependsOn={dependsOn} dependants={dependants}/>}
        </div>

        <div className="stack">
          <div className="card">
            <div className="card-head"><h3>Ownership</h3></div>
            <div>
              {OWNER_ROLES.map(o => {
                const r = resolveParty(comp.owners && comp.owners[o.key], idx);
                return (
                  <div className="owner-row" key={o.key}>
                    {r ? <Avatar name={r.name} kind={r.kind}/> :
                      <span className="avatar" style={{background:'#dde3ea',color:'#8a94a3'}}>—</span>}
                    <div className="owner-meta">
                      <div className="owner-role">{o.label}</div>
                      <div className={'owner-name' + (r?'':' vacant')}>{r ? r.name : 'Not assigned'}</div>
                      {r && <div className="xsmall dim" style={{overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{r.sub}</div>}
                    </div>
                    {r && r.email && <a className="icon-btn" href={'mailto:'+r.email} title={'Email ' + r.name}><I.mail size={15}/></a>}
                  </div>
                );
              })}
            </div>
          </div>

          <div className="card">
            <div className="card-head"><h3>Availability</h3></div>
            {monitorTargets.length === 0
              ? <div className="card-body">
                  <span className="xsmall dim">Not monitored. </span>
                  <a className="clink" style={{cursor:'pointer'}}
                    onClick={()=>go({view:'observability', tab:'availability'})}>Set up monitoring</a>
                </div>
              : <div className="side-list">
                  {monitorTargets.map(t => (
                    <div className="side-row clickable" key={t.id} onClick={()=>setDetailTargetId(t.id)}>
                      <span className="k" style={{minWidth:0}}>
                        <span style={{display:'block',fontWeight:600,color:'var(--text)'}}>{t.toolName}</span>
                        <span className="xsmall dim">
                          {t.enabled ? (t.checkedAt ? relative(t.checkedAt) : 'No checks yet') : 'Disabled'}
                          {t.uptimePct != null ? ` · ${t.uptimePct}% (30d)` : ''}
                        </span>
                      </span>
                      <span className="v">{t.enabled && t.status
                        ? <Badge tone={t.status==='Up'?'green':t.status==='Degraded'?'amber':'red'} dot>{t.status}</Badge>
                        : <span className="dim xsmall">—</span>}</span>
                    </div>
                  ))}
                </div>}
          </div>

          <div className="card">
            <div className="card-head"><h3>Additional information</h3></div>
            <div className="side-list">
              {sideRows.map(([k,v]) =>
                <div className="side-row" key={k}><span className="k">{k}</span><span className="v">{v}</span></div>)}
            </div>
          </div>

          {(comp.tags||[]).length > 0 &&
            <div className="card">
              <div className="card-head"><h3>Tags</h3></div>
              <div className="card-body"><div className="pill-list">
                {comp.tags.map(t => <Badge key={t} tone="purple">{t}</Badge>)}</div></div>
            </div>}

          <div className="card">
            <div className="card-head"><h3>Record history</h3></div>
            <div className="side-list">
              <div className="side-row"><span className="k">Created</span>
                <span className="v small">{fmtDateTime(comp.createdAt)}<br/><span className="dim xsmall">{comp.createdBy}</span></span></div>
              <div className="side-row"><span className="k">Last updated</span>
                <span className="v small">{fmtDateTime(comp.updatedAt)}<br/><span className="dim xsmall">{comp.updatedBy}</span></span></div>
            </div>
          </div>
        </div>
      </div>

      {dlg && dlg.t === 'delete' &&
        <ConfirmDialog title="Delete component" onClose={()=>setDlg(null)}
          message={`Delete "${comp.name}"? Its relationships, links, notes, documents and uploaded files will be removed too. You'll have a few seconds to undo.`}
          onConfirm={()=>{ deleteWithUndo(act, toast, base, comp.name); go({view:'components'}); }}/>}

      {detailTargetId &&
        <MonitorTargetDetailModal targetId={detailTargetId} state={state} idx={idx} go={go}
          onClose={()=>setDetailTargetId(null)}/>}
    </div>
  );
}

function OverviewTab({comp, idx, out, inc}){
  const hard = (out[comp.id]||[]).filter(d=>d.criticality==='Hard');
  const accountable = (comp.raci||[]).filter(r => r.letter === 'A').length;
  return (
    <div className="stack">
      <div className="card">
        <div className="card-head"><h3>Description</h3></div>
        <div className="card-body">
          {comp.description
            ? <RichText value={comp.description}/>
            : <p style={{margin:0}} className="dim">No description recorded.</p>}
          {comp.notes && <><div className="divider"/>
            <div className="sub-head">Summary notes</div>
            <p style={{margin:0}} className="muted pre-wrap">{comp.notes}</p></>}
        </div>
      </div>

      <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit,minmax(170px,1fr))'}}>
        <div className="stat"><div className="n">{(out[comp.id]||[]).length}</div><div className="l">Depends on</div>
          <div className="xsmall dim" style={{marginTop:4}}>{hard.length} hard, {(out[comp.id]||[]).length-hard.length} soft</div></div>
        <div className="stat"><div className="n">{(inc[comp.id]||[]).length}</div><div className="l">Depended on by</div>
          <div className="xsmall dim" style={{marginTop:4}}>Impact if this fails</div></div>
        <div className="stat"><div className="n">{allStakeholders(comp, idx).filter(s=>s.resolved).length}</div>
          <div className="l">Stakeholders</div>
          <div className="xsmall dim" style={{marginTop:4}}>Including the four owners</div></div>
        <div className="stat"><div className="n" style={{color: accountable ? undefined : '#b3261e'}}>{accountable}</div>
          <div className="l">RACI activities</div>
          <div className="xsmall dim" style={{marginTop:4}}>with someone accountable</div></div>
      </div>

      <div className="card">
        <div className="card-head"><h3>At a glance</h3></div>
        <div className="card-body">
          <dl className="kv-block">
            <dt>Current version</dt><dd>{comp.currentVersion
              ? comp.currentVersion.version + (comp.currentVersion.releaseDate ? ' — ' + fmtDate(comp.currentVersion.releaseDate) : '')
              : <span className="dim">—</span>}</dd>
            <dt>Business capability</dt><dd>{comp.businessCapability || <span className="dim">—</span>}</dd>
            <dt>Vendor / supplier</dt><dd>{comp.vendor || <span className="dim">—</span>}</dd>
            <dt>Hosting location</dt><dd>{comp.hostingLocation || <span className="dim">—</span>}</dd>
            <dt>Environment</dt><dd>{comp.environment || <span className="dim">—</span>}</dd>
            <dt>Recovery objectives</dt><dd>{comp.recoveryTier || <span className="dim">—</span>}</dd>
            <dt>Cost centre</dt><dd>{comp.costCentre || <span className="dim">—</span>}</dd>
            <dt>Go live</dt><dd>{fmtDate(comp.goLiveDate) || <span className="dim">—</span>}</dd>
            <dt>Last reviewed</dt><dd>{fmtDate(comp.lastReviewed) || <span className="dim">Never reviewed</span>}</dd>
          </dl>
        </div>
      </div>
    </div>
  );
}

/* ------------------------------------------------------------ stakeholders */

function StakeholdersTab({comp, state, idx, act, base, editable, rows, onEditOwners}){
  const [dlg, setDlg] = useState(null);
  const [draft, setDraft] = useState({party:null, capacityId:'', notes:''});
  const [creating, setCreating] = useState(null); // null | 'person' | 'role'
  const capacities = state.capacities;
  const canEditDir = state.permissions.canEditDirectory;

  const start = (row) => {
    if (row){ setDraft({party:row.party, capacityId:row.capacityId, notes:row.notes}); setDlg({id:row.id}); }
    else { setDraft({party:null, capacityId:(capacities.find(c=>!c.isOwnerLevel)||{}).id||'', notes:''}); setDlg({id:null}); }
  };
  const save = async () => {
    if (!draft.party) return;
    const payload = {party:draft.party, capacityId:draft.capacityId, notes:draft.notes};
    await act(dlg.id ? API.put(`${base}/stakeholders/${dlg.id}`, payload)
                     : API.post(`${base}/stakeholders`, payload), 'Stakeholder saved');
    setDlg(null);
  };

  const grouped = {};
  rows.forEach(r => { (grouped[r.capacityName] = grouped[r.capacityName] || []).push(r); });
  const order = [...OWNER_ROLES.map(o=>o.label),
    ...capacities.map(c=>c.name).filter(n => !OWNER_ROLES.some(o=>o.label===n))];

  return (
    <div className="stack">
      <div className="card">
        <div className="card-head">
          <h3>Stakeholder register</h3><div className="spacer"/>
          {editable && <>
            <button className="btn sm" onClick={onEditOwners}><I.edit size={13}/>Change owners</button>
            <button className="btn sm primary" onClick={()=>start(null)}><I.plus size={13}/>Add stakeholder</button>
          </>}
        </div>
        <div className="tbl-wrap">
          <table>
            <thead><tr>
              <th style={{width:'32%'}}>Stakeholder</th><th style={{width:'20%'}}>Capacity</th>
              <th>Contact</th><th>Notes</th>{editable && <th style={{width:60}}></th>}
            </tr></thead>
            <tbody>
              {order.filter(cap => grouped[cap]).map(cap => grouped[cap].map(r => (
                <tr key={r.id}>
                  <td>{r.resolved ? <PartyChip resolved={r.resolved}/> : <span className="dim small">Not assigned</span>}</td>
                  <td>
                    <Badge tone={r.derived ? 'accent' : 'slate'}>{r.capacityName}</Badge>
                    {r.derived && <div className="xsmall dim" style={{marginTop:3}}>Owner</div>}
                  </td>
                  <td className="small">
                    {r.resolved && r.resolved.email
                      ? <a className="clink" href={'mailto:'+r.resolved.email}><I.mail size={12}/><span>{r.resolved.email}</span></a>
                      : <span className="dim">—</span>}
                  </td>
                  <td className="small muted">{r.notes || <span className="dim">—</span>}</td>
                  {editable && <td>{!r.derived &&
                    <div className="row" style={{gap:2}}>
                      <button className="icon-btn" onClick={()=>start(r)} title="Edit"><I.edit size={14}/></button>
                      <button className="icon-btn" title="Remove"
                        onClick={()=>act(API.del(`${base}/stakeholders/${r.id}`), 'Stakeholder removed')}><I.trash size={14}/></button>
                    </div>}</td>}
                </tr>
              )))}
            </tbody>
          </table>
        </div>
        <div className="card-body" style={{borderTop:'1px solid var(--border)',paddingTop:12}}>
          <span className="xsmall dim">
            The four owner rows always appear here and are edited on the component record.
            Any stakeholder can be a named individual or a role with a primary assignee.
          </span>
        </div>
      </div>

      {dlg &&
        <Modal title={dlg.id ? 'Edit stakeholder' : 'Add stakeholder'} onClose={()=>setDlg(null)}
          footer={<>
            <button className="btn" onClick={()=>setDlg(null)}>Cancel</button>
            <button className="btn primary" disabled={!draft.party} onClick={save}>{dlg.id ? 'Save' : 'Add'}</button>
          </>}>
          <Field label="Person or role" hint="Roles resolve to their primary assignee for contact details.">
            <div className="row" style={{gap:8}}>
              <div style={{flex:1,minWidth:0}}>
                <PartySelect value={draft.party} state={state} onChange={p=>setDraft(d=>({...d, party:p}))} placeholder="Choose…"/>
              </div>
              {canEditDir && <>
                <button type="button" className="btn sm" onClick={()=>setCreating('person')}>+ Person</button>
                <button type="button" className="btn sm" onClick={()=>setCreating('role')}>+ Role</button>
              </>}
            </div>
          </Field>
          <Field label="Stakeholder capacity">
            <select value={draft.capacityId||''} onChange={e=>setDraft(d=>({...d, capacityId:e.target.value}))}>
              {capacities.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
            </select>
          </Field>
          <Field label="Notes">
            <textarea value={draft.notes} onChange={e=>setDraft(d=>({...d, notes:e.target.value}))}
              placeholder="What is their involvement?"/>
          </Field>
        </Modal>}

      {creating === 'person' &&
        <PersonForm value={NEW_PERSON()} state={state}
          onClose={()=>setCreating(null)}
          onSave={async (v)=>{
            let created = null;
            const ok = await act((async()=>{ created = await API.post('/api/people', v); })(), 'Person added');
            if (ok){ setDraft(d=>({...d, party:{kind:'person', id:created.id}})); setCreating(null); }
          }}/>}
      {creating === 'role' &&
        <RoleForm value={{name:'',description:'',primaryPersonId:null}} state={state}
          onClose={()=>setCreating(null)}
          onSave={async (v)=>{
            let created = null;
            const ok = await act((async()=>{ created = await API.post('/api/roles', v); })(), 'Role added');
            if (ok){ setDraft(d=>({...d, party:{kind:'role', id:created.id}})); setCreating(null); }
          }}/>}
    </div>
  );
}

/* -------------------------------------------------------------------- RACI */

function RaciTab({comp, state, idx, act, base, editable}){
  const [dlg, setDlg] = useState(null);
  const [draft, setDraft] = useState({activityId:'', party:null, letter:'R', notes:''});
  const activities = state.raciActivities;
  const rows = comp.raci || [];

  const byActivity = {};
  rows.forEach(r => { (byActivity[r.activityId] = byActivity[r.activityId] || []).push(r); });
  const ORDER = {A:0, R:1, C:2, I:3};

  const open = (activityId) => {
    setDraft({activityId: activityId || (activities[0]||{}).id || '', party:null, letter:'R', notes:''});
    setDlg(true);
  };
  const save = async () => {
    if (!draft.party || !draft.activityId) return;
    await act(API.post(`${base}/raci`, draft), 'RACI updated');
    setDlg(null);
  };

  const used = activities.filter(a => byActivity[a.id]);
  const unused = activities.filter(a => !byActivity[a.id]);

  return (
    <div className="stack">
      <div className="card">
        <div className="card-head">
          <h3>RACI matrix</h3>
          <div className="raci-legend" style={{marginLeft:8}}>
            {RACI_LETTERS.map(l =>
              <span key={l.k} className="row" style={{gap:5}}><RaciLetter letter={l.k}/>{l.name}</span>)}
          </div>
          <div className="spacer"/>
          {editable && <button className="btn sm primary" onClick={()=>open(null)}><I.plus size={13}/>Add assignment</button>}
        </div>

        {used.length === 0
          ? <EmptyState icon={<I.raci size={28}/>} title="No RACI assignments yet"
              body="Record who is responsible, accountable, consulted and informed for each activity."
              action={editable ? <button className="btn primary" onClick={()=>open(null)}>Add the first assignment</button> : null}/>
          : <div className="tbl-wrap">
              <table>
                <thead><tr>
                  <th style={{width:'26%'}}>Activity</th>
                  <th style={{width:'22%'}}>Accountable</th>
                  <th style={{width:'22%'}}>Responsible</th>
                  <th>Consulted &amp; informed</th>
                  {editable && <th style={{width:50}}></th>}
                </tr></thead>
                <tbody>
                  {used.map(a => {
                    const list = [...byActivity[a.id]].sort((x,y)=>ORDER[x.letter]-ORDER[y.letter]);
                    const cell = (letters) => {
                      const items = list.filter(r => letters.includes(r.letter));
                      if (!items.length) return <span className="dim small">—</span>;
                      return (
                        <div className="raci-cell">
                          {items.map(r => {
                            const p = resolveParty(r.party, idx);
                            return (
                              <span className="raci-assignee" key={r.id} title={r.notes || undefined}>
                                <RaciLetter letter={r.letter}/>
                                {p ? <Avatar name={p.name} kind={p.kind} size="sm"/> : null}
                                <span className="nm">{p ? p.name : 'Unknown'}</span>
                                {editable &&
                                  <button className="icon-btn rm" title="Remove"
                                    onClick={()=>act(API.del(`${base}/raci/${r.id}`), 'Assignment removed')}>
                                    <I.x size={12}/></button>}
                              </span>
                            );
                          })}
                        </div>
                      );
                    };
                    return (
                      <tr key={a.id}>
                        <td>
                          <div className="raci-activity">{a.name}</div>
                          {a.description && <div className="cell-sub">{a.description}</div>}
                        </td>
                        <td>{cell(['A'])}</td>
                        <td>{cell(['R'])}</td>
                        <td>{cell(['C','I'])}</td>
                        {editable && <td>
                          <button className="icon-btn" title="Add to this activity" onClick={()=>open(a.id)}>
                            <I.plus size={14}/></button>
                        </td>}
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>}

        {unused.length > 0 &&
          <div className="card-body" style={{borderTop:'1px solid var(--border)'}}>
            <div className="sub-head">Activities with nobody assigned</div>
            <div className="pill-list">
              {unused.map(a =>
                <span key={a.id} className="chip-toggle" style={{cursor: editable ? 'pointer' : 'default'}}
                      onClick={()=> editable && open(a.id)}>{a.name}{editable && ' +'}</span>)}
            </div>
          </div>}
      </div>

      {dlg &&
        <Modal title="Add RACI assignment" onClose={()=>setDlg(null)}
          footer={<>
            <button className="btn" onClick={()=>setDlg(null)}>Cancel</button>
            <button className="btn primary" disabled={!draft.party || !draft.activityId} onClick={save}>Save</button>
          </>}>
          <Field label="Activity">
            <select value={draft.activityId} onChange={e=>setDraft(d=>({...d, activityId:e.target.value}))}>
              {activities.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
            </select>
          </Field>
          <Field label="Person or role">
            <PartySelect value={draft.party} state={state} onChange={p=>setDraft(d=>({...d, party:p}))} placeholder="Choose…"/>
          </Field>
          <Field label="Responsibility" hint="Only one party can be Accountable per activity. Assigning a new one moves the previous holder to Consulted.">
            <div className="row wrap" style={{gap:8}}>
              {RACI_LETTERS.map(l =>
                <button key={l.k} type="button"
                  className={'chip-toggle' + (draft.letter===l.k?' on':'')}
                  onClick={()=>setDraft(d=>({...d, letter:l.k}))}>
                  {l.k} — {l.name}</button>)}
            </div>
          </Field>
          <Field label="Notes"><textarea value={draft.notes} onChange={e=>setDraft(d=>({...d, notes:e.target.value}))}/></Field>
        </Modal>}
    </div>
  );
}

/* ---------------------------------------------------------------- contacts */

function ContactsTab({comp, state, idx, act, base, editable, contacts}){
  const [picking, setPicking] = useState(false);
  const [creating, setCreating] = useState(false);
  const available = state.contacts.filter(c => !(comp.contactIds||[]).includes(c.id));
  const canEditDir = state.permissions.canEditDirectory;
  const grouped = {};
  contacts.forEach(c => { (grouped[c.category||'Other'] = grouped[c.category||'Other'] || []).push(c); });

  return (
    <div className="stack">
      <div className="card">
        <div className="card-head">
          <h3>Support &amp; vendor contacts</h3><div className="spacer"/>
          {editable && <button className="btn sm primary" onClick={()=>setPicking(true)}><I.plus size={13}/>Link a contact</button>}
        </div>
        <div className="card-body">
          {!contacts.length &&
            <EmptyState icon={<I.phone size={28}/>} title="No contacts linked"
              body="Link a service desk, vendor support line or account manager from the directory."/>}
          {Object.keys(grouped).sort().map(cat => (
            <React.Fragment key={cat}>
              <div className="sub-head">{cat}</div>
              <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit,minmax(268px,1fr))'}}>
                {grouped[cat].map(c => {
                  const linkedPerson = c.kind === 'person' && c.personId ? idx.person[c.personId] : null;
                  const linkedRole = c.kind === 'role' && c.roleId ? idx.role[c.roleId] : null;
                  const primary = linkedRole && linkedRole.primaryPersonId ? idx.person[linkedRole.primaryPersonId] : null;
                  return (
                    <div className="contact-card" key={c.id}>
                      <div className="row" style={{gap:10,alignItems:'flex-start'}}>
                        <Avatar name={c.name} kind={c.kind}/>
                        <div style={{minWidth:0,flex:1}}>
                          <div style={{fontWeight:650}}>{c.name}
                            {c.kind==='role' && <span className="badge accent" style={{marginLeft:6,fontSize:10}}>Role</span>}</div>
                          <div className="cell-sub">{c.organisation}{c.hours ? ' · ' + c.hours : ''}</div>
                          {primary && <div className="xsmall dim">Primary: {primary.name}</div>}
                          {linkedPerson && <div className="xsmall dim">{linkedPerson.jobTitle}</div>}
                        </div>
                        {editable &&
                          <button className="icon-btn" title="Unlink from this component"
                            onClick={()=>act(API.del(`${base}/contacts/${c.id}`), 'Contact unlinked')}><I.x size={14}/></button>}
                      </div>
                      <ContactLinks contact={c}/>
                      {c.notes && <div className="xsmall muted" style={{marginTop:9,paddingTop:9,borderTop:'1px solid var(--border)'}}>{c.notes}</div>}
                    </div>
                  );
                })}
              </div>
            </React.Fragment>
          ))}
        </div>
      </div>

      {picking &&
        <Modal title="Link contacts" onClose={()=>setPicking(false)}
          footer={<>
            {canEditDir && <button className="btn" onClick={()=>setCreating(true)}><I.plus size={13}/>New contact…</button>}
            <div className="spacer"/>
            <button className="btn primary" onClick={()=>setPicking(false)}>Done</button>
          </>}>
          {!available.length && <p className="dim">Every contact in the directory is already linked.</p>}
          {available.map(c =>
            <div key={c.id} className="row" style={{padding:'9px 0',borderBottom:'1px solid var(--border)'}}>
              <Avatar name={c.name} kind={c.kind}/>
              <div style={{flex:1,minWidth:0}}>
                <div style={{fontWeight:600}}>{c.name}</div>
                <div className="cell-sub">{c.organisation} · {c.category}</div>
              </div>
              <button className="btn sm" onClick={()=>act(API.post(`${base}/contacts/${c.id}`), 'Contact linked')}>Link</button>
            </div>)}
        </Modal>}

      {creating &&
        <ContactForm state={state} onClose={()=>setCreating(false)}
          value={{kind:'role', name:'', roleId:null, personId:null, organisation:'',
            category:(state.lookups.contact_category||[])[0]||'', phone:'', email:'', portalUrl:'', hours:'', notes:''}}
          onSave={async (v)=>{
            const ok = await act((async()=>{
              const made = await API.post('/api/contacts', v);
              await API.post(`${base}/contacts/${made.id}`);
            })(), 'Contact created and linked');
            if (ok){ setCreating(false); setPicking(false); }
          }}/>}
    </div>
  );
}

/* --------------------------------- links, held in the Tools & Services module */

function ComponentToolsTab({comp, state, idx, act, editable, go, tools}){
  const [creating, setCreating] = useState(false);
  const [picking, setPicking] = useState(false);
  const [selection, setSelection] = useState([]);
  const available = state.tools.filter(t => !(t.componentIds||[]).includes(comp.id));

  const grouped = {};
  tools.forEach(t => { (grouped[t.category || 'General'] = grouped[t.category || 'General'] || []).push(t); });

  return (
    <div className="stack">
      <div className="card">
        <div className="card-head">
          <h3>Links</h3><Badge tone="slate">{tools.length}</Badge>
          <span className="xsmall dim">
            Held in Tools &amp; Services, so the same link can serve several components
          </span>
          <div className="spacer"/>
          <button className="btn sm" onClick={()=>go({view:'tools'})}>Open Tools &amp; Services</button>
          {editable && <>
            <button className="btn sm" onClick={()=>{ setSelection([]); setPicking(true); }}>
              <I.link size={13}/>Link existing</button>
            <button className="btn sm primary" onClick={()=>setCreating(true)}>
              <I.plus size={13}/>Add link</button>
          </>}
        </div>
        {tools.length === 0
          ? <EmptyState icon={<I.link size={28}/>} title="No links yet"
              body="Add the consoles, dashboards and repositories people need when they work on this component."
              action={editable ? <button className="btn primary" onClick={()=>setCreating(true)}>
                Add the first link</button> : null}/>
          : <div>
              {Object.keys(grouped).sort().map(cat => (
                <div key={cat}>
                  <div className="sub-head" style={{padding:'12px 14px 6px',margin:0,
                    background:'var(--surface-2)',borderTop:'1px solid var(--border)',
                    borderBottom:'1px solid var(--border)'}}>{cat}</div>
                  {grouped[cat].map(t =>
                    <ToolRow key={t.id} tool={t} idx={idx} go={go} showComponents={false}
                      onOpen={()=>go({view:'tools'})}
                      onUnlink={editable
                        ? () => act(API.del(`/api/tools/${t.id}/components/${comp.id}`), 'Link removed')
                        : null}/>)}
                </div>
              ))}
            </div>}
      </div>

      {creating &&
        <ToolForm value={NEW_TOOL([comp.id])} state={state} lockedComponentId={comp.id}
          onClose={()=>setCreating(false)}
          onSave={async (v)=>{
            const ok = await act(API.post('/api/tools', {...v, componentIds:[comp.id]}), 'Link added');
            if (ok) setCreating(false);
          }}/>}

      {picking &&
        <Modal wide title="Link existing tools" onClose={()=>setPicking(false)}
          footer={<>
            <button className="btn" onClick={()=>setPicking(false)}>Cancel</button>
            <button className="btn primary" disabled={!selection.length}
              onClick={async ()=>{
                for (const id of selection) await API.post(`/api/tools/${id}/components/${comp.id}`);
                await act(Promise.resolve(), `${selection.length} link(s) added`);
                setPicking(false);
              }}>Link {selection.length || ''}</button>
          </>}>
          <p className="small muted" style={{marginTop:0}}>
            Pick from everything already in Tools &amp; Services. A tool can serve as many
            components as you need.
          </p>
          {!available.length && <p className="dim">Every tool is already linked to this component.</p>}
          <div className="multi-pick" style={{maxHeight:360}}>
            {available.map(t =>
              <label key={t.id}>
                <input type="checkbox" checked={selection.includes(t.id)}
                  onChange={()=>setSelection(s => s.includes(t.id) ? s.filter(x=>x!==t.id) : [...s, t.id])}/>
                <span style={{flex:1,minWidth:0}}>
                  <span className="truncate" style={{display:'block',fontWeight:600}}>{t.name}</span>
                  <span className="cell-sub truncate" style={{display:'block'}}>{t.primaryUrl}</span>
                </span>
                <Badge tone="slate">{t.category}</Badge>
              </label>)}
          </div>
        </Modal>}
    </div>
  );
}

/* ----------------------------------------------------------- documentation */

/* ------------------------------------------------------------------- notes */

function NotesTab({comp, state, act, base, editable}){
  const [dlg, setDlg] = useState(null);
  const [draft, setDraft] = useState({title:'', body:'', category:'General', isPinned:false});
  const [filter, setFilter] = useState('');
  const categories = state.lookups.note_category || ['General'];
  const notes = comp.componentNotes || [];
  const shown = filter ? notes.filter(n => n.category === filter) : notes;

  const open = (n) => {
    setDraft(n ? {title:n.title, body:n.body, category:n.category, isPinned:n.isPinned}
               : {title:'', body:'', category:categories[0]||'General', isPinned:false});
    setDlg({id: n ? n.id : null});
  };
  const save = async () => {
    await act(dlg.id ? API.put(`${base}/notes/${dlg.id}`, draft) : API.post(`${base}/notes`, draft), 'Note saved');
    setDlg(null);
  };

  return (
    <div className="stack">
      <div className="row wrap">
        <button className={'chip-toggle'+(filter===''?' on':'')} onClick={()=>setFilter('')}>All · {notes.length}</button>
        {categories.filter(c => notes.some(n => n.category === c)).map(c =>
          <button key={c} className={'chip-toggle'+(filter===c?' on':'')} onClick={()=>setFilter(c)}>
            {c} · {notes.filter(n=>n.category===c).length}</button>)}
        <div className="spacer"/>
        {editable && <button className="btn sm primary" onClick={()=>open(null)}><I.plus size={13}/>Add note</button>}
      </div>

      {shown.length === 0
        ? <div className="card"><EmptyState icon={<I.note size={28}/>} title="No notes"
            body="Capture decisions, risks, incidents and anything else worth remembering about this component."
            action={editable ? <button className="btn primary" onClick={()=>open(null)}>Write the first note</button> : null}/></div>
        : <div className="stack">
            {shown.map(n => (
              <div className={'note' + (n.isPinned ? ' pinned' : '')} key={n.id}>
                <div className="note-head">
                  {n.isPinned && <span style={{color:'var(--accent)'}} title="Pinned"><I.pin size={14}/></span>}
                  <span className="note-title">{n.title || 'Note'}</span>
                  <Badge tone="slate">{n.category}</Badge>
                  <div className="spacer"/>
                  {editable && <>
                    <button className="icon-btn" onClick={()=>open(n)} title="Edit"><I.edit size={14}/></button>
                    <button className="icon-btn" title="Delete"
                      onClick={()=>act(API.del(`${base}/notes/${n.id}`), 'Note deleted')}><I.trash size={14}/></button>
                  </>}
                </div>
                <div className="note-body">{n.body}</div>
                <div className="note-meta" style={{marginTop:9}}>
                  {n.createdBy} · {fmtDateTime(n.createdAt)}
                  {n.updatedAt !== n.createdAt && <> · edited {relative(n.updatedAt)} by {n.updatedBy}</>}
                </div>
              </div>
            ))}
          </div>}

      {dlg &&
        <Modal title={dlg.id ? 'Edit note' : 'Add note'} onClose={()=>setDlg(null)}
          footer={<>
            <button className="btn" onClick={()=>setDlg(null)}>Cancel</button>
            <button className="btn primary" disabled={!draft.body.trim()} onClick={save}>Save note</button>
          </>}>
          <div className="form-grid">
            <Field label="Title"><input type="text" value={draft.title} autoFocus
              onChange={e=>setDraft(d=>({...d,title:e.target.value}))} placeholder="Short summary"/></Field>
            <Field label="Category">
              <select value={draft.category} onChange={e=>setDraft(d=>({...d,category:e.target.value}))}>
                {categories.map(c => <option key={c}>{c}</option>)}
              </select></Field>
          </div>
          <Field label="Note">
            <textarea style={{minHeight:170}} value={draft.body}
              onChange={e=>setDraft(d=>({...d,body:e.target.value}))}/>
          </Field>
          <label className="row" style={{gap:8,cursor:'pointer'}}>
            <input type="checkbox" style={{width:'auto'}} checked={draft.isPinned}
              onChange={e=>setDraft(d=>({...d,isPinned:e.target.checked}))}/>
            <span className="small">Pin to the top of the list</span>
          </label>
        </Modal>}
    </div>
  );
}

/* --------------------------------------------------------------- versions */

function VersionsTab({comp, act, base, editable}){
  const [dlg, setDlg] = useState(null);
  const [draft, setDraft] = useState({version:'', releaseDate:'', releaseNotesUrl:''});
  const versions = comp.versions || [];

  const open = (v) => {
    setDraft(v ? {version:v.version, releaseDate:v.releaseDate, releaseNotesUrl:v.releaseNotesUrl}
               : {version:'', releaseDate:'', releaseNotesUrl:''});
    setDlg({id: v ? v.id : null});
  };
  const save = async () => {
    await act(dlg.id ? API.put(`${base}/versions/${dlg.id}`, draft) : API.post(`${base}/versions`, draft), 'Version saved');
    setDlg(null);
  };

  return (
    <div className="stack">
      <div className="card">
        <div className="card-head">
          <h3>Version history</h3><Badge tone="slate">{versions.length}</Badge><div className="spacer"/>
          {editable && <button className="btn sm primary" onClick={()=>open(null)}><I.plus size={13}/>Add version</button>}
        </div>
        {versions.length === 0
          ? <EmptyState icon={<I.tag size={28}/>} title="No versions recorded"
              body="Track release version numbers, dates and release notes as this component ships."
              action={editable ? <button className="btn primary" onClick={()=>open(null)}>Add the first version</button> : null}/>
          : <div className="tbl-wrap">
              <table>
                <thead><tr><th>Version</th><th>Released</th><th>Release notes</th>{editable && <th style={{width:60}}></th>}</tr></thead>
                <tbody>{versions.map((v,i) => (
                  <tr key={v.id}>
                    <td><span className="cell-name mono">{v.version}</span>
                      {i===0 && <Badge tone="accent" style={{marginLeft:8}}>Latest</Badge>}</td>
                    <td className="small muted">{fmtDate(v.releaseDate) || <span className="dim">—</span>}</td>
                    <td className="small">{v.releaseNotesUrl
                      ? <a className="clink" href={v.releaseNotesUrl} target="_blank" rel="noopener noreferrer">
                          <I.external size={12}/><span>Release notes</span></a>
                      : <span className="dim">—</span>}</td>
                    {editable && <td><div className="row" style={{gap:2}}>
                      <button className="icon-btn" onClick={()=>open(v)} title="Edit"><I.edit size={14}/></button>
                      <button className="icon-btn" title="Delete"
                        onClick={()=>act(API.del(`${base}/versions/${v.id}`), 'Version removed')}><I.trash size={14}/></button>
                    </div></td>}
                  </tr>
                ))}</tbody>
              </table>
            </div>}
      </div>

      {dlg &&
        <Modal title={dlg.id ? 'Edit version' : 'Add version'} onClose={()=>setDlg(null)}
          footer={<>
            <button className="btn" onClick={()=>setDlg(null)}>Cancel</button>
            <button className="btn primary" disabled={!draft.version.trim()} onClick={save}>Save</button>
          </>}>
          <div className="form-grid">
            <Field label="Version"><input type="text" value={draft.version} autoFocus
              onChange={e=>setDraft(d=>({...d,version:e.target.value}))} placeholder="e.g. 2.4.1"/></Field>
            <Field label="Release date"><input type="date" value={draft.releaseDate}
              onChange={e=>setDraft(d=>({...d,releaseDate:e.target.value}))}/></Field>
          </div>
          <Field label="Release notes URL" hint="Link to the changelog or release notes for this version.">
            <input type="url" value={draft.releaseNotesUrl}
              onChange={e=>setDraft(d=>({...d,releaseNotesUrl:e.target.value}))} placeholder="https://"/>
          </Field>
        </Modal>}
    </div>
  );
}

/* ------------------------------------------------------------ architecture */

function ArchitectureTab({comp, state, idx, act, go, editable, graph, depth, setDepth, dependsOn, dependants}){
  const [dlg, setDlg] = useState(null);
  const [confirm, setConfirm] = useState(null);

  const DepTable = ({title, rows, direction, empty}) => (
    <div className="card">
      <div className="card-head">
        <h3>{title}</h3><Badge tone="slate">{rows.length}</Badge><div className="spacer"/>
        {editable && <button className="btn sm" onClick={()=>setDlg({direction})}><I.plus size={13}/>Add</button>}
      </div>
      {rows.length ? (
        <div className="tbl-wrap">
          <table>
            <thead><tr><th>Component</th><th>Type</th><th>Tier</th><th>Relationship</th>
              <th>Criticality</th><th>Notes</th>{editable && <th style={{width:40}}></th>}</tr></thead>
            <tbody>
              {rows.map(d => {
                const otherId = direction === 'out' ? d.toId : d.fromId;
                const o = idx.comp[otherId];
                if (!o) return null;
                const t = idx.type[o.typeId]; const TIcon = typeIconFor(t);
                return (
                  <tr key={d.id} className="clickable" onClick={()=>go({view:'component',id:o.id})}>
                    <td><div className="row" style={{gap:8}}>
                      <span style={{color:'var(--text-3)'}}><TIcon size={15}/></span>
                      <span><span className="cell-name" style={{display:'block'}}>{o.name}</span>
                        <span className="cell-sub mono">{o.identifier}</span></span></div></td>
                    <td className="small muted">{t ? t.name : '—'}</td>
                    <td><TierBadge tier={o.tier}/></td>
                    <td className="small">{d.type}</td>
                    <td><Badge tone={d.criticality==='Hard'?'red':'slate'}>{d.criticality}</Badge></td>
                    <td className="small muted">{d.notes || <span className="dim">—</span>}</td>
                    {editable && <td onClick={e=>e.stopPropagation()}>
                      <button className="icon-btn" title="Remove relationship"
                        onClick={()=>setConfirm(d)}><I.trash size={14}/></button></td>}
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      ) : <EmptyState icon={<I.graph size={26}/>} title={empty}/>}
    </div>
  );

  return (
    <div className="stack">
      <div className="row wrap">
        <span className="small muted">Show</span>
        <select style={{width:'auto'}} value={depth} onChange={e=>setDepth(Number(e.target.value))}>
          <option value={1}>Direct relationships</option>
          <option value={2}>2 hops</option>
          <option value={3}>3 hops</option>
          <option value={4}>Full chain</option>
        </select>
        <div className="spacer"/>
        <button className="btn sm" onClick={()=>go({view:'components', tab:'architecture'})}><I.layers size={13}/>Whole estate map</button>
      </div>

      <DependencyGraph nodes={graph.nodes} links={graph.links} focusId={comp.id} height={420}
        onSelect={(id)=>{ if (id !== comp.id) go({view:'component',id}); }}/>

      <DepTable title="This component depends on" rows={dependsOn} direction="out" empty="No upstream dependencies recorded"/>
      <DepTable title="Depended on by" rows={dependants} direction="in" empty="Nothing depends on this component"/>

      {dlg &&
        <DependencyForm comp={comp} state={state} direction={dlg.direction} onClose={()=>setDlg(null)}
          onSave={async (v)=>{ await act(API.post('/api/dependencies', v), 'Relationship added'); setDlg(null); }}/>}
      {confirm &&
        <ConfirmDialog title="Remove relationship" confirmLabel="Remove"
          message="This removes the relationship between the two components. The components themselves are not affected."
          onClose={()=>setConfirm(null)}
          onConfirm={()=>act(API.del(`/api/dependencies/${confirm.id}`), 'Relationship removed')}/>}
    </div>
  );
}

/* ------------------------------------ documentation, owned by its own module */

function ComponentDocsTab({comp, state, idx, act, editable, go}){
  const [picking, setPicking] = useState(false);
  const [selection, setSelection] = useState([]);
  const documents = state.documents.filter(d => (d.componentIds||[]).includes(comp.id));
  const tools = state.tools.filter(t => (t.componentIds||[]).includes(comp.id));
  const available = state.documents.filter(d => !(d.componentIds||[]).includes(comp.id));

  return (
    <div className="stack">
      <div className="card">
        <div className="card-head">
          <h3>Documentation</h3><Badge tone="slate">{documents.length}</Badge>
          <span className="xsmall dim">Held in the Documents module and linked here</span>
          <div className="spacer"/>
          <button className="btn sm" onClick={()=>go({view:'documents'})}>Open Documents</button>
          {editable &&
            <button className="btn sm primary" onClick={()=>{ setSelection([]); setPicking(true); }}>
              <I.plus size={13}/>Link a document</button>}
        </div>
        {documents.length === 0
          ? <EmptyState icon={<I.book size={28}/>} title="No documentation linked"
              body="Runbooks, architecture notes and policies for this component live in the Documents module."/>
          : <DocumentTable documents={documents} idx={idx} go={go}
              onUnlink={editable ? (d)=>act(
                API.del(`/api/documents/${d.id}/components/${comp.id}`), 'Document unlinked') : null}/>}
      </div>

      {picking &&
        <Modal wide title="Link existing documents" onClose={()=>setPicking(false)}
          footer={<>
            <button className="btn" onClick={()=>setPicking(false)}>Cancel</button>
            <button className="btn primary" disabled={!selection.length}
              onClick={async ()=>{
                for (const id of selection) await API.post(`/api/documents/${id}/components/${comp.id}`);
                await act(Promise.resolve(), `${selection.length} document(s) linked`);
                setPicking(false);
              }}>Link {selection.length || ''}</button>
          </>}>
          {!available.length && <p className="dim">Every document is already linked to this component.</p>}
          <div className="multi-pick" style={{maxHeight:340}}>
            {available.map(d =>
              <label key={d.id}>
                <input type="checkbox" checked={selection.includes(d.id)}
                  onChange={()=>setSelection(s => s.includes(d.id) ? s.filter(x=>x!==d.id) : [...s, d.id])}/>
                <span style={{flex:1,minWidth:0}}>
                  <span className="truncate" style={{display:'block',fontWeight:600}}>{d.title}</span>
                  <span className="cell-sub">{d.docType}{d.version ? ' · ' + d.version : ''}</span>
                </span>
                <Badge tone={DOC_STATUS_TONE[d.status]}>{d.status}</Badge>
              </label>)}
          </div>
        </Modal>}
    </div>
  );
}

/* -------------------------------------- requests raised against this component */

function ComponentRequestsTab({comp, state, idx, go, toast, requests}){
  const [showClosed, setShowClosed] = useState(false);
  const shown = showClosed ? requests : requests.filter(r => !['Done','Rejected'].includes(r.status));
  const {menu, openMenu, closeMenu} = useRecordMenu();
  return (
    <div className="stack">
      <div className="row wrap">
        <button className={'chip-toggle' + (showClosed ? ' on' : '')} onClick={()=>setShowClosed(v=>!v)}>
          Include closed
        </button>
        <div className="spacer"/>
        <button className="btn sm" onClick={()=>go({view:'requests'})}>Open Business Requests</button>
      </div>
      <div className="card">
        <div className="card-head">
          <h3>Business requests</h3><Badge tone="slate">{shown.length}</Badge>
        </div>
        {shown.length === 0
          ? <EmptyState icon={<I.inbox size={28}/>} title="No requests against this component"/>
          : <RequestTable requests={shown} idx={idx} go={go} onOpen={(r)=>go({view:'request', id:r.id})}
              onContextMenu={(e,r)=>openMenu(e, recordMenuItem('request', r))}/>}
      </div>
      <RecordContextMenu menu={menu} onClose={closeMenu} toast={toast}/>
    </div>
  );
}

/* --------------------------------------- changes landing on this component */

function ComponentChangesTab({comp, state, idx, go, toast, changes}){
  const [showClosed, setShowClosed] = useState(false);
  const shown = showClosed
    ? changes
    : changes.filter(c => !['Closed','Cancelled','Rejected'].includes(c.status));
  const upcoming = shown.filter(c => c.plannedStart && new Date(c.plannedStart) > new Date());
  const {menu, openMenu, closeMenu} = useRecordMenu();
  return (
    <div className="stack">
      <div className="row wrap">
        <button className={'chip-toggle' + (showClosed ? ' on' : '')} onClick={()=>setShowClosed(v=>!v)}>
          Include closed</button>
        {upcoming.length > 0 &&
          <span className="xsmall dim">
            Next: {upcoming[upcoming.length-1].reference} on {fmtDate(upcoming[upcoming.length-1].plannedStart)}
          </span>}
        <div className="spacer"/>
        <button className="btn sm" onClick={()=>go({view:'changes'})}>Open Change Management</button>
      </div>
      <div className="card">
        <div className="card-head"><h3>Changes</h3><Badge tone="slate">{shown.length}</Badge></div>
        {shown.length === 0
          ? <EmptyState icon={<I.change size={28}/>} title="No changes against this component"/>
          : <div className="tbl-wrap">
              <table>
                <thead><tr><th style={{width:100}}>Ref</th><th>Change</th><th>Type</th>
                  <th>Risk</th><th>Status</th><th>Planned</th></tr></thead>
                <tbody>
                  {shown.map(c => (
                    <tr key={c.id} className="clickable" onClick={()=>go({view:'change', id:c.id})}
                        onContextMenu={(e)=>openMenu(e, recordMenuItem('change', c))}>
                      <td className="mono dim">{c.reference}</td>
                      <td><span className="cell-name">{c.title}</span></td>
                      <td><ChangeType value={c.changeType}/></td>
                      <td><Risk value={c.risk}/></td>
                      <td><Badge tone={CHANGE_STATUS_TONE[c.status] || 'slate'} dot>{c.status}</Badge></td>
                      <td className="small">{c.plannedStart
                        ? fmtDate(c.plannedStart) : <span className="dim">—</span>}</td>
                    </tr>))}
                </tbody>
              </table>
            </div>}
      </div>
      <RecordContextMenu menu={menu} onClose={closeMenu} toast={toast}/>
    </div>
  );
}

function DependencyForm({comp, state, direction, onClose, onSave}){
  const [dir, setDir] = useState(direction || 'out');
  const [otherId, setOtherId] = useState('');
  const [type, setType] = useState('Depends on');
  const [criticality, setCriticality] = useState('Hard');
  const [notes, setNotes] = useState('');
  const types = state.lookups.dependency_type || ['Depends on'];
  const options = state.components.filter(c => c.id !== comp.id);

  return (
    <Modal title="Add relationship" onClose={onClose}
      footer={<>
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn primary" disabled={!otherId}
          onClick={()=>onSave({
            fromId: dir === 'out' ? comp.id : otherId,
            toId:   dir === 'out' ? otherId : comp.id,
            type, criticality, notes})}>Add relationship</button>
      </>}>
      <Field label="Direction">
        <div className="row wrap" style={{gap:8}}>
          <button type="button" className={'chip-toggle'+(dir==='out'?' on':'')} onClick={()=>setDir('out')}>
            {comp.name} depends on …</button>
          <button type="button" className={'chip-toggle'+(dir==='in'?' on':'')} onClick={()=>setDir('in')}>
            … depends on {comp.name}</button>
        </div>
      </Field>
      <Field label={dir==='out' ? 'Depends on which component?' : 'Which component depends on this?'}>
        <select value={otherId} onChange={e=>setOtherId(e.target.value)}>
          <option value="">Choose a component…</option>
          {options.map(c => <option key={c.id} value={c.id}>{c.identifier} — {c.name}</option>)}
        </select>
      </Field>
      <div className="form-grid">
        <Field label="Relationship type">
          <select value={type} onChange={e=>setType(e.target.value)}>{types.map(t=><option key={t}>{t}</option>)}</select>
        </Field>
        <Field label="Criticality" hint="Hard = fails without it. Soft = degraded but usable.">
          <select value={criticality} onChange={e=>setCriticality(e.target.value)}>
            {CRITICALITIES.map(t=><option key={t}>{t}</option>)}</select>
        </Field>
      </div>
      <Field label="Notes"><textarea value={notes} onChange={e=>setNotes(e.target.value)}
        placeholder="Optional detail about the interface or coupling."/></Field>
    </Modal>
  );
}

/* ---------------------------------------------------------- component form */

function ComponentForm({value, state, onClose, onSave, toast}){
  const [c, setC] = useState(()=>clone(value));
  const [tagText, setTagText] = useState((value.tags||[]).join(', '));
  const [busy, setBusy] = useState(false);
  const set = (k,v) => setC(x => ({...x, [k]:v}));
  const setOwner = (k,v) => setC(x => ({...x, owners: {...x.owners, [k]:v}}));
  const isNew = !value.id;
  const environments = state.lookups.environment || [];

  const submit = async () => {
    if (!c.name.trim()) return;
    setBusy(true);
    try {
      await onSave({...c, tags: tagText.split(',').map(s=>s.trim()).filter(Boolean), tier: Number(c.tier)});
    } finally { setBusy(false); }
  };

  return (
    <Modal wide title={isNew ? 'New component' : 'Edit ' + value.name} onClose={onClose}
      footer={<>
        <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn primary" disabled={!c.name.trim() || busy} onClick={submit}>
          {busy ? 'Saving…' : isNew ? 'Create component' : 'Save changes'}</button>
      </>}>
      <div className="sub-head">Identity</div>
      <div className="form-grid">
        <Field label="Name"><input type="text" value={c.name} onChange={e=>set('name', e.target.value)}
          placeholder="e.g. Ledgerline Finance ERP" autoFocus/></Field>
        <Field label="Identifier" hint="Your internal reference, e.g. FIN-001">
          <input type="text" value={c.identifier||''} onChange={e=>set('identifier', e.target.value)}/></Field>
      </div>
      <Field label="Description">
        <RichTextEditor value={c.description||''} onChange={v=>set('description', v)} toast={toast}
          uploadContext={c.id ? {entityKind:'component', entityId:c.id} : null}
          placeholder="What it does and which business process it supports."/>
      </Field>

      <div className="sub-head">Classification</div>
      <div className="form-grid three">
        <Field label="Type">
          <select value={c.typeId||''} onChange={e=>set('typeId', e.target.value)}>
            <option value="">Choose…</option>
            {state.componentTypes.map(t=><option key={t.id} value={t.id}>{t.name}</option>)}
          </select></Field>
        <Field label="Status">
          <select value={c.status} onChange={e=>set('status', e.target.value)}>
            {STATUSES.map(s=><option key={s}>{s}</option>)}</select></Field>
        <Field label="Tier" hint="1 = most critical">
          <select value={c.tier} onChange={e=>set('tier', e.target.value)}>
            {TIERS.map(t=><option key={t} value={t}>Tier {t} — {TIER_LABEL[t]}</option>)}</select></Field>
      </div>

      <div className="sub-head">Ownership</div>
      <div className="form-grid">
        {OWNER_ROLES.map(o =>
          <Field key={o.key} label={o.label}>
            <PartySelect value={c.owners && c.owners[o.key]} state={state} onChange={v=>setOwner(o.key, v)}/>
          </Field>)}
      </div>

      <div className="sub-head">Delivery &amp; hosting</div>
      <div className="form-grid">
        <Field label="Vendor / supplier"><input type="text" value={c.vendor||''}
          onChange={e=>set('vendor', e.target.value)} placeholder="In-house or vendor name"/></Field>
        <Field label="Business capability"><input type="text" value={c.businessCapability||''}
          onChange={e=>set('businessCapability', e.target.value)} placeholder="e.g. Finance &amp; Accounting"/></Field>
        <Field label="Hosting location"><input type="text" value={c.hostingLocation||''}
          onChange={e=>set('hostingLocation', e.target.value)}/></Field>
        <Field label="Environment">
          <select value={c.environment||''} onChange={e=>set('environment', e.target.value)}>
            <option value="">Not set</option>
            {environments.map(v=><option key={v}>{v}</option>)}
          </select></Field>
        <Field label="Recovery objectives"><input type="text" value={c.recoveryTier||''}
          onChange={e=>set('recoveryTier', e.target.value)} placeholder="e.g. RTO 4h / RPO 15m"/></Field>
        <Field label="Cost centre"><input type="text" value={c.costCentre||''}
          onChange={e=>set('costCentre', e.target.value)}/></Field>
        <Field label="Go live date"><input type="date" value={c.goLiveDate||''}
          onChange={e=>set('goLiveDate', e.target.value)}/></Field>
        <Field label="Last reviewed"><input type="date" value={c.lastReviewed||''}
          onChange={e=>set('lastReviewed', e.target.value)}/></Field>
      </div>
      <Field label="Tags" hint="Comma separated"><input type="text" value={tagText}
        onChange={e=>setTagText(e.target.value)} placeholder="e.g. Public facing, GDPR"/></Field>
      <Field label="Summary notes" hint="Longer notes belong on the Notes tab.">
        <textarea value={c.notes||''} onChange={e=>set('notes', e.target.value)}/></Field>
    </Modal>
  );
}


/* ============================================================
   COMPONENTS CONFIGURATION
   The classification lists used across the model: stakeholder capacities,
   RACI activities and component types. Moved in from the Directory module,
   since these describe components rather than people.
   ============================================================ */
function LookupForm({title, label, value, onClose, onSave, extra}){
  const [v, setV] = useState(()=>clone(value));
  return (
    <Modal title={title} onClose={onClose}
      footer={<><button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn primary" disabled={!v.name.trim()} onClick={()=>onSave(v)}>Save</button></>}>
      <Field label={label}><input type="text" value={v.name}
        onChange={e=>setV(x=>({...x,name:e.target.value}))} autoFocus/></Field>
      <Field label="Description"><textarea value={v.description||''}
        onChange={e=>setV(x=>({...x,description:e.target.value}))}/></Field>
      {extra && extra(v, setV)}
    </Modal>
  );
}

function ComponentsConfiguration({state, idx, act, toast}){
  const [tab, setTab] = useState('capacities');
  const [dlg, setDlg] = useState(null);
  const [q, setQ] = useState('');
  const canEditLk = state.permissions.canEditLookups;

  const term = q.trim().toLowerCase();
  const hits = (...parts) => !term || parts.filter(Boolean).join(' ').toLowerCase().includes(term);
  const capacities = state.capacities.filter(c => hits(c.name, c.description));
  const activities = state.raciActivities.filter(a => hits(a.name, a.description));
  const types = state.componentTypes.filter(t => hits(t.name, t.description));
  const shown = {capacities, activities, types}[tab];

  const TabBtn = ({k, label, n}) =>
    <button className={'tab'+(tab===k?' active':'')} onClick={()=>setTab(k)}>{label} <span className="n">{n}</span></button>;

  const addButton = {
    capacities: canEditLk && {label:'Add capacity', dlg:{t:'capacity', v:{name:'',description:''}}},
    activities: canEditLk && {label:'Add activity', dlg:{t:'activity', v:{name:'',description:''}}},
    types: canEditLk && {label:'Add type', dlg:{t:'type', v:{name:'',description:'',icon:'system'}}}
  }[tab];

  return (
    <div className="stack">
      {!canEditLk &&
        <ReadOnlyBanner message="You have read-only access to these classification lists. Administrators can make changes."/>}

      <div className="row wrap">
        <div className="tabs" style={{margin:0}}>
          <TabBtn k="capacities" label="Stakeholder capacities" n={state.capacities.length}/>
          <TabBtn k="activities" label="RACI activities" n={state.raciActivities.length}/>
          <TabBtn k="types" label="Component types" n={state.componentTypes.length}/>
        </div>
        <div className="spacer"/>
        {addButton &&
          <button className="btn sm primary" onClick={()=>setDlg(addButton.dlg)}>
            <I.plus size={13}/>{addButton.label}</button>}
      </div>

      <div className="row wrap">
        <SearchBox value={q} onChange={setQ} aria="Search configuration" title="Search the open tab"/>
        {q && <button className="btn ghost sm" onClick={()=>setQ('')}>Clear</button>}
        <span className="xsmall dim">{shown.length} shown</span>
      </div>

      {tab === 'capacities' &&
        <div className="card">
          <div className="card-head"><span className="xsmall dim">The lookup list used when adding stakeholders</span></div>
          <div className="tbl-wrap">
            <table>
              <thead><tr><th>Capacity</th><th>Description</th><th>In use</th>{canEditLk && <th style={{width:70}}></th>}</tr></thead>
              <tbody>{capacities.map(cap => {
                const used = state.components.filter(c => (c.stakeholders||[]).some(s=>s.capacityId===cap.id)).length;
                return (
                  <tr key={cap.id}>
                    <td><span className="cell-name">{cap.name}</span>
                      {cap.isOwnerLevel && <Badge tone="accent">Owner level</Badge>}</td>
                    <td className="small muted">{cap.description||'—'}</td>
                    <td className="small mono">{cap.isOwnerLevel ? state.components.length : used}</td>
                    {canEditLk && <td>{!cap.isOwnerLevel && <div className="row" style={{gap:2}}>
                      <button className="icon-btn" onClick={()=>setDlg({t:'capacity', v:cap})}><I.edit size={14}/></button>
                      <button className="icon-btn"
                        onClick={()=>deleteWithUndo(act, toast, `/api/lookup/capacities/${cap.id}`, cap.name)}><I.trash size={14}/></button>
                    </div>}</td>}
                  </tr>);
              })}</tbody>
            </table>
          </div>
        </div>}

      {tab === 'activities' &&
        <div className="card">
          <div className="card-head"><span className="xsmall dim">{'The rows of every component’s RACI table'}</span></div>
          <div className="tbl-wrap">
            <table>
              <thead><tr><th>Activity</th><th>Description</th><th>Assignments</th>{canEditLk && <th style={{width:70}}></th>}</tr></thead>
              <tbody>{activities.map(a => {
                const n = state.components.reduce((s,c)=>s+(c.raci||[]).filter(r=>r.activityId===a.id).length, 0);
                return (
                  <tr key={a.id}>
                    <td><span className="cell-name">{a.name}</span></td>
                    <td className="small muted">{a.description||'—'}</td>
                    <td className="small mono">{n}</td>
                    {canEditLk && <td><div className="row" style={{gap:2}}>
                      <button className="icon-btn" onClick={()=>setDlg({t:'activity', v:a})}><I.edit size={14}/></button>
                      <button className="icon-btn" disabled={!!n} title={n ? 'Still in use' : 'Delete'}
                        onClick={()=>{ if (!n) deleteWithUndo(act, toast, `/api/lookup/raci-activities/${a.id}`, a.name); }}>
                        <I.trash size={14}/></button>
                    </div></td>}
                  </tr>);
              })}</tbody>
            </table>
          </div>
        </div>}

      {tab === 'types' &&
        <div className="card">
          <div className="tbl-wrap">
            <table>
              <thead><tr><th style={{width:60}}></th><th>Type</th><th>Components</th>{canEditLk && <th style={{width:70}}></th>}</tr></thead>
              <tbody>{types.map(t => {
                const TIcon = typeIconFor(t);
                const n = state.components.filter(c=>c.typeId===t.id).length;
                return (
                  <tr key={t.id}>
                    <td><span className="type-icon" style={{width:30,height:30,flexBasis:30}}><TIcon size={15}/></span></td>
                    <td><span className="cell-name">{t.name}</span></td>
                    <td className="small mono">{n}</td>
                    {canEditLk && <td><div className="row" style={{gap:2}}>
                      <button className="icon-btn" onClick={()=>setDlg({t:'type', v:t})}><I.edit size={14}/></button>
                      <button className="icon-btn" disabled={!!n} title={n ? 'Still in use' : 'Delete'}
                        onClick={()=>{ if (!n) deleteWithUndo(act, toast, `/api/lookup/component-types/${t.id}`, t.name); }}>
                        <I.trash size={14}/></button>
                    </div></td>}
                  </tr>);
              })}</tbody>
            </table>
          </div>
        </div>}

      {dlg && dlg.t === 'capacity' &&
        <LookupForm title={dlg.v.id?'Edit capacity':'Add capacity'} label="Capacity name" value={dlg.v}
          onClose={()=>setDlg(null)} onSave={async (v)=>{
            await act(v.id ? API.put(`/api/lookup/capacities/${v.id}`, v) : API.post('/api/lookup/capacities', v), 'Saved');
            setDlg(null);
          }}/>}
      {dlg && dlg.t === 'activity' &&
        <LookupForm title={dlg.v.id?'Edit activity':'Add RACI activity'} label="Activity name" value={dlg.v}
          onClose={()=>setDlg(null)} onSave={async (v)=>{
            await act(v.id ? API.put(`/api/lookup/raci-activities/${v.id}`, v) : API.post('/api/lookup/raci-activities', v), 'Saved');
            setDlg(null);
          }}/>}
      {dlg && dlg.t === 'type' &&
        <LookupForm title={dlg.v.id?'Edit type':'Add type'} label="Type name" value={dlg.v}
          onClose={()=>setDlg(null)}
          onSave={async (v)=>{
            await act(v.id ? API.put(`/api/lookup/component-types/${v.id}`, v) : API.post('/api/lookup/component-types', v), 'Saved');
            setDlg(null);
          }}
          extra={(v,setV)=>(
            <Field label="Icon">
              <select value={v.icon||'system'} onChange={e=>setV(x=>({...x, icon:e.target.value}))}>
                {Object.keys(TYPE_ICON).map(k=><option key={k} value={k}>{k}</option>)}
              </select>
            </Field>)}/>}
    </div>
  );
}
