/* Gestão de TI (ITSM) · BNI Brasil.
   Cadastros de gestão de tecnologia (acessos, ativos, sistemas, fornecedores,
   custos e LGPD), organização (setores e colaboradores) e acompanhamento dos
   artefatos da consultoria, sobre o design system BNI
   (window.BNIDesignSystem_b8f606). */
const {
  Button, IconButton, Badge, Input, Select, Card, SectionHeading, Stat,
  Alert, Dialog, Logo, Checkbox, Switch,
} = window.BNIDesignSystem_b8f606;

const { useState, useEffect, useRef, useCallback, useMemo, createContext, useContext } = React;

// Usuário autenticado disponível a toda a árvore (papel, áreas e somente-leitura).
const ContextoUsuario = createContext(null);
const usarUsuario = () => useContext(ContextoUsuario);
const podeArea = (usuario, area) => !!usuario && (usuario.areas === '*' || usuario.areas.includes(area));

/* Ícones estilo Lucide (mesma substituição documentada no design system). */
function Icone({ nome, tamanho = 16 }) {
  const caminhos = {
    mais: <><path d="M5 12h14" /><path d="M12 5v14" /></>,
    lapis: <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />,
    lixeira: <><path d="M3 6h18" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></>,
    painel: <><rect width="7" height="9" x="3" y="3" rx="1" /><rect width="7" height="5" x="14" y="3" rx="1" /><rect width="7" height="9" x="14" y="12" rx="1" /><rect width="7" height="5" x="3" y="16" rx="1" /></>,
    prancheta: <><rect width="8" height="4" x="8" y="2" rx="1" ry="1" /><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" /><path d="m9 14 2 2 4-4" /></>,
    predio: <><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z" /><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2" /><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2" /><path d="M10 6h4" /><path d="M10 10h4" /><path d="M10 14h4" /><path d="M10 18h4" /></>,
    pessoas: <><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></>,
    chave: <><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" /><circle cx="16.5" cy="7.5" r=".5" fill="currentColor" /></>,
    notebook: <path d="M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9m16 0H4m16 0 1.28 2.55a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45L4 16" />,
    servidor: <><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /><rect width="20" height="8" x="2" y="14" rx="2" ry="2" /><path d="M6 6h.01" /><path d="M6 18h.01" /></>,
    pacote: <><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" /><path d="M12 22V12" /><path d="m3.3 7 8.7 5 8.7-5" /></>,
    cartao: <><rect width="20" height="14" x="2" y="5" rx="2" /><path d="M2 10h20" /></>,
    escudo: <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1 1 0 0 1 1.52 0C14.5 3.8 17 5 19 5a1 1 0 0 1 1 1z" />,
    painelEsquerdo: <><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M9 3v18" /></>,
    externo: <><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></>,
    voltar: <path d="m15 18-6-6 6-6" />,
    clipe: <path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" />,
    cadeado: <><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></>,
    sair: <><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><path d="m16 17 5-5-5-5" /><path d="M21 12H9" /></>,
    contrato: <><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z" /><path d="M14 2v5h5" /><path d="M9 13h6" /><path d="M9 17h6" /></>,
    engrenagem: <><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></>,
    ajustes: <><line x1="4" x2="4" y1="21" y2="14" /><line x1="4" x2="4" y1="10" y2="3" /><line x1="12" x2="12" y1="21" y2="12" /><line x1="12" x2="12" y1="8" y2="3" /><line x1="20" x2="20" y1="21" y2="16" /><line x1="20" x2="20" y1="12" y2="3" /><line x1="2" x2="6" y1="14" y2="14" /><line x1="10" x2="14" y1="8" y2="8" /><line x1="18" x2="22" y1="16" y2="16" /></>,
  };
  return (
    <svg width={tamanho} height={tamanho} viewBox="0 0 24 24" fill="none" stroke="currentColor"
      strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      {caminhos[nome]}
    </svg>
  );
}

const ICONE_MENU = {
  'resumo': 'painel',
  'artefatos': 'prancheta',
  'setores': 'predio',
  'colaboradores': 'pessoas',
  'acessos-e-contas': 'chave',
  'parque-de-maquinas': 'notebook',
  'sistemas': 'servidor',
  'fornecedores': 'pacote',
  'contratos': 'contrato',
  'custos-e-servicos': 'cartao',
  'dados-pessoais-lgpd': 'escudo',
  'usuarios': 'cadeado',
  'papeis': 'ajustes',
  'configuracoes': 'engrenagem',
};

// Rótulo do botão que cadastra um registro vinculado a partir da ficha (relação reversa).
const ROTULO_NOVO = {
  'sistemas': 'Adicionar sistema',
  'contratos': 'Adicionar contrato',
  'colaboradores': 'Adicionar colaborador',
  'custos-e-servicos': 'Adicionar custo',
  'acessos-e-contas': 'Adicionar acesso',
  'parque-de-maquinas': 'Adicionar máquina',
};

const APARENCIA_STATUS = {
  'Não iniciado': { tone: 'grey', variant: 'soft' },
  'Em andamento': { tone: 'red', variant: 'soft' },
  'Em revisão': { tone: 'red', variant: 'outline' },
  'Entregue': { tone: 'red', variant: 'solid' },
};

/* Áreas do sistema cujos cadastros alimentam cada artefato. */
const FONTES_ARTEFATO = {
  '01': ['setores', 'colaboradores', 'acessos-e-contas', 'parque-de-maquinas', 'sistemas', 'dados-pessoais-lgpd', 'fornecedores'],
  '02': ['sistemas', 'fornecedores'],
  '03': ['custos-e-servicos', 'parque-de-maquinas', 'sistemas'],
  '04': ['acessos-e-contas', 'colaboradores'],
  '05': ['acessos-e-contas', 'dados-pessoais-lgpd', 'sistemas', 'fornecedores', 'parque-de-maquinas'],
  '06': [],
  '07': [],
  '08': ['sistemas', 'acessos-e-contas', 'fornecedores', 'setores'],
  '09': ['sistemas', 'fornecedores'],
  '10': ['fornecedores', 'sistemas'],
};

async function chamarApi(caminho, opcoes) {
  const resposta = await fetch(caminho, opcoes);
  if (resposta.status === 401) {
    // Sessão ausente ou expirada: avisa o app para voltar à tela de login.
    window.dispatchEvent(new CustomEvent('sessao-expirada'));
  }
  if (resposta.status === 204) return null;
  const corpo = await resposta.json().catch(() => ({}));
  if (!resposta.ok) throw new Error(corpo.erro || `Falha na requisição (${resposta.status}).`);
  return corpo;
}

function dadosVazios(entidade) {
  return Object.fromEntries(entidade.campos.map((c) => [c.chave, '']));
}

function dataBr(carimbo) {
  return carimbo ? carimbo.slice(0, 10).split('-').reverse().join('/') : '';
}

// Formata um campo numérico para exibição (ex.: 30000 -> "30.000,00").
function formatarNumero(valor) {
  const texto = String(valor == null ? '' : valor).trim();
  if (!texto) return '';
  const n = Number(texto.replace(',', '.'));
  if (Number.isNaN(n)) return texto;
  return n.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}

// Só deixa passar links de navegação de verdade; bloqueia javascript:, data:, etc.
function hrefSeguro(url) {
  const texto = String(url || '').trim();
  return /^(https?:|mailto:)/i.test(texto) ? texto : '#';
}

// Nome legível de um registro: resolve o campo principal quando ele é um vínculo (ref),
// para nunca exibir o id cru (ex.: acesso cujo campo principal aponta para um sistema).
function nomePrincipal(entidade, dados, referencias, entidades) {
  const campo = entidade.campos[0];
  if (campo.tipo === 'ref') {
    const alvo = (referencias?.[campo.refEntidade] || []).find((r) => String(r.id) === String(dados[campo.chave] || ''));
    if (!alvo) return '';
    const def = entidades.find((e) => e.chave === campo.refEntidade);
    return alvo.dados[def.campos[0].chave] || `Registro ${alvo.id}`;
  }
  return dados[campo.chave] || '';
}

// Rótulo de um registro numa lista de vínculo reverso: primeiro campo de texto
// identificador, ignorando o próprio vínculo de volta (que seria igual para todos).
function rotuloReverso(rev, reg) {
  for (const campo of rev.entidade.campos) {
    if (campo.chave === rev.campo || campo.tipo === 'ref' || campo.tipo === 'textarea') continue;
    if (reg.dados[campo.chave]) return reg.dados[campo.chave];
  }
  return `Registro ${reg.id}`;
}

/* Placeholders de carregamento (skeleton): blocos que pulsam até os dados chegarem.
   Substituem a tela em branco enquanto a API responde, em todas as telas. */
function Esqueleto({ largura = '100%', altura = 'var(--space-4)', raio, style }) {
  return (
    <span className="esqueleto" aria-hidden="true"
      style={{ width: largura, height: altura, borderRadius: raio || 'var(--radius-sm)', ...style }} />
  );
}

