/* 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, Tabs,
} = 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" /></>,
    menos: <path d="M5 12h14" />,
    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" /></>,
    grafico: <><line x1="12" x2="12" y1="20" y2="10" /><line x1="18" x2="18" y1="20" y2="4" /><line x1="6" x2="6" y1="20" y2="16" /><path d="M3 20h18" /></>,
    olho: <><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" /><circle cx="12" cy="12" r="3" /></>,
  };
  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',
  'bi-membros': 'grafico',
};

// 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 }) {
  const ehAdmin = usarUsuario()?.papel === 'admin';
  return entidade.campos.map((campo) => {
    if (campo.soAdmin && !ehAdmin) return null;
    const rotulo = campo.obrigatorio ? `${campo.rotulo} *` : campo.rotulo;
    if (campo.tipo === 'booleano') {
      return (
        <label key={campo.chave} className="campo-cheio campo-check">
          <input type="checkbox" checked={dados[campo.chave] === 'true'}
            onChange={(evento) => definir(campo.chave)({ target: { value: evento.target.checked ? 'true' : 'false' } })} />
          <span className="campo-rotulo-check">{campo.rotulo}</span>
        </label>
      );
    }
    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, categorias, textoVazio }) {
  const opcoesCategoria = categorias || ['Contrato', 'Proposta', 'Outro documento'];
  const podeEditar = !usarUsuario()?.somenteLeitura;
  const [anexos, setAnexos] = useState(null);
  const [enviando, setEnviando] = useState(false);
  const [categoria, setCategoria] = useState(opcoesCategoria[0]);
  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">{textoVazio || '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={opcoesCategoria}
              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 usuario = usarUsuario();
  const podeEditar = !usuario?.somenteLeitura;
  const ehAdmin = usuario?.papel === 'admin';
  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);
  // Campos marcados como filtráveis no schema (ex.: Situação em Parque de máquinas)
  // viram checkboxes que escondem as linhas cujo valor não estiver marcado.
  const filtros = useMemo(
    () => entidade.campos.filter((c) => c.filtravel && Array.isArray(c.opcoes)),
    [entidade],
  );
  const [selecoesFiltro, setSelecoesFiltro] = useState({});

  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('');
    setSelecoesFiltro(Object.fromEntries(filtros.map((c) => [c.chave, new Set(c.opcoes)])));
    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 passaFiltros = (r) => filtros.every((c) => {
    const sel = selecoesFiltro[c.chave];
    if (!sel) return true;
    const valor = r.dados[c.chave] || '';
    return !valor || sel.has(valor); // valor vazio nunca é escondido por este filtro
  });
  const visiveis = (registros || []).filter((r) => passaFiltros(r)
    && (!filtro || entidade.campos.some((c) => valorCelula(c, r).toLowerCase().includes(filtro))));

  function alternarFiltro(chave, opcao) {
    setSelecoesFiltro((atual) => {
      const conjunto = new Set(atual[chave] || []);
      if (conjunto.has(opcao)) conjunto.delete(opcao); else conjunto.add(opcao);
      return { ...atual, [chave]: conjunto };
    });
  }

  const colunas = entidade.colunas
    ? entidade.colunas.map((chave) => entidade.campos.find((c) => c.chave === chave)).filter(Boolean)
    : entidade.campos.slice(0, 5);
  // Colunas de check só de admin (ex.: Reunião e Documentação em Sistemas). Ficam fora
  // das colunas enxutas da tabela e aparecem apenas para administradores.
  const colunasAdmin = ehAdmin ? entidade.campos.filter((c) => c.soAdmin) : [];
  const colunasEsqueleto = colunas.length + colunasAdmin.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 alternarCheck(registro, chave, marcado) {
    const novos = { ...registro.dados, [chave]: marcado ? 'true' : 'false' };
    // Atualização otimista: reflete o clique na hora e desfaz se a API recusar.
    setRegistros((atual) => (atual || []).map((r) => (r.id === registro.id ? { ...r, dados: novos } : r)));
    try {
      await chamarApi(`/api/${entidade.chave}/${registro.id}`, {
        method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ dados: novos }),
      });
    } catch (excecao) {
      avisar('warning', excecao.message);
      await carregar();
    }
  }

  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>

      {filtros.length > 0 && (
        <div className="toolbar-filtros">
          {filtros.map((c) => (
            <div key={c.chave} className="toolbar-filtro">
              <span className="toolbar-filtro-rotulo">{c.rotulo}</span>
              {c.opcoes.map((op) => (
                <label key={op} className="filtro-check">
                  <input type="checkbox"
                    checked={selecoesFiltro[c.chave] ? selecoesFiltro[c.chave].has(op) : true}
                    onChange={() => alternarFiltro(c.chave, op)} />
                  <span>{op}</span>
                </label>
              ))}
            </div>
          ))}
        </div>
      )}

      <p className="toolbar-contagem">
        {registros === null ? <Esqueleto largura="120px" altura="var(--space-3)" style={{ display: 'inline-block' }} />
          : visiveis.length !== registros.length ? `${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>)}
                {colunasAdmin.map((c) => <th key={c.chave} className="celula-check">{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>
                    );
                  })}
                  {colunasAdmin.map((c) => (
                    <td key={c.chave} className="celula-check" onClick={(evento) => evento.stopPropagation()}>
                      <input type="checkbox" checked={registro.dados[c.chave] === 'true'} disabled={!podeEditar}
                        onChange={(evento) => alternarCheck(registro, c.chave, evento.target.checked)} />
                    </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 [anexosPorArtefato, setAnexosPorArtefato] = useState({});

  const carregar = useCallback(async () => {
    const { registros: lista } = await chamarApi('/api/artefatos');
    lista.sort((a, b) => (a.dados.numero || '').localeCompare(b.dados.numero || ''));
    setRegistros(lista);
    // Contagem de documentos anexados por artefato (para a coluna Documento).
    try {
      const { anexos } = await chamarApi('/api/artefatos/anexos');
      const contagens = {};
      for (const anexo of anexos) {
        contagens[anexo.dono] = (contagens[anexo.dono] || 0) + 1;
      }
      setAnexosPorArtefato(contagens);
    } catch {
      setAnexosPorArtefato({});
    }
  }, []);

  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>Documento</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>
                    {(() => {
                      const total = anexosPorArtefato[`artefatos:${registro.id}`] || 0;
                      if (total > 0) {
                        return (
                          <a className="link-detalhe" href={`#/artefatos/${registro.dados.numero}`}>
                            <Icone nome="clipe" tamanho={13} /> {total === 1 ? '1 arquivo' : `${total} arquivos`}
                          </a>
                        );
                      }
                      if (registro.dados.link) {
                        return <a className="link-externo" href={hrefSeguro(registro.dados.link)} target="_blank" rel="noreferrer">Abrir documento <Icone nome="externo" tamanho={13} /></a>;
                      }
                      return '·';
                    })()}
                  </td>
                  <td>{dataBr(registro.atualizadoEm)}</td>
                  <td className="celula-acoes">
                    <IconButton variant="ghost" size="sm" label="Visualizar"
                      onClick={() => { location.hash = `#/artefatos/${registro.dados.numero}`; }}>
                      <Icone nome="olho" />
                    </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);
    if (campo.tipo === 'booleano') return bruto === 'true' ? 'Sim' : 'Não';
    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 ehAdmin = usarUsuario()?.papel === 'admin';
  const camposCurtos = entidade.campos.filter((c) => c.tipo !== 'textarea' && (!c.soAdmin || ehAdmin));
  const camposLongos = entidade.campos.filter((c) => c.tipo === 'textarea' && (!c.soAdmin || ehAdmin));
  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>

      <div className="detalhe-secao">
        <div className="grupo-menu-rotulo">Documentos do artefato</div>
        <Card>
          <ListaAnexos entidade={entidade} registro={registro} avisar={avisar}
            categorias={['Entregável', 'Versão anterior', 'Material de apoio']}
            textoVazio="Nenhum documento anexado ainda. Envie aqui o entregável deste artefato (até 20 MB)." />
        </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. */
/* Barras horizontais simples (sem biblioteca): proporção pela maior contagem.
   `totalReal` corrige a nota quando o servidor já mandou a lista truncada. */
function BarrasHorizontais({ dados, limite, formato, totalReal }) {
  const lista = dados || [];
  const visiveis = limite ? lista.slice(0, limite) : lista;
  const maior = visiveis.reduce((m, d) => Math.max(m, d.total), 0) || 1;
  const universo = totalReal || lista.length;
  return (
    <div className="bi-barras">
      {visiveis.map((d) => (
        <div key={d.chave} className="bi-barra-linha" title={`${d.chave}: ${d.total.toLocaleString('pt-BR')}`}>
          <span className="bi-barra-rotulo">{formato ? formato(d.chave) : d.chave}</span>
          <span className="bi-barra-trilha">
            <span className="bi-barra-preenchida" style={{ width: `${Math.max(2, Math.round((d.total / maior) * 100))}%` }} />
          </span>
          <span className="bi-barra-valor">{d.total.toLocaleString('pt-BR')}</span>
        </div>
      ))}
      {visiveis.length < universo && (
        <p className="bi-barras-nota">Mostrando {visiveis.length} de {universo}.</p>
      )}
    </div>
  );
}

/* Colunas verticais para a linha do tempo de renovações. */
function ColunasMensais({ dados }) {
  const maior = dados.reduce((m, d) => Math.max(m, d.total), 0) || 1;
  const rotuloMes = (chave) => {
    const [ano, mes] = chave.split('-');
    const nomes = ['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov', 'dez'];
    return `${nomes[Number(mes) - 1] || mes}/${ano.slice(2)}`;
  };
  return (
    <div className="bi-colunas">
      {dados.map((d) => (
        <div key={d.chave} className="bi-coluna" title={`${rotuloMes(d.chave)}: ${d.total.toLocaleString('pt-BR')} renovações`}>
          <span className="bi-coluna-valor">{d.total >= 1000 ? `${(d.total / 1000).toFixed(1)}k` : d.total}</span>
          <span className="bi-coluna-trilha">
            <span className="bi-coluna-preenchida" style={{ height: `${Math.max(3, Math.round((d.total / maior) * 100))}%` }} />
          </span>
          <span className="bi-coluna-rotulo">{rotuloMes(d.chave)}</span>
        </div>
      ))}
    </div>
  );
}

// Nome por extenso das UFs, para os rótulos do gráfico por estado.
const NOMES_UF = {
  AC: 'Acre', AL: 'Alagoas', AP: 'Amapá', AM: 'Amazonas', BA: 'Bahia', CE: 'Ceará',
  DF: 'Distrito Federal', ES: 'Espírito Santo', GO: 'Goiás', MA: 'Maranhão',
  MT: 'Mato Grosso', MS: 'Mato Grosso do Sul', MG: 'Minas Gerais', PA: 'Pará',
  PB: 'Paraíba', PR: 'Paraná', PE: 'Pernambuco', PI: 'Piauí', RJ: 'Rio de Janeiro',
  RN: 'Rio Grande do Norte', RS: 'Rio Grande do Sul', RO: 'Rondônia', RR: 'Roraima',
  SC: 'Santa Catarina', SP: 'São Paulo', SE: 'Sergipe', TO: 'Tocantins',
};
const NOME_UF = (sigla) => NOMES_UF[sigla] || sigla;

// Aparência das situações de afiliação, com os tons existentes no design system.
const APARENCIA_AFILIACAO = {
  'Ativo': { tone: 'red', variant: 'soft' },
  'Atrasado': { tone: 'black', variant: 'soft' },
  'Suspenso': { tone: 'black', variant: 'solid' },
  'Renovação pendente': { tone: 'grey', variant: 'soft' },
};