/* Tabela fantasma: mesmo envelope e grade da tabela real, com linhas pulsando. */
function EsqueletoTabela({ colunas = 4, linhas = 6 }) {
  const cols = Array.from({ length: Math.max(1, colunas) });
  const corpo = Array.from({ length: linhas });
  return (
    <div className="tabela-envelope" role="status" aria-label="Carregando registros">
      <table className="tabela">
        <thead>
          <tr>{cols.map((_, i) => (
            <th key={i}><Esqueleto largura={i === 0 ? '60%' : '42%'} altura="var(--space-3)" /></th>
          ))}</tr>
        </thead>
        <tbody>
          {corpo.map((_, l) => (
            <tr key={l}>
              {cols.map((_, c) => (
                <td key={c}><Esqueleto largura={c === 0 ? '78%' : `${45 + ((l + c) % 4) * 12}%`} /></td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

/* Ficha fantasma: voltar, título, ações e uma grade de campos pulsando. */
function EsqueletoFicha({ itens = 6 }) {
  return (
    <div role="status" aria-label="Carregando registro">
      <Esqueleto largura="140px" altura="var(--space-4)" style={{ marginBottom: 'var(--space-4)' }} />
      <Esqueleto largura="42%" altura="var(--space-6)" style={{ marginBottom: 'var(--space-3)' }} />
      <div className="detalhe-acoes">
        <Esqueleto largura="180px" altura="var(--space-4)" />
        <Esqueleto largura="104px" altura="var(--space-7)" />
        <Esqueleto largura="104px" altura="var(--space-7)" />
      </div>
      <div className="detalhe-secao">
        <Card accent>
          <div className="ficha-grade">
            {Array.from({ length: itens }).map((_, i) => (
              <div key={i} className="ficha-item">
                <Esqueleto largura="55%" altura="var(--space-3)" style={{ marginBottom: 'var(--space-2)' }} />
                <Esqueleto largura="85%" />
              </div>
            ))}
          </div>
        </Card>
      </div>
    </div>
  );
}

/* Grade de cartões fantasma (visão geral). */
function EsqueletoCartoes({ n = 6 }) {
  return (
    <div className="resumo-grade" role="status" aria-label="Carregando">
      {Array.from({ length: n }).map((_, i) => (
        <Card key={i} accent>
          <Esqueleto largura="45%" altura="var(--space-7)" style={{ marginBottom: 'var(--space-3)' }} />
          <Esqueleto largura="70%" altura="var(--space-3)" />
          <div className="cartao-frente-entregavel"><Esqueleto largura="90%" altura="var(--space-3)" /></div>
        </Card>
      ))}
    </div>
  );
}

/* Tela inteira em carregamento (verificação de sessão e esquema inicial). */
function EsqueletoInicial() {
  return (
    <div className="carregando-inicial" role="status" aria-label="Carregando o sistema">
      <div className="carregando-cartao">
        <Logo variant="red" height={30} src="/ds/assets/bni-logo-red.png" />
        <Esqueleto largura="70%" altura="var(--space-3)" />
        <Esqueleto largura="92%" />
        <Esqueleto largura="84%" />
        <Esqueleto largura="60%" />
      </div>
    </div>
  );
}

/* Campos de edição de um registro; usados no diálogo de criação e na edição
   inline da ficha. */
function CamposDeFormulario({ entidade, entidades, referencias, dados, definir }) {
  return entidade.campos.map((campo) => {
    const rotulo = campo.obrigatorio ? `${campo.rotulo} *` : campo.rotulo;
    if (campo.tipo === 'select') {
      return (
        <Select key={campo.chave} label={rotulo} options={['', ...campo.opcoes]}
          value={dados[campo.chave]} onChange={definir(campo.chave)} />
      );
    }
    if (campo.tipo === 'ref') {
      const alvo = entidades.find((e) => e.chave === campo.refEntidade);
      const lista = referencias[campo.refEntidade];
      const opcoes = [{ value: '', label: '' }].concat(
        (lista || []).map((r) => ({ value: String(r.id), label: r.dados[alvo.campos[0].chave] || `Registro ${r.id}` })),
      );
      const ajuda = Array.isArray(lista) && lista.length === 0
        ? `Nenhum registro em ${alvo.titulo} ainda. Cadastre lá primeiro.` : undefined;
      return (
        <Select key={campo.chave} label={rotulo} options={opcoes}
          value={dados[campo.chave]} onChange={definir(campo.chave)} helper={ajuda} />
      );
    }
    if (campo.tipo === 'textarea') {
      return (
        <label key={campo.chave} className="campo-cheio">
          <span className="campo-rotulo">{rotulo}</span>
          <textarea className="campo-textarea" value={dados[campo.chave]} onChange={definir(campo.chave)} />
        </label>
      );
    }
    const tipoEntrada = campo.tipo === 'data' ? 'date'
      : campo.tipo === 'url' ? 'url'
      : campo.tipo === 'numero' ? 'number' : 'text';
    return (
      <Input key={campo.chave} label={rotulo} type={tipoEntrada}
        placeholder={campo.tipo === 'url' ? 'https://' : campo.tipo === 'numero' ? '0.00' : undefined}
        value={dados[campo.chave]} onChange={definir(campo.chave)} />
    );
  });
}

/* Formulário de criação e edição de um registro de qualquer entidade. */
function DialogoRegistro({ entidade, entidades, referencias, registro, iniciais, aoFechar, aoSalvar }) {
  const [dados, setDados] = useState(() => ({ ...dadosVazios(entidade), ...(iniciais || {}), ...(registro ? registro.dados : {}) }));
  const [erro, setErro] = useState('');
  const [salvando, setSalvando] = useState(false);
  const [contratos, setContratos] = useState([]);

  useEffect(() => {
    if (!entidade.anexos || !registro) return;
    chamarApi(`/api/${entidade.chave}/${registro.id}/anexos`)
      .then((corpo) => setContratos(corpo.anexos))
      .catch(() => {});
  }, []);

  const definir = (chave) => (evento) => setDados((atual) => ({ ...atual, [chave]: evento.target.value }));

  async function salvar() {
    const faltando = entidade.campos.find((c) => c.obrigatorio && !dados[c.chave].trim());
    if (faltando) {
      setErro(`O campo "${faltando.rotulo}" é obrigatório.`);
      return;
    }
    setSalvando(true);
    try {
      const corpo = { method: registro ? 'PUT' : 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ dados }) };
      const caminho = registro ? `/api/${entidade.chave}/${registro.id}` : `/api/${entidade.chave}`;
      await chamarApi(caminho, corpo);
      aoSalvar(registro ? 'Registro atualizado.' : 'Registro criado.');
    } catch (excecao) {
      setErro(excecao.message);
      setSalvando(false);
    }
  }

  return (
    <Dialog open title={registro ? `Editar · ${entidade.titulo}` : `Novo registro · ${entidade.titulo}`}
      accent onClose={aoFechar} width={Math.min(760, window.innerWidth - 24)}
      footer={<>
        <Button variant="ghost" onClick={aoFechar}>Cancelar</Button>
        <Button variant="primary" disabled={salvando} onClick={salvar}>{salvando ? 'Salvando…' : 'Salvar registro'}</Button>
      </>}>
      {erro ? <div className="form-erro"><Alert tone="warning" title="Não foi possível salvar">{erro}</Alert></div> : null}
      <div className="form-grade">
        <CamposDeFormulario entidade={entidade} entidades={entidades}
          referencias={referencias} dados={dados} definir={definir} />
      </div>
      {contratos.length > 0 && (
        <div className="form-anexos">
          {contratos.map((anexo) => (
            <a key={anexo.id} className="link-externo"
              href={`/api/${entidade.chave}/${registro.id}/anexos/${anexo.id}`} target="_blank" rel="noreferrer">
              <Icone nome="clipe" tamanho={13} /> {anexo.nome}
            </a>
          ))}
        </div>
      )}
    </Dialog>
  );
}

function tamanhoLegivel(n) {
  return n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
}

/* Anexos de um registro: lista, upload e exclusão de arquivos (contratos etc.). */
function ListaAnexos({ entidade, registro, avisar }) {
  const podeEditar = !usarUsuario()?.somenteLeitura;
  const [anexos, setAnexos] = useState(null);
  const [enviando, setEnviando] = useState(false);
  const [categoria, setCategoria] = useState('Contrato');
  const arquivoRef = useRef(null);
  const base = `/api/${entidade.chave}/${registro.id}/anexos`;

  const carregar = useCallback(async () => {
    const corpo = await chamarApi(base);
    setAnexos(corpo.anexos);
  }, [base]);

  useEffect(() => {
    carregar().catch((excecao) => avisar('warning', excecao.message));
  }, [carregar]);

  async function enviar(evento) {
    const arquivo = evento.target.files[0];
    evento.target.value = '';
    if (!arquivo) return;
    setEnviando(true);
    try {
      await chamarApi(base, {
        method: 'POST',
        headers: {
          'content-type': arquivo.type || 'application/octet-stream',
          'x-nome-arquivo': encodeURIComponent(arquivo.name),
          'x-categoria-anexo': encodeURIComponent(categoria),
        },
        body: arquivo,
      });
      avisar('success', 'Arquivo anexado.');
      await carregar();
    } catch (excecao) {
      avisar('warning', excecao.message);
    }
    setEnviando(false);
  }

  async function excluir(anexo) {
    try {
      await chamarApi(`${base}/${anexo.id}`, { method: 'DELETE' });
      avisar('success', 'Anexo excluído.');
      await carregar();
    } catch (excecao) {
      avisar('warning', excecao.message);
    }
  }

  return (
    <div>
      <input ref={arquivoRef} type="file" hidden onChange={enviar} />
      {anexos === null ? (
        <div className="lista-anexos" role="status" aria-label="Carregando anexos">
          {[0, 1].map((i) => (
            <div key={i} className="linha-anexo">
              <Esqueleto largura="16px" altura="16px" />
              <div className="linha-anexo-nome">
                <Esqueleto largura="55%" style={{ marginBottom: 'var(--space-1)' }} />
                <Esqueleto largura="28%" altura="var(--space-3)" />
              </div>
              <Esqueleto largura="72px" altura="var(--space-5)" />
            </div>
          ))}
        </div>
      ) : anexos.length === 0 ? (
        <p className="toolbar-contagem">Nenhum arquivo anexado ainda. Contratos e documentos de até 20 MB.</p>
      ) : (
        <div className="lista-anexos">
          {anexos.map((anexo) => (
            <div key={anexo.id} className="linha-anexo">
              <Icone nome="clipe" tamanho={15} />
              <div className="linha-anexo-nome">
                <a className="link-detalhe" href={`${base}/${anexo.id}`} target="_blank" rel="noreferrer">{anexo.nome}</a>
                <div className="linha-secundaria">{tamanhoLegivel(anexo.tamanho)} · {dataBr(anexo.criadoEm)}</div>
              </div>
              <Badge tone="grey" variant="soft">{anexo.categoria || 'Documento'}</Badge>
              {podeEditar && (
                <IconButton variant="ghost" size="sm" label="Excluir anexo" onClick={() => excluir(anexo)}>
                  <Icone nome="lixeira" />
                </IconButton>
              )}
            </div>
          ))}
        </div>
      )}
      {podeEditar && (
        <div className="anexos-acoes">
          <div className="anexos-categoria">
            <Select label="Categoria" options={['Contrato', 'Proposta', 'Outro documento']}
              value={categoria} onChange={(evento) => setCategoria(evento.target.value)} />
          </div>
          <Button variant="secondary" size="sm" disabled={enviando} leadingIcon={<Icone nome="mais" />}
            onClick={() => arquivoRef.current.click()}>
            {enviando ? 'Enviando…' : 'Anexar arquivo'}
          </Button>
        </div>
      )}
    </div>
  );
}

function DialogoAnexos({ entidade, registro, aoFechar, avisar }) {
  return (
    <Dialog open title={`Anexos · ${registro.dados[entidade.campos[0].chave]}`} accent
      onClose={aoFechar} width={Math.min(640, window.innerWidth - 24)}
      footer={<Button variant="ghost" onClick={aoFechar}>Fechar</Button>}>
      <ListaAnexos entidade={entidade} registro={registro} avisar={avisar} />
    </Dialog>
  );
}

/* Listagem genérica de uma entidade, com busca, exportação e importação. */
function PainelEntidade({ entidade, entidades, avisar }) {
  const podeEditar = !usarUsuario()?.somenteLeitura;
  const [registros, setRegistros] = useState(null);
  const [referencias, setReferencias] = useState({});
  const [anexosPorDono, setAnexosPorDono] = useState({});
  const [busca, setBusca] = useState('');
  const [dialogo, setDialogo] = useState(null); // null | { registro: objeto | null }
  const [excluindo, setExcluindo] = useState(null);
  const [anexosDe, setAnexosDe] = useState(null);

  const refEntidades = useMemo(() => {
    const conjunto = new Set(entidade.campos.filter((c) => c.tipo === 'ref').map((c) => c.refEntidade));
    if (entidade.chave === 'setores') conjunto.add('colaboradores');
    return [...conjunto];
  }, [entidade]);

  const carregar = useCallback(async () => {
    const [propria, ...outras] = await Promise.all([
      chamarApi(`/api/${entidade.chave}`),
      ...refEntidades.map((chave) => chamarApi(`/api/${chave}`)),
    ]);
    setRegistros(propria.registros);
    setReferencias(Object.fromEntries(refEntidades.map((chave, i) => [chave, outras[i].registros])));
    if (entidade.chave === 'fornecedores') {
      const { anexos } = await chamarApi(`/api/${entidade.chave}/anexos`);
      const mapa = {};
      for (const anexo of anexos) (mapa[anexo.dono] = mapa[anexo.dono] || []).push(anexo);
      setAnexosPorDono(mapa);
    }
  }, [entidade.chave, refEntidades]);

  useEffect(() => {
    setRegistros(null);
    setBusca('');
    carregar().catch((excecao) => avisar('warning', excecao.message));
  }, [carregar]);

  const primeiroCampo = (chave) => entidades.find((e) => e.chave === chave).campos[0].chave;
  const nomeRef = (campo, valor) => {
    if (!valor) return '';
    const alvo = (referencias[campo.refEntidade] || []).find((r) => String(r.id) === valor);
    return alvo ? alvo.dados[primeiroCampo(campo.refEntidade)] : '';
  };
  const valorCelula = (campo, registro) => {
    const bruto = registro.dados[campo.chave] || '';
    if (campo.tipo === 'ref') return nomeRef(campo, bruto);
    if (campo.tipo === 'data') return dataBr(bruto);
    if (campo.tipo === 'numero') return formatarNumero(bruto);
    return bruto;
  };

  const filtro = busca.trim().toLowerCase();
  const visiveis = (registros || []).filter((r) => !filtro
    || entidade.campos.some((c) => valorCelula(c, r).toLowerCase().includes(filtro)));

  const colunas = entidade.colunas
    ? entidade.colunas.map((chave) => entidade.campos.find((c) => c.chave === chave)).filter(Boolean)
    : entidade.campos.slice(0, 5);
  const colunasEsqueleto = colunas.length
    + (entidade.chave === 'fornecedores' ? 1 : 0)
    + (entidade.chave === 'setores' ? 1 : 0)
    + (!entidade.detalhe ? 1 : 0);
  const contagemColaboradores = (setorId) =>
    (referencias['colaboradores'] || []).filter((c) => c.dados.setor === String(setorId)).length;

  async function excluir() {
    try {
      await chamarApi(`/api/${entidade.chave}/${excluindo.id}`, { method: 'DELETE' });
      setExcluindo(null);
      avisar('success', 'Registro excluído.');
      await carregar();
    } catch (excecao) {
      setExcluindo(null);
      avisar('warning', excecao.message);
    }
  }

  return (
    <div>
      <SectionHeading className="titulo-secao" overline={entidade.finalidade} title={entidade.titulo} subtitle={entidade.descricao} />

      <div className="toolbar">
        <div className="toolbar-busca">
          <Input label="Buscar" placeholder="Filtrar em qualquer campo"
            value={busca} onChange={(evento) => setBusca(evento.target.value)} />
        </div>
        {podeEditar && (
          <Button variant="primary" leadingIcon={<Icone nome="mais" />} onClick={() => setDialogo({ registro: null })}>Novo registro</Button>
        )}
      </div>

      <p className="toolbar-contagem">
        {registros === null ? <Esqueleto largura="120px" altura="var(--space-3)" style={{ display: 'inline-block' }} />
          : filtro ? `${visiveis.length} de ${registros.length} registro(s)`
          : `${registros.length} registro(s)`}
      </p>

      {registros === null ? (
        <EsqueletoTabela colunas={colunasEsqueleto} />
      ) : (
      <div className="tabela-envelope">
        {visiveis.length === 0 ? (
          <div className="vazio">
            <p>{filtro ? 'Nenhum registro corresponde à busca.' : 'Nenhum registro nesta área ainda.'}</p>
            {!filtro && podeEditar && (
              <Button variant="secondary" leadingIcon={<Icone nome="mais" />} onClick={() => setDialogo({ registro: null })}>
                Cadastrar o primeiro
              </Button>
            )}
          </div>
        ) : (
          <table className="tabela">
            <thead>
              <tr>
                {colunas.map((c) => <th key={c.chave}>{c.rotulo}</th>)}
                {entidade.chave === 'fornecedores' && <th>Contrato</th>}
                {entidade.chave === 'setores' && <th>Colaboradores</th>}
                {!entidade.detalhe && <th className="celula-acoes">Ações</th>}
              </tr>
            </thead>
            <tbody>
              {visiveis.map((registro) => (
                <tr key={registro.id}
                  className={entidade.detalhe ? 'linha-clicavel' : undefined}
                  onClick={entidade.detalhe ? (evento) => {
                    if (evento.target.closest('a, button')) return;
                    window.location.hash = `/${entidade.chave}/${registro.id}`;
                  } : undefined}>
                  {colunas.map((c, indice) => {
                    const texto = valorCelula(c, registro);
                    return (
                      <td key={c.chave} className={indice === 0 ? 'celula-forte' : undefined} title={texto}>
                        {indice === 0 && entidade.detalhe
                          ? <a className="link-detalhe" href={`#/${entidade.chave}/${registro.id}`}>{texto || '·'}</a>
                          : c.tipo === 'url' && texto
                            ? <a className="link-externo" href={hrefSeguro(texto)} target="_blank" rel="noreferrer">Abrir <Icone nome="externo" tamanho={13} /></a>
                            : texto || '·'}
                      </td>
                    );
                  })}
                  {entidade.chave === 'fornecedores' && (() => {
                    const contrato = (anexosPorDono[`${entidade.chave}:${registro.id}`] || [])
                      .find((a) => a.categoria === 'Contrato');
                    return (
                      <td>
                        {contrato
                          ? <a className="link-externo" title={contrato.nome}
                              href={`/api/${entidade.chave}/${registro.id}/anexos/${contrato.id}`}
                              target="_blank" rel="noreferrer">
                              Ver contrato <Icone nome="externo" tamanho={13} />
                            </a>
                          : '·'}
                      </td>
                    );
                  })()}
                  {entidade.chave === 'setores' && <td>{contagemColaboradores(registro.id)}</td>}
                  {!entidade.detalhe && (
                    <td className="celula-acoes">
                      {entidade.anexos && (
                        <span className="acao">
                          <IconButton variant="ghost" size="sm" label="Anexos" onClick={() => setAnexosDe(registro)}>
                            <Icone nome="clipe" />
                          </IconButton>
                        </span>
                      )}
                      <span className="acao">
                        <IconButton variant="ghost" size="sm" label="Editar" onClick={() => setDialogo({ registro })}>
                          <Icone nome="lapis" />
                        </IconButton>
                      </span>
                      <span className="acao">
                        <IconButton variant="ghost" size="sm" label="Excluir" onClick={() => setExcluindo(registro)}>
                          <Icone nome="lixeira" />
                        </IconButton>
                      </span>
                    </td>
                  )}
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
      )}

      {dialogo && (
        <DialogoRegistro entidade={entidade} entidades={entidades} referencias={referencias}
          registro={dialogo.registro}
          aoFechar={() => setDialogo(null)}
          aoSalvar={async (mensagem) => {
            setDialogo(null);
            avisar('success', mensagem);
            await carregar();
          }} />
      )}

      {anexosDe && (
        <DialogoAnexos entidade={entidade} registro={anexosDe} avisar={avisar}
          aoFechar={() => {
            setAnexosDe(null);
            carregar().catch(() => {});
          }} />
      )}

      {excluindo && (
        <Dialog open title="Excluir registro" accent onClose={() => setExcluindo(null)}
          footer={<>
            <Button variant="ghost" onClick={() => setExcluindo(null)}>Cancelar</Button>
            <Button variant="primary" onClick={excluir}>Excluir</Button>
          </>}>
          <p>Excluir "{nomePrincipal(entidade, excluindo.dados, referencias, entidades)}" de {entidade.titulo}? Essa ação não pode ser desfeita.</p>
        </Dialog>
      )}
    </div>
  );
}

/* Acompanhamento dos artefatos entregáveis da consultoria (prestação de contas). */
function PainelArtefatos({ entidade, entidades, avisar }) {
  const podeEditar = !usarUsuario()?.somenteLeitura;
  const [registros, setRegistros] = useState(null);
  const [dialogo, setDialogo] = useState(null);

  const carregar = useCallback(async () => {
    const { registros: lista } = await chamarApi('/api/artefatos');
    lista.sort((a, b) => (a.dados.numero || '').localeCompare(b.dados.numero || ''));
    setRegistros(lista);
  }, []);

  useEffect(() => {
    carregar().catch((excecao) => avisar('warning', excecao.message));
  }, [carregar]);

  const lista = registros || [];
  const entregues = lista.filter((r) => r.dados.status === 'Entregue').length;
  const percentual = lista.length ? Math.round((entregues / lista.length) * 100) : 0;

  return (
    <div>
      <SectionHeading className="titulo-secao" overline="Consultoria de tecnologia · prestação de contas"
        title="Artefatos da consultoria"
        subtitle="Status de desenvolvimento e link de cada entregável da consultoria de tecnologia." />

      <div className="toolbar">
        <div className="toolbar-busca">
          <div className="progresso-trilha"><div className="progresso-preenchimento" style={{ width: `${percentual}%` }} /></div>
          <p className="progresso-legenda">{entregues} de {lista.length} artefatos entregues</p>
        </div>
        {podeEditar && (
          <Button variant="primary" leadingIcon={<Icone nome="mais" />} onClick={() => setDialogo({ registro: null })}>Novo artefato</Button>
        )}
      </div>

      {registros === null ? (
        <EsqueletoTabela colunas={6} />
      ) : (
      <div className="tabela-envelope">
        <table className="tabela">
          <thead>
            <tr>
              <th>Nº</th><th>Artefato</th><th>Status</th><th>Link</th><th>Atualizado</th>
              <th className="celula-acoes">Ações</th>
            </tr>
          </thead>
          <tbody>
            {lista.map((registro) => {
              const aparencia = APARENCIA_STATUS[registro.dados.status] || APARENCIA_STATUS['Não iniciado'];
              const resumo = String(registro.dados.descricao || '').split('\n')[0];
              return (
                <tr key={registro.id}>
                  <td className="artefato-numero">{registro.dados.numero}</td>
                  <td className="celula-forte celula-artefato">
                    <a className="link-detalhe" href={`#/artefatos/${registro.dados.numero}`}>{registro.dados.nome}</a>
                    {resumo && <div className="linha-secundaria">{resumo}</div>}
                  </td>
                  <td><Badge tone={aparencia.tone} variant={aparencia.variant}>{registro.dados.status || 'Não iniciado'}</Badge></td>
                  <td>
                    {registro.dados.link
                      ? <a className="link-externo" href={hrefSeguro(registro.dados.link)} target="_blank" rel="noreferrer">Abrir documento <Icone nome="externo" tamanho={13} /></a>
                      : '·'}
                  </td>
                  <td>{dataBr(registro.atualizadoEm)}</td>
                  <td className="celula-acoes">
                    <IconButton variant="ghost" size="sm" label="Editar" onClick={() => setDialogo({ registro })}>
                      <Icone nome="lapis" />
                    </IconButton>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      )}

      {dialogo && (
        <DialogoRegistro entidade={entidade} entidades={entidades} referencias={{}}
          registro={dialogo.registro}
          aoFechar={() => setDialogo(null)}
          aoSalvar={async (mensagem) => {
            setDialogo(null);
            avisar('success', mensagem);
            await carregar();
          }} />
      )}
    </div>
  );
}

/* Ficha completa de um registro (ex.: fornecedor): todos os campos, edição,
   exclusão e anexos com categoria (contratos, propostas e documentos). */
function PainelRegistroDetalhe({ entidade, id, entidades, avisar, aoNavegar }) {
  const podeEditar = !usarUsuario()?.somenteLeitura;
  const [registro, setRegistro] = useState(null);
  const [falha, setFalha] = useState(false);
  const [referencias, setReferencias] = useState({});
  const [excluindo, setExcluindo] = useState(false);
  // Edição inline: os campos abrem na própria ficha, sem diálogo.
  const [rascunho, setRascunho] = useState(null);
  const [erroEdicao, setErroEdicao] = useState('');
  const [salvando, setSalvando] = useState(false);
  // Relações reversas: outras entidades que apontam para esta (ex.: sistemas -> fornecedor).
  const [reversos, setReversos] = useState({});
  const [dialogoNovo, setDialogoNovo] = useState(null); // { entidade, iniciais, referencias }

  const refEntidades = useMemo(
    () => [...new Set(entidade.campos.filter((c) => c.tipo === 'ref').map((c) => c.refEntidade))],
    [entidade],
  );
  const entidadesReversas = useMemo(
    () => entidades.flatMap((e) => e.campos
      .filter((c) => c.tipo === 'ref' && c.refEntidade === entidade.chave)
      .map((c) => ({ entidade: e, campo: c.chave }))),
    [entidades, entidade.chave],
  );

  const carregar = useCallback(async () => {
    try {
      const [proprio, ...outras] = await Promise.all([
        chamarApi(`/api/${entidade.chave}/${id}`),
        ...refEntidades.map((chave) => chamarApi(`/api/${chave}`)),
      ]);
      setRegistro(proprio.registro);
      setReferencias(Object.fromEntries(refEntidades.map((chave, i) => [chave, outras[i].registros])));
      if (entidadesReversas.length > 0) {
        const listas = await Promise.all(entidadesReversas.map((r) => chamarApi(`/api/${r.entidade.chave}`)));
        const mapa = {};
        entidadesReversas.forEach((r, i) => {
          mapa[r.entidade.chave] = listas[i].registros.filter((reg) => String(reg.dados[r.campo] || '') === String(id));
        });
        setReversos(mapa);
      }
    } catch {
      setFalha(true);
    }
  }, [entidade.chave, id, refEntidades, entidadesReversas]);

  useEffect(() => {
    carregar();
  }, [carregar]);

  const voltar = (
    <a className="voltar" href={`#/${entidade.chave}`}><Icone nome="voltar" tamanho={14} /> {entidade.titulo}</a>
  );
  if (falha) {
    return (
      <div>
        {voltar}
        <Alert tone="warning" title="Registro não encontrado">Volte para a lista e escolha um item.</Alert>
      </div>
    );
  }
  if (registro === null) {
    return <EsqueletoFicha itens={entidade.campos.filter((c) => c.tipo !== 'textarea').length || 6} />;
  }

  const { dados } = registro;
  const primeiroCampo = (chave) => entidades.find((e) => e.chave === chave).campos[0].chave;
  const valorDe = (campo) => {
    const bruto = dados[campo.chave] || '';
    if (campo.tipo === 'ref') {
      const alvo = (referencias[campo.refEntidade] || []).find((r) => String(r.id) === bruto);
      return alvo ? alvo.dados[primeiroCampo(campo.refEntidade)] : '';
    }
    if (campo.tipo === 'data') return dataBr(bruto);
    if (campo.tipo === 'numero') return formatarNumero(bruto);
    return bruto;
  };

  async function excluir() {
    try {
      await chamarApi(`/api/${entidade.chave}/${registro.id}`, { method: 'DELETE' });
      avisar('success', 'Registro excluído.');
      aoNavegar(entidade.chave);
    } catch (excecao) {
      setExcluindo(false);
      avisar('warning', excecao.message);
    }
  }

  function iniciarEdicao() {
    setRascunho({ ...dadosVazios(entidade), ...registro.dados });
    setErroEdicao('');
  }

  async function salvarEdicao() {
    const faltando = entidade.campos.find((c) => c.obrigatorio && !rascunho[c.chave].trim());
    if (faltando) {
      setErroEdicao(`O campo "${faltando.rotulo}" é obrigatório.`);
      return;
    }
    setSalvando(true);
    try {
      await chamarApi(`/api/${entidade.chave}/${registro.id}`, {
        method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ dados: rascunho }),
      });
      setRascunho(null);
      avisar('success', 'Registro atualizado.');
      await carregar();
    } catch (excecao) {
      setErroEdicao(excecao.message);
    }
    setSalvando(false);
  }

  // Abre o cadastro de um registro vinculado, carregando TODOS os vínculos do alvo
  // (ex.: um acesso precisa da lista de sistemas e da de colaboradores) e já deixa
  // o vínculo de volta pré-selecionado.
  async function abrirCadastroVinculado(rev) {
    const alvo = rev.entidade;
    const refs = [...new Set(alvo.campos.filter((c) => c.tipo === 'ref').map((c) => c.refEntidade))];
    try {
      const listas = await Promise.all(refs.map((chave) => chamarApi(`/api/${chave}`)));
      setDialogoNovo({
        entidade: alvo,
        iniciais: { [rev.campo]: String(registro.id) },
        referencias: Object.fromEntries(refs.map((chave, i) => [chave, listas[i].registros])),
      });
    } catch (excecao) {
      avisar('warning', excecao.message);
    }
  }

  const camposCurtos = entidade.campos.filter((c) => c.tipo !== 'textarea');
  const camposLongos = entidade.campos.filter((c) => c.tipo === 'textarea');
  const editando = rascunho !== null;

  return (
    <div>
      {voltar}
      <SectionHeading className="titulo-secao" overline={entidade.finalidade}
        title={nomePrincipal(entidade, dados, referencias, entidades) || `Registro ${registro.id}`} />

      <div className="detalhe-acoes">
        <span className="toolbar-contagem detalhe-atualizado">Atualizado em {dataBr(registro.atualizadoEm)}</span>
        {editando ? (
          <>
            <Button variant="primary" disabled={salvando} leadingIcon={<Icone nome="lapis" />} onClick={salvarEdicao}>
              {salvando ? 'Salvando…' : 'Salvar alterações'}
            </Button>
            <Button variant="ghost" disabled={salvando} onClick={() => setRascunho(null)}>Cancelar</Button>
          </>
        ) : podeEditar ? (
          <>
            <Button variant="secondary" leadingIcon={<Icone nome="lapis" />} onClick={iniciarEdicao}>Editar</Button>
            <Button variant="ghost" leadingIcon={<Icone nome="lixeira" />} onClick={() => setExcluindo(true)}>Excluir</Button>
          </>
        ) : null}
      </div>

      {editando && erroEdicao && (
        <div className="form-erro"><Alert tone="warning" title="Não foi possível salvar">{erroEdicao}</Alert></div>
      )}

      <div className="detalhe-secao">
        <Card accent>
          {editando ? (
            <div className="form-grade">
              <CamposDeFormulario entidade={entidade} entidades={entidades} referencias={referencias}
                dados={rascunho}
                definir={(chave) => (evento) => setRascunho((atual) => ({ ...atual, [chave]: evento.target.value }))} />
            </div>
          ) : (
            <>
              <div className="ficha-grade">
                {camposCurtos.map((campo) => (
                  <div key={campo.chave} className="ficha-item">
                    <div className="ficha-rotulo">{campo.rotulo}</div>
                    <div className="ficha-valor">
                      {campo.tipo === 'url' && dados[campo.chave]
                        ? <a className="link-externo" href={hrefSeguro(dados[campo.chave])} target="_blank" rel="noreferrer">Abrir <Icone nome="externo" tamanho={13} /></a>
                        : valorDe(campo) || '·'}
                    </div>
                  </div>
                ))}
              </div>
              {camposLongos.map((campo) => (dados[campo.chave] ? (
                <div key={campo.chave} className="ficha-item-longa">
                  <div className="ficha-rotulo">{campo.rotulo}</div>
                  <div className="detalhe-texto">{dados[campo.chave]}</div>
                </div>
              ) : null))}
            </>
          )}
        </Card>
      </div>

      {entidadesReversas.map((rev) => {
        const lista = reversos[rev.entidade.chave] || [];
        return (
          <div key={rev.entidade.chave} className="detalhe-secao">
            <div className="grupo-menu-rotulo">{rev.entidade.titulo}</div>
            <Card>
              {lista.length === 0 ? (
                <p className="toolbar-contagem" style={{ margin: 0 }}>
                  Nenhum registro de {rev.entidade.titulo} vinculado ainda.
                </p>
              ) : (
                <div className="lista-vinculos">
                  {lista.map((reg) => (
                    <a key={reg.id} className="linha-vinculo" href={`#/${rev.entidade.chave}/${reg.id}`}>
                      <Icone nome={ICONE_MENU[rev.entidade.chave] || 'painel'} tamanho={15} />
                      <span className="link-detalhe">{rotuloReverso(rev, reg)}</span>
                    </a>
                  ))}
                </div>
              )}
              {podeEditar && (
                <div className="vinculos-acao">
                  <Button variant="secondary" size="sm" leadingIcon={<Icone nome="mais" />}
                    onClick={() => abrirCadastroVinculado(rev)}>
                    {ROTULO_NOVO[rev.entidade.chave] || 'Adicionar registro'}
                  </Button>
                </div>
              )}
            </Card>
          </div>
        );
      })}

      {entidade.anexos && (
        <div className="detalhe-secao">
          <div className="grupo-menu-rotulo">Contratos, propostas e documentos</div>
          <Card>
            <ListaAnexos entidade={entidade} registro={registro} avisar={avisar} />
          </Card>
        </div>
      )}

      {dialogoNovo && (
        <DialogoRegistro entidade={dialogoNovo.entidade} entidades={entidades}
          referencias={dialogoNovo.referencias}
          registro={null} iniciais={dialogoNovo.iniciais}
          aoFechar={() => setDialogoNovo(null)}
          aoSalvar={async (mensagem) => { setDialogoNovo(null); avisar('success', mensagem); await carregar(); }} />
      )}

      {excluindo && (
        <Dialog open title="Excluir registro" accent onClose={() => setExcluindo(false)}
          footer={<>
            <Button variant="ghost" onClick={() => setExcluindo(false)}>Cancelar</Button>
            <Button variant="primary" onClick={excluir}>Excluir</Button>
          </>}>
          <p>Excluir "{nomePrincipal(entidade, dados, referencias, entidades)}" de {entidade.titulo}? Os anexos vão junto. Essa ação não pode ser desfeita.</p>
        </Dialog>
      )}
    </div>
  );
}

/* Página de um artefato: o que é o documento, status, link e áreas que o alimentam. */
function PainelArtefatoDetalhe({ numero, entidade, entidades, avisar, aoNavegar }) {
  const podeEditar = !usarUsuario()?.somenteLeitura;
  const [registros, setRegistros] = useState(null);
  // Edição inline: os campos abrem na própria página, sem diálogo.
  const [rascunho, setRascunho] = useState(null);
  const [erroEdicao, setErroEdicao] = useState('');
  const [salvando, setSalvando] = useState(false);

  const carregar = useCallback(async () => {
    const { registros: lista } = await chamarApi('/api/artefatos');
    setRegistros(lista);
  }, []);

  useEffect(() => {
    carregar().catch((excecao) => avisar('warning', excecao.message));
  }, [carregar]);

  if (registros === null) {
    return (
      <div role="status" aria-label="Carregando artefato">
        <Esqueleto largura="200px" altura="var(--space-4)" style={{ marginBottom: 'var(--space-4)' }} />
        <Esqueleto largura="50%" altura="var(--space-6)" style={{ marginBottom: 'var(--space-3)' }} />
        <div className="detalhe-acoes">
          <Esqueleto largura="120px" altura="var(--space-6)" />
          <Esqueleto largura="160px" altura="var(--space-4)" />
        </div>
        <div className="detalhe-secao">
          <Card accent>
            <Esqueleto largura="95%" style={{ marginBottom: 'var(--space-2)' }} />
            <Esqueleto largura="90%" style={{ marginBottom: 'var(--space-2)' }} />
            <Esqueleto largura="70%" />
          </Card>
        </div>
      </div>
    );
  }

  const registro = registros.find((r) => r.dados.numero === numero);
  if (!registro) {
    return (
      <div>
        <a className="voltar" href="#/artefatos"><Icone nome="voltar" tamanho={14} /> Artefatos da consultoria</a>
        <Alert tone="warning" title="Artefato não encontrado">
          Não existe artefato com o número {numero}. Volte para a lista e escolha um item.
        </Alert>
      </div>
    );
  }

  const { dados } = registro;
  const aparencia = APARENCIA_STATUS[dados.status] || APARENCIA_STATUS['Não iniciado'];
  const linhas = String(dados.descricao || '').split('\n');
  const resumo = linhas[0] || '';
  const corpoDescricao = linhas.slice(1).join('\n').trim();
  const fontes = (FONTES_ARTEFATO[numero] || [])
    .map((chave) => entidades.find((e) => e.chave === chave))
    .filter(Boolean);

  return (
    <div>
      <a className="voltar" href="#/artefatos"><Icone nome="voltar" tamanho={14} /> Artefatos da consultoria</a>
      <SectionHeading className="titulo-secao"
        overline={`Artefato ${dados.numero} · Entregável da consultoria`}
        title={dados.nome} subtitle={resumo} />

      <div className="detalhe-acoes">
        <Badge tone={aparencia.tone} variant={aparencia.variant}>{dados.status || 'Não iniciado'}</Badge>
        <span className="toolbar-contagem detalhe-atualizado">Atualizado em {dataBr(registro.atualizadoEm)}</span>
        {dados.link && !rascunho && (
          <a className="link-externo" href={hrefSeguro(dados.link)} target="_blank" rel="noreferrer">
            Abrir documento <Icone nome="externo" tamanho={13} />
          </a>
        )}
        {rascunho ? (
          <>
            <Button variant="primary" disabled={salvando} leadingIcon={<Icone nome="lapis" />}
              onClick={async () => {
                const faltando = entidade.campos.find((c) => c.obrigatorio && !rascunho[c.chave].trim());
                if (faltando) {
                  setErroEdicao(`O campo "${faltando.rotulo}" é obrigatório.`);
                  return;
                }
                setSalvando(true);
                try {
                  await chamarApi(`/api/artefatos/${registro.id}`, {
                    method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ dados: rascunho }),
                  });
                  setRascunho(null);
                  avisar('success', 'Artefato atualizado.');
                  await carregar();
                } catch (excecao) {
                  setErroEdicao(excecao.message);
                }
                setSalvando(false);
              }}>
              {salvando ? 'Salvando…' : 'Salvar alterações'}
            </Button>
            <Button variant="ghost" disabled={salvando} onClick={() => setRascunho(null)}>Cancelar</Button>
          </>
        ) : podeEditar ? (
          <Button variant="secondary" leadingIcon={<Icone nome="lapis" />}
            onClick={() => {
              setRascunho({ ...dadosVazios(entidade), ...registro.dados });
              setErroEdicao('');
            }}>
            Editar artefato
          </Button>
        ) : null}
      </div>

      {rascunho && erroEdicao && (
        <div className="form-erro"><Alert tone="warning" title="Não foi possível salvar">{erroEdicao}</Alert></div>
      )}

      <div className="detalhe-secao">
        <Card accent>
          {rascunho ? (
            <div className="form-grade">
              <CamposDeFormulario entidade={entidade} entidades={entidades} referencias={{}}
                dados={rascunho}
                definir={(chave) => (evento) => setRascunho((atual) => ({ ...atual, [chave]: evento.target.value }))} />
            </div>
          ) : (
            <div className="detalhe-texto">{corpoDescricao || 'Sem descrição cadastrada ainda. Use "Editar artefato" para preencher.'}</div>
          )}
        </Card>
      </div>

      {fontes.length > 0 && (
        <div className="detalhe-secao">
          <div className="grupo-menu-rotulo">Alimentado pelas áreas</div>
          <div className="detalhe-areas">
            {fontes.map((fonte) => (
              <Button key={fonte.chave} variant="ghost" size="sm"
                leadingIcon={<Icone nome={ICONE_MENU[fonte.chave] || 'painel'} tamanho={14} />}
                onClick={() => aoNavegar(fonte.chave)}>
                {fonte.titulo}
              </Button>
            ))}
          </div>
        </div>
      )}

      {dados.observacoes && (
        <div className="detalhe-secao">
          <div className="grupo-menu-rotulo">Observações</div>
          <Card>
            <div className="detalhe-texto">{dados.observacoes}</div>
          </Card>
        </div>
      )}

    </div>
  );
}

/* Visão geral: contagens das frentes, progresso dos artefatos e organização. */
function PainelResumo({ entidades, aoAbrir }) {
  const usuario = usarUsuario();
  const veArtefatos = podeArea(usuario, 'artefatos');
  const [contagens, setContagens] = useState({});
  const [artefatos, setArtefatos] = useState([]);
  const [carregando, setCarregando] = useState(true);

  useEffect(() => {
    chamarApi('/api/resumo').then((corpo) => setContagens(corpo.contagens)).catch(() => {}).finally(() => setCarregando(false));
    if (veArtefatos) chamarApi('/api/artefatos').then((corpo) => setArtefatos(corpo.registros)).catch(() => {});
  }, [veArtefatos]);

  // Só as áreas que o papel enxerga aparecem nos cartões.
  const frentes = entidades.slice(0, 7).filter((e) => podeArea(usuario, e.chave));
  const entregues = artefatos.filter((r) => r.dados.status === 'Entregue').length;
  const percentual = artefatos.length ? Math.round((entregues / artefatos.length) * 100) : 0;

  return (
    <div>
      <SectionHeading className="titulo-secao" overline="BNI Brasil · Gestão de TI"
        title="Visão geral"
        subtitle="Inventário e gestão da tecnologia da BNI Brasil: acessos, ativos, sistemas, fornecedores, custos e dados pessoais." />
      {carregando ? <EsqueletoCartoes n={Math.max(3, frentes.length)} /> : (
      <div className="resumo-grade">
        {frentes.map((entidade) => (
          <div key={entidade.chave} className="cartao-frente" onClick={() => aoAbrir(entidade.chave)}>
            <Card accent>
              <Stat value={String(contagens[entidade.chave] ?? '·')} label={entidade.titulo} tone="red" />
              <div className="cartao-frente-entregavel">{entidade.finalidade}</div>
            </Card>
          </div>
        ))}
      </div>
      )}

      <div className="resumo-secao">
        <SectionHeading className="titulo-secao" overline="Cadastros de apoio e contrato"
          title="Organização e consultoria"
          subtitle="Estrutura da BNI Brasil e acompanhamento dos entregáveis da consultoria de tecnologia." />
        {carregando ? <EsqueletoCartoes n={veArtefatos ? 3 : 2} /> : (
        <div className="resumo-grade">
          {veArtefatos && (
            <div className="cartao-frente" onClick={() => aoAbrir('artefatos')}>
              <Card accent>
                <Stat value={`${entregues} / ${artefatos.length || 10}`} label="Artefatos entregues" tone="red" />
                <div className="progresso-trilha"><div className="progresso-preenchimento" style={{ width: `${percentual}%` }} /></div>
                <div className="cartao-frente-entregavel">Status e link de cada entregável da consultoria</div>
              </Card>
            </div>
          )}
          <div className="cartao-frente" onClick={() => aoAbrir('colaboradores')}>
            <Card accent>
              <Stat value={String(contagens['colaboradores'] ?? '·')} label="Colaboradores" tone="black" />
              <div className="cartao-frente-entregavel">Pessoas da BNI Brasil</div>
            </Card>
          </div>
          <div className="cartao-frente" onClick={() => aoAbrir('setores')}>
            <Card accent>
              <Stat value={String(contagens['setores'] ?? '·')} label="Setores" tone="black" />
              <div className="cartao-frente-entregavel">Estrutura organizacional</div>
            </Card>
          </div>
        </div>
        )}
      </div>

      <div className="resumo-aviso">
        <Alert tone="info" title="Como preencher">
          Uma linha por item. O que não souber, registre como "A confirmar" em vez de deixar em branco.
          Cadastre setores e colaboradores primeiro: acessos, máquinas e custos apontam para eles.
        </Alert>
      </div>
    </div>
  );
}

/* Configurações da conta do usuário: troca da própria senha. */
function PainelConfiguracoes({ avisar }) {
  const usuario = usarUsuario();
  const [atual, setAtual] = useState('');
  const [nova, setNova] = useState('');
  const [confirma, setConfirma] = useState('');
  const [erro, setErro] = useState('');
  const [salvando, setSalvando] = useState(false);

  async function trocar(evento) {
    if (evento) evento.preventDefault();
    setErro('');
    if (nova.length < 8) { setErro('A nova senha deve ter ao menos 8 caracteres.'); return; }
    if (nova !== confirma) { setErro('A confirmação não confere com a nova senha.'); return; }
    setSalvando(true);
    try {
      await chamarApi('/api/minha-senha', {
        method: 'POST', headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ atual, nova }),
      });
      setAtual(''); setNova(''); setConfirma('');
      avisar('success', 'Senha alterada. Suas outras sessões foram encerradas.');
    } catch (excecao) {
      setErro(excecao.message);
    }
    setSalvando(false);
  }

  return (
    <div>
      <SectionHeading className="titulo-secao" overline="Configurações · sua conta"
        title="Configurações"
        subtitle="Seus dados de acesso e a troca da sua senha." />

      <div className="detalhe-secao">
        <div className="grupo-menu-rotulo">Sua conta</div>
        <Card>
          <div className="ficha-grade">
            <div className="ficha-item"><div className="ficha-rotulo">Nome</div><div className="ficha-valor">{usuario.nome}</div></div>
            <div className="ficha-item"><div className="ficha-rotulo">E-mail</div><div className="ficha-valor">{usuario.email}</div></div>
            <div className="ficha-item"><div className="ficha-rotulo">Papel</div><div className="ficha-valor">{usuario.papelTitulo}</div></div>
          </div>
        </Card>
      </div>

      <div className="detalhe-secao">
        <div className="grupo-menu-rotulo">Trocar senha</div>
        <Card accent>
          <form className="form-config" onSubmit={trocar}>
            {erro && <div className="form-erro"><Alert tone="warning" title="Não foi possível trocar a senha">{erro}</Alert></div>}
            <Input label="Senha atual" type="password" value={atual} onChange={(e) => setAtual(e.target.value)} />
            <Input label="Nova senha (mínimo 8 caracteres)" type="password" value={nova} onChange={(e) => setNova(e.target.value)} />
            <Input label="Confirmar nova senha" type="password" value={confirma} onChange={(e) => setConfirma(e.target.value)} />
            <div className="config-acao">
              <Button variant="primary" disabled={salvando} leadingIcon={<Icone nome="cadeado" />} onClick={trocar}>
                {salvando ? 'Salvando…' : 'Trocar senha'}
              </Button>
            </div>
            <p className="config-nota">Ao trocar a senha, suas outras sessões (outros navegadores ou dispositivos) são encerradas. A sessão atual continua.</p>
          </form>
        </Card>
      </div>
    </div>
  );
}

/* Menu lateral expansível e retrátil. */
function MenuLateral({ entidades, aba, aoNavegar, recolhido }) {
  const usuario = usarUsuario();
  const item = (chave, titulo) => {
    // Cada item aparece só se o papel do usuário enxerga a área.
    if (chave === 'configuracoes') {
      // Configurações (trocar a própria senha) fica disponível a todo usuário.
    } else if (chave === 'usuarios' || chave === 'papeis') {
      if (!usuario?.gerenciaUsuarios) return null;
    } else if (!podeArea(usuario, chave)) {
      return null;
    }
    return (
      <button key={chave} type="button" title={titulo}
        className={'item-menu' + (aba === chave ? ' ativo' : '')}
        onClick={() => aoNavegar(chave)}>
        <Icone nome={ICONE_MENU[chave] || 'painel'} tamanho={18} />
        <span className="item-menu-rotulo">{titulo}</span>
      </button>
    );
  };
  const grupo = (rotulo, itens) => {
    const validos = itens.filter(Boolean);
    if (validos.length === 0) return null;
    return (
      <div className="grupo-menu">
        {rotulo && <div className="grupo-menu-rotulo">{rotulo}</div>}
        {validos}
      </div>
    );
  };
  const areas = entidades.slice(0, 7);
  return (
    <aside className={'lateral' + (recolhido ? ' recolhida' : '')}>
      <nav aria-label="Menu principal">
        {grupo(null, [item('resumo', 'Resumo')])}
        {grupo('Gestão de TI', areas.map((e) => item(e.chave, e.titulo)))}
        {grupo('Organização', [item('setores', 'Setores'), item('colaboradores', 'Colaboradores')])}
        {grupo('Consultoria', [item('artefatos', 'Artefatos da consultoria')])}
        {grupo('Administração', [item('usuarios', 'Usuários'), item('papeis', 'Grupos de acesso')])}
        {grupo('Conta', [item('configuracoes', 'Configurações')])}
      </nav>
    </aside>
  );
}

/* Tela de login do sistema. */
function TelaLogin({ aoEntrar }) {
  const [email, setEmail] = useState('');
  const [senha, setSenha] = useState('');
  const [erro, setErro] = useState('');
  const [entrando, setEntrando] = useState(false);

  async function submeter(evento) {
    evento.preventDefault();
    setErro('');
    setEntrando(true);
    try {
      const corpo = await chamarApi('/api/sessao', {
        method: 'POST', headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ email, senha }),
      });
      aoEntrar(corpo.usuario);
    } catch (excecao) {
      setErro(excecao.message);
      setEntrando(false);
    }
  }

  return (
    <div className="login-tela">
      <form className="login-cartao" onSubmit={submeter}>
        <div className="login-marca">
          <Logo variant="red" height={32} src="/ds/assets/bni-logo-red.png" />
        </div>
        <div className="login-titulo">Gestão de TI · ITSM</div>
        <p className="login-sub">Entre com suas credenciais da BNI Brasil.</p>
        {erro && <div className="form-erro"><Alert tone="warning" title="Não foi possível entrar">{erro}</Alert></div>}
        <Input label="E-mail" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
        <Input label="Senha" type="password" value={senha} onChange={(e) => setSenha(e.target.value)} />
        <Button variant="primary" size="lg" fullWidth disabled={entrando}
          leadingIcon={<Icone nome="cadeado" />} onClick={submeter}>
          {entrando ? 'Entrando…' : 'Entrar'}
        </Button>
      </form>
      <div className="login-rodape">Sistema interno · uso restrito · BNI Brasil</div>
    </div>
  );
}

const PAPEL_VAZIO = { nome: '', email: '', papel: 'ti', senha: '', ativo: 'Sim' };

/* Gestão de usuários e papéis de acesso (só para administradores). */
function PainelUsuarios({ avisar, usuarioAtual }) {
  const [usuarios, setUsuarios] = useState(null);
  const [papeisLista, setPapeisLista] = useState([]);
  const [form, setForm] = useState(null); // null | { id?, ...campos }
  const [erro, setErro] = useState('');
  const [salvando, setSalvando] = useState(false);
  const [excluindo, setExcluindo] = useState(null);

  const carregar = useCallback(async () => {
    const [u, p] = await Promise.all([chamarApi('/api/usuarios'), chamarApi('/api/papeis')]);
    setUsuarios(u.usuarios);
    setPapeisLista(p.papeis);
  }, []);

  useEffect(() => {
    carregar().catch((excecao) => avisar('warning', excecao.message));
  }, [carregar]);

  const definir = (chave) => (evento) => setForm((atual) => ({ ...atual, [chave]: evento.target.value }));

  async function salvar() {
    setErro('');
    setSalvando(true);
    try {
      const dados = { nome: form.nome, email: form.email, papel: form.papel, ativo: form.ativo };
      if (form.senha) dados.senha = form.senha;
      const caminho = form.id ? `/api/usuarios/${form.id}` : '/api/usuarios';
      await chamarApi(caminho, {
        method: form.id ? 'PUT' : 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ dados }),
      });
      setForm(null);
      avisar('success', form.id ? 'Usuário atualizado.' : 'Usuário criado.');
      await carregar();
    } catch (excecao) {
      setErro(excecao.message);
    }
    setSalvando(false);
  }

  async function excluir() {
    try {
      await chamarApi(`/api/usuarios/${excluindo.id}`, { method: 'DELETE' });
      setExcluindo(null);
      avisar('success', 'Usuário excluído.');
      await carregar();
    } catch (excecao) {
      setExcluindo(null);
      avisar('warning', excecao.message);
    }
  }

  const opcoesPapel = papeisLista.map((p) => ({ value: p.chave, label: p.titulo }));

  return (
    <div>
      <SectionHeading className="titulo-secao" overline="Administração · controle de acesso"
        title="Usuários"
        subtitle="Quem entra no sistema e o que cada papel enxerga. As senhas ficam guardadas com hash, nunca em texto." />

      <div className="toolbar">
        <div className="toolbar-busca" />
        <Button variant="primary" leadingIcon={<Icone nome="mais" />} onClick={() => { setErro(''); setForm({ ...PAPEL_VAZIO }); }}>
          Novo usuário
        </Button>
      </div>

      {usuarios === null ? (
        <EsqueletoTabela colunas={5} />
      ) : (
      <div className="tabela-envelope">
        <table className="tabela">
          <thead>
            <tr><th>Nome</th><th>E-mail</th><th>Papel</th><th>Situação</th><th className="celula-acoes">Ações</th></tr>
          </thead>
          <tbody>
            {(usuarios || []).map((u) => (
              <tr key={u.id}>
                <td className="celula-forte">{u.nome}</td>
                <td>{u.email}</td>
                <td>{u.papelTitulo}</td>
                <td>
                  <Badge tone={u.ativo ? 'red' : 'grey'} variant={u.ativo ? 'soft' : 'outline'}>
                    {u.ativo ? 'Ativo' : 'Inativo'}
                  </Badge>
                </td>
                <td className="celula-acoes">
                  <span className="acao">
                    <IconButton variant="ghost" size="sm" label="Editar"
                      onClick={() => { setErro(''); setForm({ id: u.id, nome: u.nome, email: u.email, papel: u.papel, ativo: u.ativo ? 'Sim' : 'Não', senha: '' }); }}>
                      <Icone nome="lapis" />
                    </IconButton>
                  </span>
                  {u.id !== usuarioAtual.id && (
                    <span className="acao">
                      <IconButton variant="ghost" size="sm" label="Excluir" onClick={() => setExcluindo(u)}>
                        <Icone nome="lixeira" />
                      </IconButton>
                    </span>
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      )}

      <div className="detalhe-secao" style={{ marginTop: 'var(--space-5)' }}>
        <Alert tone="info" title="O que cada papel acessa">
          {papeisLista.map((p) => `${p.titulo}: ${p.descricao}`).join('  ·  ')}
        </Alert>
      </div>

      {form && (
        <Dialog open title={form.id ? 'Editar usuário' : 'Novo usuário'} accent
          onClose={() => setForm(null)} width={Math.min(560, window.innerWidth - 24)}
          footer={<>
            <Button variant="ghost" onClick={() => setForm(null)}>Cancelar</Button>
            <Button variant="primary" disabled={salvando} onClick={salvar}>{salvando ? 'Salvando…' : 'Salvar'}</Button>
          </>}>
          {erro && <div className="form-erro"><Alert tone="warning" title="Não foi possível salvar">{erro}</Alert></div>}
          <div className="form-grade">
            <Input label="Nome *" value={form.nome} onChange={definir('nome')} />
            <Input label="E-mail *" type="email" value={form.email} onChange={definir('email')} />
            <Select label="Papel *" options={opcoesPapel} value={form.papel} onChange={definir('papel')} />
            <Select label="Situação" options={[{ value: 'Sim', label: 'Ativo' }, { value: 'Não', label: 'Inativo' }]}
              value={form.ativo} onChange={definir('ativo')} />
            <div className="campo-cheio">
              <Input label={form.id ? 'Nova senha (deixe em branco para manter)' : 'Senha * (mínimo 8 caracteres)'}
                type="password" value={form.senha} onChange={definir('senha')} />
            </div>
          </div>
        </Dialog>
      )}

      {excluindo && (
        <Dialog open title="Excluir usuário" accent onClose={() => setExcluindo(null)}
          footer={<>
            <Button variant="ghost" onClick={() => setExcluindo(null)}>Cancelar</Button>
            <Button variant="primary" onClick={excluir}>Excluir</Button>
          </>}>
          <p>Excluir o acesso de "{excluindo.nome}" ({excluindo.email})? Ele perde o login imediatamente.</p>
        </Dialog>
      )}
    </div>
  );
}

/* Cartão editável de um papel: liga/desliga cada área e as capacidades. */
function CartaoPapel({ papel, areas, aoSalvar }) {
  const [selec, setSelec] = useState(() => new Set(papel.areas));
  const [somenteLeitura, setSomenteLeitura] = useState(!!papel.somenteLeitura);
  const [gerencia, setGerencia] = useState(!!papel.gerenciaUsuarios);
  const [salvando, setSalvando] = useState(false);

  const alternar = (chave) => setSelec((atual) => {
    const nova = new Set(atual);
    if (nova.has(chave)) nova.delete(chave); else nova.add(chave);
    return nova;
  });

  async function salvar() {
    setSalvando(true);
    await aoSalvar(papel.chave, {
      areas: [...selec],
      somenteLeitura,
      gerenciaUsuarios: gerencia,
    });
    setSalvando(false);
  }

  return (
    <div className="detalhe-secao">
      <Card accent>
        <div className="papel-cabecalho">
          <div>
            <div className="papel-titulo">{papel.titulo}</div>
            <div className="linha-secundaria">{papel.descricao}</div>
          </div>
          <Button variant="primary" size="sm" disabled={salvando} onClick={salvar}>
            {salvando ? 'Salvando…' : 'Salvar'}
          </Button>
        </div>
        <div className="grupo-menu-rotulo papel-secao">Áreas que este papel acessa</div>
        <div className="papel-areas">
          {areas.map((a) => (
            <Checkbox key={a.chave} label={a.titulo} checked={selec.has(a.chave)} onChange={() => alternar(a.chave)} />
          ))}
        </div>
        <div className="grupo-menu-rotulo papel-secao">Capacidades</div>
        <div className="papel-switches">
          <Switch label="Somente leitura (não cria, edita nem exclui)" checked={somenteLeitura} onChange={setSomenteLeitura} />
          <Switch label="Pode gerenciar usuários e papéis" checked={gerencia} onChange={setGerencia} />
        </div>
      </Card>
    </div>
  );
}

/* Grupos de acesso: o admin liga/desliga o que cada papel enxerga. */
function PainelPapeis({ avisar }) {
  const [dados, setDados] = useState(null);

  const carregar = useCallback(async () => {
    const corpo = await chamarApi('/api/papeis');
    setDados(corpo);
  }, []);

  useEffect(() => {
    carregar().catch((excecao) => avisar('warning', excecao.message));
  }, [carregar]);

  async function salvar(chave, config) {
    try {
      await chamarApi(`/api/papeis/${chave}`, {
        method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(config),
      });
      avisar('success', 'Permissões do papel atualizadas.');
      await carregar();
    } catch (excecao) {
      avisar('warning', excecao.message);
    }
  }

  if (dados === null) {
    return (
      <div>
        <SectionHeading className="titulo-secao" overline="Administração · controle de acesso"
          title="Grupos de acesso"
          subtitle="Ligue ou desligue as áreas que cada papel enxerga. Muda na hora, inclusive para quem já está logado." />
        {[0, 1, 2].map((i) => (
          <div key={i} className="detalhe-secao">
            <Card accent>
              <Esqueleto largura="28%" altura="var(--space-6)" style={{ marginBottom: 'var(--space-2)' }} />
              <Esqueleto largura="60%" altura="var(--space-3)" style={{ marginBottom: 'var(--space-4)' }} />
              <div className="papel-areas">
                {Array.from({ length: 6 }).map((_, j) => (
                  <Esqueleto key={j} largura="80%" altura="var(--space-4)" />
                ))}
              </div>
            </Card>
          </div>
        ))}
      </div>
    );
  }
  const editaveis = dados.papeis.filter((p) => p.editavel);
  const fixos = dados.papeis.filter((p) => !p.editavel);

  return (
    <div>
      <SectionHeading className="titulo-secao" overline="Administração · controle de acesso"
        title="Grupos de acesso"
        subtitle="Ligue ou desligue as áreas que cada papel enxerga. Muda na hora, inclusive para quem já está logado." />

      {editaveis.map((papel) => (
        <CartaoPapel key={papel.chave} papel={papel} areas={dados.areas} aoSalvar={salvar} />
      ))}

      {fixos.map((papel) => (
        <div key={papel.chave} className="detalhe-secao">
          <Card>
            <div className="papel-titulo">{papel.titulo}</div>
            <div className="linha-secundaria">{papel.descricao}</div>
            <div className="papel-fixo">Acesso total a todas as áreas e à administração. Este papel não pode ser restringido, para o sistema nunca ficar sem quem o administre.</div>
          </Card>
        </div>
      ))}
    </div>
  );
}

function abaDoEndereco() {
  return window.location.hash.replace(/^#\/?/, '') || 'resumo';
}

// No celular o menu vira uma gaveta sobreposta; "recolhido" passa a significar fechada.
const consultaMovel = window.matchMedia('(max-width: 720px)');

function ColetaApp() {
  const [usuario, setUsuario] = useState(undefined); // undefined=carregando, null=deslogado
  const [entidades, setEntidades] = useState(null);
  const [aba, setAbaEstado] = useState(abaDoEndereco);
  const [aviso, setAviso] = useState(null); // { tom, texto }
  const [ehMovel, setEhMovel] = useState(consultaMovel.matches);
  const [menuRecolhido, setMenuRecolhido] = useState(() => {
    if (consultaMovel.matches) return true;
    const salvo = window.localStorage.getItem('coleta-menu');
    if (salvo) return salvo === 'recolhido';
    return window.innerWidth < 900;
  });

  const carregarEsquema = useCallback(() => {
    chamarApi('/api/esquema').then((corpo) => setEntidades(corpo.entidades)).catch(() => setEntidades([]));
  }, []);

  // Ao abrir, descobre se já há sessão. Se sim, carrega o esquema.
  useEffect(() => {
    chamarApi('/api/eu')
      .then((corpo) => { setUsuario(corpo.usuario); carregarEsquema(); })
      .catch(() => setUsuario(null));
  }, [carregarEsquema]);

  // Qualquer 401 no meio da sessão volta para a tela de login.
  useEffect(() => {
    const aoExpirar = () => { setUsuario(null); setEntidades(null); };
    window.addEventListener('sessao-expirada', aoExpirar);
    return () => window.removeEventListener('sessao-expirada', aoExpirar);
  }, []);

  useEffect(() => {
    const aoMudarTela = (evento) => {
      setEhMovel(evento.matches);
      if (evento.matches) setMenuRecolhido(true);
    };
    consultaMovel.addEventListener('change', aoMudarTela);
    return () => consultaMovel.removeEventListener('change', aoMudarTela);
  }, []);

  useEffect(() => {
    const aoMudarHash = () => setAbaEstado(abaDoEndereco());
    window.addEventListener('hashchange', aoMudarHash);
    return () => window.removeEventListener('hashchange', aoMudarHash);
  }, []);

  useEffect(() => {
    if (!aviso) return undefined;
    const temporizador = setTimeout(() => setAviso(null), 4000);
    return () => clearTimeout(temporizador);
  }, [aviso]);

  const avisar = useCallback((tom, texto) => setAviso({ tom, texto }), []);
  const navegar = useCallback((chave) => {
    window.location.hash = `/${chave}`;
    if (consultaMovel.matches) setMenuRecolhido(true);
  }, []);
  const alternarMenu = () => setMenuRecolhido((atual) => {
    if (!consultaMovel.matches) window.localStorage.setItem('coleta-menu', atual ? 'expandido' : 'recolhido');
    return !atual;
  });

  async function sair() {
    try {
      await fetch('/api/sessao', { method: 'DELETE' });
    } catch { /* logout é idempotente */ }
    setUsuario(null);
    setEntidades(null);
    window.location.hash = '/resumo';
  }

  if (usuario === undefined) return <EsqueletoInicial />; // ainda verificando a sessão
  if (usuario === null) {
    return (
      <TelaLogin aoEntrar={(u) => { setUsuario(u); carregarEsquema(); }} />
    );
  }
  if (entidades === null) return <EsqueletoInicial />;
  if (entidades.length === 0) {
    return (
      <main className="conteudo">
        <Alert tone="warning" title="Não foi possível carregar o sistema">
          O servidor não respondeu com o esquema das áreas. Verifique se ele está no ar e recarregue a página.
        </Alert>
      </main>
    );
  }

  // Rotas: #/chave e #/<entidade>/<id>. Áreas fora do papel caem no resumo.
  const caminho = aba.split('/');
  const base = caminho[0] || 'resumo';
  const ehArea = base === 'resumo'
    || base === 'configuracoes'
    || ((base === 'usuarios' || base === 'papeis') && usuario.gerenciaUsuarios)
    || (base === 'artefatos' && podeArea(usuario, 'artefatos'))
    || (entidades.some((e) => e.chave === base) && podeArea(usuario, base));
  const abaValida = ehArea ? base : 'resumo';
  const artefatoAberto = abaValida === 'artefatos' ? caminho[1] : undefined;
  const entidadeAtiva = ['artefatos', 'usuarios', 'papeis', 'resumo', 'configuracoes'].includes(abaValida)
    ? null : entidades.find((e) => e.chave === abaValida);

  return (
    <ContextoUsuario.Provider value={usuario}>
      <header className="topo">
        <div className="topo-linha">
          <IconButton variant="ghost" size="sm" label="Expandir ou recolher o menu" onClick={alternarMenu}>
            <Icone nome="painelEsquerdo" tamanho={18} />
          </IconButton>
          <Logo variant="red" height={28} src="/ds/assets/bni-logo-red.png" />
          <div className="topo-divisor" />
          <div className="topo-titulo">Gestão de TI · ITSM</div>
          <div className="topo-usuario">
            <span className="topo-usuario-nome">{usuario.nome}</span>
            <span className="topo-usuario-papel">{usuario.papelTitulo}</span>
          </div>
          <Button variant="ghost" size="sm" leadingIcon={<Icone nome="sair" />} onClick={sair}>Sair</Button>
        </div>
      </header>

      <div className="corpo">
        {ehMovel && !menuRecolhido && <div className="pano-de-fundo" onClick={() => setMenuRecolhido(true)} />}
        <MenuLateral entidades={entidades} aba={abaValida} aoNavegar={navegar} recolhido={menuRecolhido} />
        <main className="conteudo">
          {abaValida === 'resumo' && <PainelResumo entidades={entidades} aoAbrir={navegar} />}
          {abaValida === 'usuarios' && <PainelUsuarios avisar={avisar} usuarioAtual={usuario} />}
          {abaValida === 'papeis' && <PainelPapeis avisar={avisar} />}
          {abaValida === 'configuracoes' && <PainelConfiguracoes avisar={avisar} />}
          {abaValida === 'artefatos' && !artefatoAberto && (
            <PainelArtefatos entidade={entidades.find((e) => e.chave === 'artefatos')}
              entidades={entidades} avisar={avisar} />
          )}
          {abaValida === 'artefatos' && artefatoAberto && (
            <PainelArtefatoDetalhe key={artefatoAberto} numero={artefatoAberto}
              entidade={entidades.find((e) => e.chave === 'artefatos')}
              entidades={entidades} avisar={avisar} aoNavegar={navegar} />
          )}
          {entidadeAtiva && !(entidadeAtiva.detalhe && caminho[1]) && (
            <PainelEntidade key={entidadeAtiva.chave} entidade={entidadeAtiva}
              entidades={entidades} avisar={avisar} />
          )}
          {entidadeAtiva && entidadeAtiva.detalhe && caminho[1] && (
            <PainelRegistroDetalhe key={`${entidadeAtiva.chave}/${caminho[1]}`}
              entidade={entidadeAtiva} id={caminho[1]}
              entidades={entidades} avisar={avisar} aoNavegar={navegar} />
          )}
        </main>
      </div>

      <footer className="rodape">
        <div className="rodape-linha">
          <span>Sistema interno de gestão de TI (ITSM) · BNI Brasil</span>
          <span>Acesso autenticado · {usuario.email}</span>
        </div>
      </footer>

      {aviso && (
        <div className="aviso-flutuante">
          <Alert tone={aviso.tom} title={aviso.tom === 'success' ? 'Tudo certo' : 'Atenção'}>{aviso.texto}</Alert>
        </div>
      )}
    </ContextoUsuario.Provider>
  );
}

window.ColetaApp = ColetaApp;