/* Mapa do Brasil em SVG puro (malha das UFs do IBGE embutida em
   /dados/mapa-brasil.js, já projetada). Cada pin é uma cidade; o raio cresce
   com a raiz da contagem e o clique filtra a lista de membros. Zoom pela roda
   do mouse (no ponto do cursor) e pelos botões; arrastar move o mapa. */
function MapaBrasil({ porCidade, cidadeAtiva, estadoAtivo, aoEscolherCidade, aoEscolherEstado }) {
  const mapa = window.MAPA_BRASIL;
  const svgRef = useRef(null);
  const arrasto = useRef(null);      // arrasto em andamento
  const arrastou = useRef(false);    // o último gesto foi arrasto (não clique)

  const base = useMemo(() => (mapa ? mapa.viewBox.split(' ').map(Number) : [0, 0, 720, 740]), [mapa]);
  const [vista, setVista] = useState(base);
  const [dica, setDica] = useState(null); // tooltip do pin sob o mouse

  const pins = useMemo(() => {
    if (!mapa) return [];
    const lista = [];
    for (const c of (porCidade || [])) {
      const ponto = mapa.cidades[`${c.cidade}|${c.estado}`];
      if (ponto) lista.push({ ...c, x: ponto[0], y: ponto[1] });
    }
    // Pins maiores primeiro: os menores ficam por cima e continuam clicáveis.
    return lista.sort((a, b) => b.total - a.total);
  }, [mapa, porCidade]);

  const prender = useCallback(([x, y, w, h]) => {
    const nx = Math.min(Math.max(x, base[0]), base[0] + base[2] - w);
    const ny = Math.min(Math.max(y, base[1]), base[1] + base[3] - h);
    return [nx, ny, w, h];
  }, [base]);

  // Zoom mantendo o ponto (cx, cy) parado; sem o ponto, centraliza.
  const aplicarZoom = useCallback((fator, cx, cy) => {
    setVista((atual) => {
      const [x, y, w, h] = atual;
      const nw = Math.min(base[2], Math.max(base[2] / 64, w / fator));
      if (nw === w) return atual;
      const nh = nw * (base[3] / base[2]);
      const px = cx == null ? x + w / 2 : cx;
      const py = cy == null ? y + h / 2 : cy;
      return prender([px - ((px - x) / w) * nw, py - ((py - y) / h) * nh, nw, nh]);
    });
  }, [base, prender]);

  // Roda do mouse com listener nativo: precisa de preventDefault para a página
  // não rolar junto, e o React registra wheel como passivo.
  useEffect(() => {
    const svg = svgRef.current;
    if (!svg) return undefined;
    const aoRodar = (evento) => {
      evento.preventDefault();
      setDica(null);
      const rect = svg.getBoundingClientRect();
      const [x, y, w, h] = svg.getAttribute('viewBox').split(' ').map(Number);
      const cx = x + ((evento.clientX - rect.left) / rect.width) * w;
      const cy = y + ((evento.clientY - rect.top) / rect.height) * h;
      aplicarZoom(evento.deltaY < 0 ? 1.5 : 1 / 1.5, cx, cy);
    };
    svg.addEventListener('wheel', aoRodar, { passive: false });
    return () => svg.removeEventListener('wheel', aoRodar);
  }, [aplicarZoom]);

  if (!mapa) return null;
  const ampliado = vista[2] < base[2] - 0.5;
  const zoomFator = base[2] / vista[2];
  const maior = pins.length ? pins[0].total : 1;
  // O raio acompanha o zoom ao contrário: o pin mantém o tamanho na tela em
  // qualquer nível de zoom (nunca some, nunca vira um círculo gigante) e as
  // cidades densas (Grande São Paulo) se separam ao aproximar. A área do pin é
  // proporcional à quantidade de membros (raio pela raiz), com piso de 3.5.
  const fatorPin = vista[2] / base[2];
  const raio = (total) => Math.max(3.5, Math.sqrt(total / maior) * 22) * fatorPin;

  // Rótulos de municípios aparecem progressivamente: quanto maior o zoom, mais
  // cidades ganham nome (as maiores primeiro), só dentro da área visível.
  const naVista = (p) => p.x >= vista[0] && p.x <= vista[0] + vista[2]
    && p.y >= vista[1] && p.y <= vista[1] + vista[3];
  const maxRotulos = Math.round(8 * zoomFator);
  const comRotulo = new Set();
  for (const p of pins) {
    if (comRotulo.size >= maxRotulos) break;
    if (naVista(p)) comRotulo.add(`${p.cidade}|${p.estado}`);
  }

  const aoPressionar = (evento) => {
    if (evento.button !== 0) return;
    setDica(null); // pan/zoom invalida a posição do tooltip
    // IMPORTANTE: não capturar o ponteiro aqui. Capturar no pointerdown faz o
    // navegador redirecionar o clique para o SVG e o clique no pin nunca
    // dispara; a captura só entra quando o arrasto de fato começa.
    arrasto.current = { id: evento.pointerId, cx: evento.clientX, cy: evento.clientY, vista0: vista, moveu: false };
  };
  const aoMover = (evento) => {
    const a = arrasto.current;
    if (!a) return;
    if (!a.moveu && Math.abs(evento.clientX - a.cx) + Math.abs(evento.clientY - a.cy) > 4) {
      a.moveu = true;
      svgRef.current.setPointerCapture(a.id);
    }
    if (!a.moveu) return;
    const rect = svgRef.current.getBoundingClientRect();
    const dx = (evento.clientX - a.cx) * (a.vista0[2] / rect.width);
    const dy = (evento.clientY - a.cy) * (a.vista0[3] / rect.height);
    setVista(prender([a.vista0[0] - dx, a.vista0[1] - dy, a.vista0[2], a.vista0[3]]));
  };
  const aoSoltar = () => {
    arrastou.current = Boolean(arrasto.current && arrasto.current.moveu);
    arrasto.current = null;
  };
  const clicarPin = (evento, cidade) => {
    if (arrastou.current) return; // fim de arrasto não é clique
    evento.stopPropagation(); // não deixa o clique cair na UF embaixo do pin
    aoEscolherCidade(cidade);
  };
  const clicarUf = (sigla) => {
    if (arrastou.current) return;
    aoEscolherEstado(sigla);
  };

  // Tooltip próprio ao passar o mouse no pin, posicionado sobre o envelope.
  const mostrarDica = (p) => {
    const svg = svgRef.current;
    if (!svg) return;
    const rect = svg.getBoundingClientRect();
    const env = svg.parentElement.getBoundingClientRect();
    setDica({
      px: ((p.x - vista[0]) / vista[2]) * rect.width + (rect.left - env.left),
      py: ((p.y - vista[1]) / vista[3]) * rect.height + (rect.top - env.top) - raio(p.total) / (vista[2] / rect.width),
      cidade: p.cidade, estado: p.estado, total: p.total,
    });
  };

  return (
    <div className="bi-mapa-envelope">
      <div className="bi-mapa-controles">
        <IconButton variant="secondary" size="sm" label="Aproximar o mapa" onClick={() => aplicarZoom(3)}>
          <Icone nome="mais" tamanho={15} />
        </IconButton>
        <IconButton variant="secondary" size="sm" label="Afastar o mapa" onClick={() => aplicarZoom(1 / 3)}>
          <Icone nome="menos" tamanho={15} />
        </IconButton>
        {ampliado && (
          <Button variant="secondary" size="sm" onClick={() => setVista(base)}>Ver tudo</Button>
        )}
      </div>
      <svg ref={svgRef} className={'bi-mapa' + (ampliado ? ' ampliado' : '')}
        viewBox={vista.join(' ')} role="img"
        aria-label="Mapa do Brasil com a quantidade de membros por cidade"
        onPointerDown={aoPressionar} onPointerMove={aoMover}
        onPointerUp={aoSoltar} onPointerCancel={aoSoltar}>
        {mapa.ufs.map(([sigla, d]) => (
          <path key={sigla} d={d}
            className={'bi-mapa-uf' + (sigla === estadoAtivo ? ' ativo' : '')}
            data-uf={sigla}
            onClick={() => clicarUf(sigla)}>
            <title>{`${NOME_UF(sigla)} (clique para filtrar o estado)`}</title>
          </path>
        ))}
        {mapa.ufs.map(([sigla, _d, cx, cy]) => (
          <text key={`rotulo-${sigla}`} className="bi-mapa-uf-rotulo"
            x={cx} y={cy} fontSize={11 * fatorPin}>
            {zoomFator >= 3 ? NOME_UF(sigla) : sigla}
          </text>
        ))}
        {pins.map((p) => (
          <circle key={`${p.cidade}|${p.estado}`}
            className={'bi-mapa-pin' + (p.cidade === cidadeAtiva ? ' ativo' : '')}
            cx={p.x} cy={p.y} r={raio(p.total)} tabIndex={0} role="button"
            aria-label={`${p.cidade}, ${p.estado}: ${p.total} membros. Filtrar a lista.`}
            onClick={(e) => clicarPin(e, p.cidade)}
            onPointerEnter={() => mostrarDica(p)}
            onPointerLeave={() => setDica(null)}
            onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); aoEscolherCidade(p.cidade); } }} />
        ))}
        {pins.filter((p) => comRotulo.has(`${p.cidade}|${p.estado}`)).map((p) => (
          <text key={`cidade-${p.cidade}|${p.estado}`} className="bi-mapa-pin-rotulo"
            x={p.x + raio(p.total) + 3 * fatorPin} y={p.y + 3.5 * fatorPin}
            fontSize={10 * fatorPin}>
            {p.cidade}
          </text>
        ))}
      </svg>
      {dica && (
        <div className="bi-mapa-tooltip" style={{ left: `${dica.px}px`, top: `${dica.py}px` }}>
          <strong>{dica.cidade} · {dica.estado}</strong>
          <span>{dica.total.toLocaleString('pt-BR')} {dica.total === 1 ? 'membro' : 'membros'}</span>
          <span className="bi-mapa-tooltip-acao">Clique para filtrar a lista</span>
        </div>
      )}
      <p className="bi-mapa-zoom-nota">Use a roda do mouse ou os botões para aproximar; arraste para mover.</p>
    </div>
  );
}

/* BI de membros: painel analítico exclusivo dos administradores. Os agregados
   vêm prontos do servidor em uma chamada; a busca é paginada no banco. */
function PainelBiMembros({ avisar }) {
  const [resumo, setResumo] = useState(null);
  const [falhou, setFalhou] = useState(false);
  const [abaAtiva, setAbaAtiva] = useState('dashboard');

  // Busca paginada, com atraso curto para não consultar a cada tecla.
  const [busca, setBusca] = useState('');
  const [filtroRegiao, setFiltroRegiao] = useState('');
  const [filtroStatus, setFiltroStatus] = useState('');
  const [filtroProfissao, setFiltroProfissao] = useState('');
  const [filtroEspecialidade, setFiltroEspecialidade] = useState('');
  const [filtroCidade, setFiltroCidade] = useState('');
  const [filtroEstado, setFiltroEstado] = useState('');
  const [pagina, setPagina] = useState(1);
  const [lista, setLista] = useState(null);
  const [buscando, setBuscando] = useState(false);
  // Membro aberto no popup de detalhe (a linha da tabela é clicável).
  const [membroAberto, setMembroAberto] = useState(null);
  // O mapa pode ser escondido; a escolha fica no navegador.
  const [mapaVisivel, setMapaVisivel] = useState(() => window.localStorage.getItem('bi-mapa') !== 'escondido');
  // Os filtros também têm mostrar/ocultar, e as opções deles são facetadas:
  // cada seletor mostra só o que existe no recorte atual, com as contagens.
  const [mostrarFiltros, setMostrarFiltros] = useState(() => window.localStorage.getItem('bi-filtros') !== 'ocultos');
  const [facetas, setFacetas] = useState(null);

  useEffect(() => {
    chamarApi('/api/bi/membros/resumo')
      .then(setResumo)
      .catch(() => { setFalhou(true); });
  }, []);

  // Última página que respondeu com sucesso: se a próxima falhar, volta para ela
  // em vez de deixar o estado divergir do que está na tela.
  const paginaConfirmada = useRef(1);

  useEffect(() => {
    const controle = { ativo: true };
    setBuscando(true);
    const temporizador = setTimeout(() => {
      const parametros = new URLSearchParams();
      if (busca.trim()) parametros.set('busca', busca.trim());
      if (filtroRegiao) parametros.set('regiao', filtroRegiao);
      if (filtroStatus) parametros.set('status', filtroStatus);
      if (filtroProfissao) parametros.set('profissao', filtroProfissao);
      if (filtroEspecialidade) parametros.set('especialidade', filtroEspecialidade);
      if (filtroCidade) parametros.set('cidade', filtroCidade);
      if (filtroEstado) parametros.set('estado', filtroEstado);
      parametros.set('pagina', String(pagina));
      parametros.set('porPagina', '15');
      chamarApi(`/api/bi/membros?${parametros}`)
        .then((corpo) => {
          if (!controle.ativo) return;
          paginaConfirmada.current = corpo.pagina;
          setLista(corpo);
          setBuscando(false);
        })
        .catch(() => {
          if (!controle.ativo) return;
          setBuscando(false);
          setPagina(paginaConfirmada.current);
          avisar('warning', 'Não foi possível buscar os membros.');
        });
      // Facetas do mesmo recorte, em paralelo (a paginação não afeta as opções).
      const parametrosFacetas = new URLSearchParams(parametros);
      parametrosFacetas.delete('pagina');
      parametrosFacetas.delete('porPagina');
      chamarApi(`/api/bi/membros/facetas?${parametrosFacetas}`)
        .then((corpo) => { if (controle.ativo) setFacetas(corpo); })
        .catch(() => { /* facetas são acessório; a lista já avisa o erro */ });
    }, 300);
    return () => { controle.ativo = false; clearTimeout(temporizador); };
  }, [busca, filtroRegiao, filtroStatus, filtroProfissao, filtroEspecialidade, filtroCidade, filtroEstado, pagina, avisar]);

  const mudarFiltro = (definir) => (valor) => { definir(valor); setPagina(1); };

  // Clique em um pin do mapa: filtra por cidade e leva direto à lista.
  // Cidade e estado são mutuamente exclusivos (a cidade já implica o estado).
  const escolherCidadeNoMapa = useCallback((cidade) => {
    setFiltroCidade((atual) => (atual === cidade ? '' : cidade));
    setFiltroEstado('');
    setPagina(1);
    setAbaAtiva('membros');
  }, []);

  // Clique no território de um estado (fora dos pins): filtra a UF inteira.
  const escolherEstadoNoMapa = useCallback((sigla) => {
    setFiltroEstado((atual) => (atual === sigla ? '' : sigla));
    setFiltroCidade('');
    setPagina(1);
    setAbaAtiva('membros');
  }, []);

  if (falhou) {
    return (
      <div>
        <SectionHeading className="titulo-secao" overline="Inteligência de negócio"
          title="BI · Membros" subtitle="Análise da base de membros do BNI Brasil." />
        <Alert tone="warning" title="Não foi possível carregar o BI">
          Confira se a migração da base de membros foi aplicada e recarregue a página.
        </Alert>
      </div>
    );
  }

  const totalPaginas = lista ? Math.max(1, Math.ceil(lista.total / lista.porPagina)) : 1;
  const dataBrSnapshot = resumo ? resumo.snapshot.split('-').reverse().join('/') : '';
  const ativos = resumo ? (resumo.porStatus.find((s) => s.chave === 'Ativo')?.total || 0) : 0;
  // Janela de renovações: 12 meses corridos a partir do mês do snapshot,
  // incluindo meses sem nenhuma renovação (o SQL só devolve meses com dado).
  const renovacoesJanela = useMemo(() => {
    if (!resumo) return [];
    const contagens = new Map(resumo.renovacoesPorMes.map((m) => [m.chave, m.total]));
    const [anoBase, mesBase] = resumo.snapshot.slice(0, 7).split('-').map(Number);
    return Array.from({ length: 12 }, (_, i) => {
      const indiceMes = mesBase - 1 + i;
      const chave = `${anoBase + Math.floor(indiceMes / 12)}-${String((indiceMes % 12) + 1).padStart(2, '0')}`;
      return { chave, total: contagens.get(chave) || 0 };
    });
  }, [resumo]);

  /* Opções de um seletor facetado: o que existe no recorte atual, com as
     contagens. Antes de as facetas chegarem, cai nas listas do resumo; o valor
     já escolhido nunca some do seletor (entra com contagem zero se preciso). */
  const opcoesFacetadas = (dimensao, valorAtual, formato) => {
    const nome = (chave) => (formato ? formato(chave) : chave);
    let itens = [];
    if (facetas && facetas[dimensao]) {
      itens = facetas[dimensao].map((f) => ({
        value: f.chave,
        label: `${nome(f.chave)} (${f.total.toLocaleString('pt-BR')})`,
      }));
    } else if (resumo) {
      const doResumo = { regiao: 'porRegiao', status: 'porStatus', profissao: 'porProfissao' };
      if (doResumo[dimensao]) {
        itens = resumo[doResumo[dimensao]].map((f) => ({ value: f.chave, label: nome(f.chave) }));
      } else if (dimensao === 'especialidade') {
        itens = [...new Set(resumo.porEspecialidade.map((e) => e.especialidade))]
          .map((valor) => ({ value: valor, label: nome(valor) }));
      }
    }
    if (valorAtual && !itens.some((opcao) => opcao.value === valorAtual)) {
      itens.unshift({ value: valorAtual, label: `${nome(valorAtual)} (0)` });
    }
    return [{ value: '', label: 'Todas' }, ...itens];
  };

  // O mapa aparece nas duas abas: no dashboard como leitura geográfica e na
  // lista como controle de filtro (clicar no pin filtra a tabela abaixo).
  // Mostrar/esconder fica lembrado no navegador.
  const cartaoMapa = resumo ? (
    <Card className="bi-mapa-cartao">
      <div className="bi-mapa-cabecalho">
        <div className="bi-cartao-titulo">Membros pelo Brasil</div>
        <Button variant="ghost" size="sm" onClick={() => setMapaVisivel((v) => {
          window.localStorage.setItem('bi-mapa', v ? 'escondido' : 'visivel');
          return !v;
        })}>
          {mapaVisivel ? 'Esconder mapa' : 'Mostrar mapa'}
        </Button>
      </div>
      {mapaVisivel && (
        <>
          <p className="bi-mapa-nota">Cada pin é uma cidade; a área acompanha a quantidade de membros. Clique em um pin para filtrar a cidade, ou no território de um estado para filtrar a UF inteira.</p>
          <MapaBrasil porCidade={resumo.porCidade} cidadeAtiva={filtroCidade} estadoAtivo={filtroEstado}
            aoEscolherCidade={escolherCidadeNoMapa} aoEscolherEstado={escolherEstadoNoMapa} />
        </>
      )}
    </Card>
  ) : null;

  return (
    <div>
      <SectionHeading className="titulo-secao" overline="Inteligência de negócio"
        title="BI · Membros"
        subtitle={`Análise da base de membros do BNI Brasil. Snapshot do BNI Connect de ${dataBrSnapshot || '…'} · visível apenas para administradores.`} />

      <Tabs value={abaAtiva} onChange={setAbaAtiva} style={{ marginBottom: 'var(--space-5)' }}
        tabs={[{ value: 'dashboard', label: 'Dashboard' }, { value: 'membros', label: 'Membros' }]} />

      {abaAtiva === 'dashboard' && (!resumo ? (
        <div className="bi-grade-stats" role="status" aria-label="Carregando o painel">
          {[0, 1, 2, 3].map((i) => (
            <Card key={i}>
              <Esqueleto largura="55%" altura="var(--space-7)" style={{ marginBottom: 'var(--space-3)' }} />
              <Esqueleto largura="75%" altura="var(--space-3)" />
            </Card>
          ))}
        </div>
      ) : (
        <>
          <div className="bi-grade-stats">
            <Card accent><Stat value={resumo.totais.membros.toLocaleString('pt-BR')} label="Membros na base" tone="red" /></Card>
            <Card><Stat value={ativos.toLocaleString('pt-BR')} label="Membros ativos" tone="black" /></Card>
            <Card><Stat value={resumo.totais.grupos.toLocaleString('pt-BR')} label="Grupos" tone="black" /></Card>
            <Card><Stat value={resumo.totais.regioes.toLocaleString('pt-BR')} label="Regiões" tone="black" /></Card>
          </div>

          {cartaoMapa}

          <div className="bi-grade-2">
            <Card>
              <div className="bi-cartao-titulo">Situação da afiliação</div>
              <BarrasHorizontais dados={resumo.porStatus} />
            </Card>
            <Card>
              <div className="bi-cartao-titulo">Renovações previstas por mês</div>
              {renovacoesJanela.length === 0
                ? <p className="toolbar-contagem">Sem datas de término no período.</p>
                : <ColunasMensais dados={renovacoesJanela} />}
            </Card>
          </div>

          <div className="bi-grade-2">
            <Card>
              <div className="bi-cartao-titulo">Membros por região</div>
              <BarrasHorizontais dados={resumo.porRegiao} limite={15}
                formato={(chave) => chave.replace(/^Região\s+/i, '')} />
            </Card>
            <Card>
              <div className="bi-cartao-titulo">Membros por estado</div>
              <BarrasHorizontais dados={resumo.porEstado} limite={15} formato={NOME_UF} />
            </Card>
          </div>

          <div className="bi-grade-2">
            <Card>
              <div className="bi-cartao-titulo">Membros por profissão</div>
              <BarrasHorizontais dados={resumo.porProfissao} limite={10} />
            </Card>
            <Card>
              <div className="bi-cartao-titulo">Maiores grupos</div>
              <BarrasHorizontais dados={resumo.porGrupo} limite={10} totalReal={resumo.totais.grupos}
                formato={(chave) => chave.replace(/^BNI\s+/i, '')} />
            </Card>
          </div>
        </>
      ))}

      {abaAtiva === 'membros' && (
      <div className="bi-busca-secao">
        {cartaoMapa}
        <Card className="bi-filtros-cartao">
          <div className="bi-filtros-linha">
            <div className="bi-filtros-busca">
              <Input placeholder="Buscar por nome, empresa ou grupo…" value={busca} maxLength={60}
                aria-label="Buscar membros por nome, empresa ou grupo"
                onChange={(e) => { setBusca(e.target.value); setPagina(1); }} />
            </div>
            <Button variant="ghost" size="sm" leadingIcon={<Icone nome="ajustes" tamanho={15} />}
              onClick={() => setMostrarFiltros((v) => {
                window.localStorage.setItem('bi-filtros', v ? 'ocultos' : 'visiveis');
                return !v;
              })}>
              {mostrarFiltros ? 'Ocultar filtros' : 'Mostrar filtros'}
            </Button>
          </div>
          {mostrarFiltros && (
            <div className="bi-filtros-grade">
              <Select label="Região BNI (do grupo)" value={filtroRegiao}
                onChange={(e) => mudarFiltro(setFiltroRegiao)(e.target.value)}
                options={opcoesFacetadas('regiao', filtroRegiao, (chave) => chave.replace(/^Região\s+/i, ''))} />
              <Select label="Situação" value={filtroStatus}
                onChange={(e) => mudarFiltro(setFiltroStatus)(e.target.value)}
                options={opcoesFacetadas('status', filtroStatus)} />
              <Select label="Profissão" value={filtroProfissao}
                onChange={(e) => { mudarFiltro(setFiltroProfissao)(e.target.value); setFiltroEspecialidade(''); }}
                options={opcoesFacetadas('profissao', filtroProfissao)} />
              <Select label="Especialidade" value={filtroEspecialidade}
                onChange={(e) => mudarFiltro(setFiltroEspecialidade)(e.target.value)}
                options={opcoesFacetadas('especialidade', filtroEspecialidade)} />
            </div>
          )}
          {(filtroCidade || filtroEstado) && (
            <p className="bi-filtro-ativo">
              <span className="toolbar-filtro-rotulo">{filtroCidade ? 'Cidade (mapa)' : 'Estado (mapa)'}</span>
              {filtroCidade ? (
                <button type="button" className="bi-chip" title="Remover o filtro de cidade"
                  onClick={() => mudarFiltro(setFiltroCidade)('')}>
                  {filtroCidade} <span aria-hidden="true">✕</span>
                </button>
              ) : (
                <button type="button" className="bi-chip" title="Remover o filtro de estado"
                  onClick={() => mudarFiltro(setFiltroEstado)('')}>
                  {NOME_UF(filtroEstado)} <span aria-hidden="true">✕</span>
                </button>
              )}
            </p>
          )}
        </Card>
        <p className="toolbar-contagem">
          {lista
            ? (lista.total === 1 ? '1 membro encontrado' : `${lista.total.toLocaleString('pt-BR')} membros encontrados`)
            : 'Carregando…'}
          {buscando ? ' · atualizando…' : ''}
        </p>
        <div className="tabela-envelope">
          <table className="tabela">
            <thead>
              <tr>
                <th>Nome</th><th>Empresa</th><th>Grupo</th><th>Região</th><th>Profissão</th><th>Situação</th>
              </tr>
            </thead>
            <tbody>
              {(lista?.membros || []).map((m) => (
                <tr key={m.id} className="linha-clicavel" tabIndex={0}
                  title="Ver todos os dados do membro"
                  onClick={() => setMembroAberto(m)}
                  onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMembroAberto(m); } }}>
                  <td className="celula-forte">{m.nome}</td>
                  <td>{m.empresa || '·'}</td>
                  <td>{m.grupo || '·'}</td>
                  <td>{m.regiao.replace(/^Região\s+/i, '') || '·'}</td>
                  <td>{m.profissao || '·'}</td>
                  <td>
                    {(() => {
                      const aparencia = APARENCIA_AFILIACAO[m.status] || { tone: 'grey', variant: 'outline' };
                      return <Badge tone={aparencia.tone} variant={aparencia.variant}>{m.status || '·'}</Badge>;
                    })()}
                  </td>
                </tr>
              ))}
              {lista && lista.membros.length === 0 && (
                <tr><td colSpan={6} className="vazio">Nenhum membro com esses filtros.</td></tr>
              )}
            </tbody>
          </table>
        </div>
        <div className="bi-paginacao">
          <Button variant="secondary" size="sm" disabled={pagina <= 1}
            onClick={() => setPagina((p) => Math.max(1, p - 1))}>Anterior</Button>
          <span className="bi-paginacao-info">Página {lista ? lista.pagina : pagina} de {totalPaginas}</span>
          <Button variant="secondary" size="sm" disabled={!lista || pagina >= totalPaginas}
            onClick={() => setPagina((p) => p + 1)}>Próxima</Button>
        </div>
      </div>
      )}

      {membroAberto && (
        <Dialog open title={membroAberto.nome} accent
          onClose={() => setMembroAberto(null)}
          width={Math.min(680, window.innerWidth - 24)}
          footer={<Button variant="primary" onClick={() => setMembroAberto(null)}>Fechar</Button>}>
          <div className="bi-detalhe-situacao">
            {(() => {
              const aparencia = APARENCIA_AFILIACAO[membroAberto.status] || { tone: 'grey', variant: 'outline' };
              return <Badge tone={aparencia.tone} variant={aparencia.variant}>{membroAberto.status || 'Sem situação'}</Badge>;
            })()}
          </div>
          <div className="bi-detalhe-grade">
            {[
              ['Empresa', membroAberto.empresa],
              ['Grupo', membroAberto.grupo],
              ['Região', membroAberto.regiao],
              ['Profissão', membroAberto.profissao],
              ['Especialidade', membroAberto.especialidade],
              ['Função no grupo', membroAberto.funcao],
              ['E-mail', membroAberto.email],
              ['Telefone', membroAberto.telefone],
              ['Cidade', membroAberto.cidade],
              ['Estado', membroAberto.estado],
              ['Início do ciclo atual', dataBr(membroAberto.dataInicio)],
              ['Término do ciclo atual', dataBr(membroAberto.dataTermino)],
              ['ID no BNI Connect', String(membroAberto.id)],
            ].map(([rotulo, valor]) => (
              <div key={rotulo} className="bi-detalhe-item">
                <span className="bi-detalhe-rotulo">{rotulo}</span>
                <span className="bi-detalhe-valor">{valor || '·'}</span>
              </div>
            ))}
          </div>
          <p className="config-nota">Dados do snapshot do BNI Connect de {dataBrSnapshot}. Contato visível apenas para administradores; tratar conforme a LGPD.</p>
        </Dialog>
      )}
    </div>
  );
}

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 (chave === 'bi-membros') {
      // O BI de membros é exclusivo do papel administrador.
      if (usuario?.papel !== 'admin') 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('Inteligência', [item('bi-membros', 'BI · Membros')])}
        {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. */
/* Diálogo de criação de um grupo de acesso novo. */
function DialogoNovoGrupo({ areas, aoFechar, aoCriar }) {
  const [titulo, setTitulo] = useState('');
  const [descricao, setDescricao] = useState('');
  const [selec, setSelec] = useState(() => new Set(['resumo']));
  const [somenteLeitura, setSomenteLeitura] = useState(false);
  const [gerencia, setGerencia] = useState(false);
  const [salvando, setSalvando] = useState(false);
  const [erro, setErro] = useState('');

  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 criar() {
    setErro('');
    setSalvando(true);
    try {
      await aoCriar({
        titulo, descricao,
        areas: [...selec],
        somenteLeitura,
        gerenciaUsuarios: gerencia,
      });
    } catch (excecao) {
      setErro(excecao.message);
      setSalvando(false);
    }
  }

  return (
    <Dialog open title="Novo grupo de acesso" accent onClose={aoFechar}
      width={Math.min(680, window.innerWidth - 24)}
      footer={<>
        <Button variant="ghost" onClick={aoFechar}>Cancelar</Button>
        <Button variant="primary" disabled={salvando} onClick={criar}>
          {salvando ? 'Criando…' : 'Criar grupo'}
        </Button>
      </>}>
      {erro && <div className="form-erro"><Alert tone="warning" title="Não foi possível criar">{erro}</Alert></div>}
      <div className="form-grade">
        <Input label="Nome do grupo" value={titulo} onChange={(e) => setTitulo(e.target.value)}
          placeholder="Ex.: Consultoria externa" />
        <Input label="Descrição (opcional)" value={descricao} onChange={(e) => setDescricao(e.target.value)}
          placeholder="Para que serve este grupo" />
      </div>
      <div className="grupo-menu-rotulo papel-secao">Áreas que este grupo 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>
    </Dialog>
  );
}

function CartaoPapel({ papel, areas, aoSalvar, aoExcluir }) {
  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}
              {papel.personalizado && <Badge tone="grey" variant="soft" style={{ marginLeft: 'var(--space-2)' }}>Criado pelo admin</Badge>}
            </div>
            <div className="linha-secundaria">{papel.descricao}</div>
          </div>
          <div className="papel-acoes">
            {papel.personalizado && (
              <Button variant="ghost" size="sm" leadingIcon={<Icone nome="lixeira" tamanho={14} />}
                onClick={() => aoExcluir(papel)}>Excluir</Button>
            )}
            <Button variant="primary" size="sm" disabled={salvando} onClick={salvar}>
              {salvando ? 'Salvando…' : 'Salvar'}
            </Button>
          </div>
        </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 [criando, setCriando] = useState(false);
  // Exclusão bloqueada por usuários vinculados: guarda o grupo e a contagem
  // para o diálogo de transferência.
  const [transferencia, setTransferencia] = useState(null); // { papel, vinculados, para }
  const [transferindo, setTransferindo] = useState(false);

  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);
    }
  }

  async function criarGrupo(config) {
    await chamarApi('/api/papeis', {
      method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(config),
    });
    setCriando(false);
    avisar('success', 'Grupo de acesso criado.');
    await carregar();
  }

  async function excluirGrupo(papel) {
    const resposta = await fetch(`/api/papeis/${papel.chave}`, { method: 'DELETE' });
    if (resposta.status === 204) {
      avisar('success', `Grupo "${papel.titulo}" excluído.`);
      await carregar();
      return;
    }
    const corpo = await resposta.json().catch(() => ({}));
    if (resposta.status === 409 && corpo.usuariosVinculados) {
      // Há usuários no grupo: abre o diálogo pedindo a transferência primeiro.
      setTransferencia({ papel, vinculados: corpo.usuariosVinculados, para: '' });
      return;
    }
    avisar('warning', corpo.erro || 'Não foi possível excluir o grupo.');
  }

  async function transferirEExcluir() {
    const { papel, para } = transferencia;
    if (!para) return;
    setTransferindo(true);
    try {
      const { transferidos } = await chamarApi(`/api/papeis/${papel.chave}/transferir`, {
        method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ para }),
      });
      await chamarApi(`/api/papeis/${papel.chave}`, { method: 'DELETE' });
      avisar('success', `${transferidos} usuário(s) transferidos e grupo "${papel.titulo}" excluído.`);
      setTransferencia(null);
      await carregar();
    } catch (excecao) {
      avisar('warning', excecao.message);
    } finally {
      setTransferindo(false);
    }
  }

  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." />

      <div className="papeis-acoes">
        <Button variant="primary" size="sm" leadingIcon={<Icone nome="mais" tamanho={15} />}
          onClick={() => setCriando(true)}>Novo grupo de acesso</Button>
      </div>

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

      {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>
      ))}

      {criando && (
        <DialogoNovoGrupo areas={dados.areas} aoFechar={() => setCriando(false)} aoCriar={criarGrupo} />
      )}

      {transferencia && (
        <Dialog open title={`Excluir · ${transferencia.papel.titulo}`} accent
          onClose={() => setTransferencia(null)}
          width={Math.min(560, window.innerWidth - 24)}
          footer={<>
            <Button variant="ghost" onClick={() => setTransferencia(null)}>Cancelar</Button>
            <Button variant="primary" disabled={!transferencia.para || transferindo}
              onClick={transferirEExcluir}>
              {transferindo ? 'Transferindo…' : 'Transferir e excluir'}
            </Button>
          </>}>
          <Alert tone="warning" title="Este grupo tem usuários vinculados">
            {transferencia.vinculados === 1
              ? 'Há 1 usuário neste grupo. Transfira o usuário para outro grupo de acesso antes de excluir.'
              : `Há ${transferencia.vinculados} usuários neste grupo. Transfira os usuários para outro grupo de acesso antes de excluir.`}
          </Alert>
          <div className="papel-transferencia">
            <Select label="Transferir os usuários para" value={transferencia.para}
              onChange={(e) => setTransferencia((t) => ({ ...t, para: e.target.value }))}
              options={[{ value: '', label: 'Escolha um grupo…' },
                ...dados.papeis
                  .filter((p) => p.chave !== transferencia.papel.chave)
                  .map((p) => ({ value: p.chave, label: p.titulo }))]} />
          </div>
        </Dialog>
      )}
    </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 === 'bi-membros' && usuario.papel === 'admin')
    || (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', 'bi-membros'].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 === 'bi-membros' && <PainelBiMembros avisar={avisar} />}
          {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;
