/**
 * Back office — painéis.
 *
 * Lê do stock da NetScreen (`/api/admin/hardware/stock`): inventário com
 * número de série, tipo, garantia e a que cliente e loja está atribuído.
 *
 * O problema que este ecrã resolve, e que veio da reunião de 15 Set:
 *
 *   Um painel sem dono não recebe campanha — o conteúdo DOOH não tem destino.
 *   Alguém tem de dizer, à mão, que aquele painel é daquele cliente.
 *
 * Por isso o ecrã abre nos que **ainda não têm dono** e atribui-os em lote,
 * em vez de listar painéis bonitos.
 *
 * Atribuir é um **modal**, não um painel lateral: uma escolha de cliente e de
 * loja, sobre uma tabela que se quer continuar a ver. Ver docs/padroes-interface.md.
 *
 * **A medida é do painel** (migração 013) e regista-se aqui, no modal
 * "Medida", com o player da VNNOX a que corresponde. Importa: é a medida que o
 * criativo DOOH das campanhas desenha — um painel sem ela fica de fora da
 * pré-visualização, e a gaveta da campanha diz qual é.
 *
 * **O estado online vem da VNNOX** desde 23 Set, para os painéis ligados a um
 * player (lib/vnnox/). Um painel por ligar não tem ponto; um ligado cuja VNNOX
 * não respondeu tem-no cinzento — "não sabemos", e não "desligado".
 */

// Os rótulos dos tipos vêm da base desde a migração 013 (`hardware_types`) e
// chegam em `tipo_nome`. Estavam escritos aqui, e era isso que obrigava a mexer
// no front-end para acrescentar um equipamento novo.

const ROTULO_ESTADO_PAINEL = {
  free: 'Por atribuir',
  allocated: 'Atribuído',
  maintenance: 'Manutenção',
  end_of_life: 'Fim de vida',
};

function ScreenAdminHardware({ me }) {
  // Devolver, mudar de loja e a manutenção são do Admin (becos de 9 Out, ponto 13): o servidor recusa ao Operator.
  const ehAdmin = Boolean(me && me.user && me.user.role === 'admin');
  const [paineis, setPaineis] = React.useState(null);
  const [erro, setErro] = React.useState(null);
  const [seleccao, setSeleccao] = React.useState([]);
  // O «A tratar» abre os que não têm medida (?ver=sem-medida, becos de 9 Out, ponto 5).
  // ?ver=offline vem do alerta da Rede (administrador virgem A4: «Ver painéis» abria «Por atribuir», vazio).
  const [filtro, setFiltro] = React.useState(() => { const v = new URLSearchParams(window.location.search).get('ver'); return ['sem-medida', 'offline', 'atribuidos'].includes(v) ? v : 'por-atribuir'; });
  const [aAtribuir, setAAtribuir] = React.useState(false);
  // Vindo da ficha de um cliente (?para=<conta>): o modal de atribuir já o traz escolhido (agentes, 20 Out:
  // a ficha dizia «sem painéis» e não tinha por onde os atribuir).
  const [para] = React.useState(() => new URLSearchParams(window.location.search).get('para'));
  const [aRegistar, setARegistar] = React.useState(false);
  // Registar a partir de um player da Rede VNNOX (30 Out): o formulário vem preenchido e já ligado.
  const [playerARegistar, setPlayerARegistar] = React.useState(null);
  const [versaoRede, setVersaoRede] = React.useState(0);
  const [aMedir, setAMedir] = React.useState(null);
  const [aGerir, setAGerir] = React.useState(null);
  const [toast, setToast] = React.useState(null);

  const carregar = React.useCallback(
    () =>
      api
        .get('/api/admin/hardware/stock')
        .then((r) => {
          setPaineis(r.paineis);
          setErro(null);
        })
        .catch((e) => setErro(e.message || 'Não foi possível ler o stock de painéis.')),
    []
  );

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

  const fecharToast = React.useCallback(() => setToast(null), []);

  const visiveis = React.useMemo(() => {
    if (!paineis) return [];
    if (filtro === 'por-atribuir') return paineis.filter((p) => p.estado === 'free');
    if (filtro === 'atribuidos') return paineis.filter((p) => p.estado === 'allocated');
    if (filtro === 'manutencao') return paineis.filter((p) => p.estado === 'maintenance');
    if (filtro === 'sem-medida') return paineis.filter((p) => !p.resolucao && p.conta_nome);
    if (filtro === 'offline') return paineis.filter((p) => p.online === false && p.conta_nome);
    return paineis;
  }, [paineis, filtro]);

  // A selecção não sobrevive à mudança de filtro: atribuir em lote algo que já
  // não se está a ver é como se atribui o painel errado.
  React.useEffect(() => setSeleccao([]), [filtro]);

  // Só se atribui o que está livre. Nos outros estados a caixa fica desactivada.
  const seleccionaveis = visiveis.filter((p) => p.estado === 'free');
  const alternar = (id) => setSeleccao((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]));
  const todosSeleccionados = seleccionaveis.length > 0 && seleccionaveis.every((p) => seleccao.includes(p.id));
  const seleccionados = (paineis || []).filter((p) => seleccao.includes(p.id));

  const colunas = [
    {
      chave: 'seleccao',
      largura: 'w-10',
      rotuloCartao: 'Seleccionar',
      rotulo: (
        <input
          type="checkbox"
          checked={todosSeleccionados}
          disabled={!seleccionaveis.length}
          onChange={() => setSeleccao(todosSeleccionados ? [] : seleccionaveis.map((p) => p.id))}
          aria-label="Seleccionar todos os painéis por atribuir que estão visíveis"
          className="accent-[hsl(var(--primary))]"
        />
      ),
      render: (p) => (
        <input
          type="checkbox"
          checked={seleccao.includes(p.id)}
          disabled={p.estado !== 'free'}
          title={p.estado !== 'free' ? 'Só se atribuem painéis por atribuir' : undefined}
          onChange={() => alternar(p.id)}
          aria-label={`Seleccionar ${p.serial}`}
          onClick={(e) => e.stopPropagation()}
          className="accent-[hsl(var(--primary))] disabled:opacity-30"
        />
      ),
    },
    {
      chave: 'serial', rotulo: 'Painel', ordenavel: true, principal: true,
      render: (p) => (
        <span>
          {p.nome || p.serial}
          <span className="mt-0.5 block text-xs font-normal text-muted-foreground">
            {[p.tipo_nome || p.tipo, p.nome ? p.serial : null].filter(Boolean).join(' · ')}
          </span>
        </span>
      ),
    },
    {
      // Mostrava sempre um traço, mesmo depois de a API passar a devolver a
      // medida (013). Um painel medido a dizer "—" é tão falso como inventar.
      chave: 'resolucao', rotulo: 'Resolução',
      render: (p) => (
        <span className="flex items-center gap-2">
          {p.resolucao ? (
            <span className="tabular-nums">{p.resolucao}</span>
          ) : (
            <span className="text-muted-foreground">sem medida</span>
          )}
          {/* Online só para os ligados a um player, e só se a VNNOX respondeu. */}
          {p.vnnox_player_id && (
            <span
              className="h-2 w-2 shrink-0"
              title={p.online === null ? 'estado desconhecido' : p.online ? 'online na VNNOX' : 'offline na VNNOX'}
              aria-label={p.online === null ? 'estado desconhecido' : p.online ? 'online' : 'offline'}
              style={{ background: `hsl(var(${p.online === null ? '--unknown' : p.online ? '--online' : '--offline'}))` }}
            />
          )}
          <button
            type="button"
            onClick={(e) => { e.stopPropagation(); setAMedir(p); }}
            aria-label={`Registar a medida de ${p.serial}`}
            className="border border-border px-2 py-0.5 text-xs text-muted-foreground outline-none hover:border-primary hover:text-foreground focus-visible:ring-1 focus-visible:ring-accent"
          >
            Medida
          </button>
        </span>
      ),
    },
    {
      chave: 'estado', rotulo: 'Estado', ordenavel: true,
      render: (p) => (
        <Badge variante={p.estado === 'free' ? 'primario' : p.estado === 'allocated' ? 'sucesso' : p.estado === 'maintenance' ? 'erro' : 'contorno'}>
          {ROTULO_ESTADO_PAINEL[p.estado] || p.estado}
        </Badge>
      ),
    },
    {
      chave: 'conta_nome', rotulo: 'Cliente', ordenavel: true,
      render: (p) => (p.conta_nome ? p.conta_nome : <span className="text-muted-foreground">—</span>),
    },
    {
      chave: 'loja_nome', rotulo: 'Loja',
      // Com o «rua»/«dentro», para o técnico ver de relance (A3).
      render: (p) => p.loja_nome ? <span>{p.loja_nome}{typeof p.virado_para_rua === 'boolean' && <span className="text-muted-foreground">{p.virado_para_rua ? ' · rua' : ' · dentro'}</span>}</span> : <span className="text-muted-foreground">—</span>,
    },
    {
      chave: 'garantia_ate', rotulo: 'Garantia', ordenavel: true,
      valorOrdenacao: (p) => (p.garantia_ate ? new Date(String(p.garantia_ate).slice(0, 10)).getTime() : null),
      render: (p) => {
        if (!p.garantia_ate) return <span className="text-muted-foreground">—</span>;
        const dias = diasAte(p.garantia_ate);
        // 90 dias é o aviso: dá tempo de trocar antes de a garantia acabar.
        return (
          <span className={dias < 90 ? 'text-accent' : ''}>
            {dias < 0 ? 'expirou ' : ''}{formatData(p.garantia_ate)}
          </span>
        );
      },
    },
    // Devolver, mudar de loja, manutenção (becos de 9 Out, ponto 13). Só para o Admin; um painel por
    // atribuir só tem a manutenção, e um em fim de vida não tem nada. Um por atribuir também tem o
    // «Atribuir» na linha: só se atribuía marcando a caixa à esquerda, e ninguém dava com ela (agentes, 20 Out).
    {
      chave: 'gerir', rotulo: '', largura: 'w-36',
      render: (p) => (
        <span className="flex flex-wrap gap-1.5">
          {p.estado === 'free' && (
            <button
              type="button"
              onClick={(e) => { e.stopPropagation(); setSeleccao([p.id]); setAAtribuir(true); }}
              aria-label={`Atribuir ${p.serial} a um cliente`}
              className="border border-primary px-2 py-0.5 text-xs text-primary outline-none hover:bg-primary hover:text-primary-foreground focus-visible:ring-1 focus-visible:ring-accent"
            >
              Atribuir
            </button>
          )}
          {ehAdmin && p.estado !== 'end_of_life' && (
            <button
              type="button"
              onClick={(e) => { e.stopPropagation(); setAGerir(p); }}
              aria-label={`Mudar ${p.serial}: devolver, loja ou manutenção`}
              className="border border-border px-2 py-0.5 text-xs text-muted-foreground outline-none hover:border-primary hover:text-foreground focus-visible:ring-1 focus-visible:ring-accent"
            >
              Mudar
            </button>
          )}
        </span>
      ),
    },
  ];

  const contar = (estado) => (paineis ? paineis.filter((p) => p.estado === estado).length : 0);
  const porAtribuir = contar('free');
  const semMedida = paineis ? paineis.filter((p) => !p.resolucao && p.conta_nome).length : 0;

  const chip = (valor, texto) => (
    <button
      type="button"
      aria-pressed={filtro === valor}
      onClick={() => setFiltro(valor)}
      className="ns-chip"
    >
      {texto}
    </button>
  );

  if (erro) {
    return (
      <div className="space-y-6">
        <header>
          <h1 className="mt-1 font-display text-4xl uppercase leading-none tracking-wide">Painéis</h1>
        </header>
        <div className="border border-border bg-card px-6 py-12 text-center">
          <p className="font-display text-xl tracking-wide">Não deu para ler o stock</p>
          <p className="mt-2 text-sm text-muted-foreground">{erro}</p>
        </div>
      </div>
    );
  }

  return (
    <div className="space-y-6">
      <header>
        <h1 className="mt-1 font-display text-4xl uppercase leading-none tracking-wide">Painéis</h1>
        {paineis && (
          <p className="mt-2 text-sm text-muted-foreground">
            {/* «no stock» com todos atribuídos lia-se como «em armazém» (comercial virgem A5). */}
            {paineis.length} {paineis.length === 1 ? 'painel' : 'painéis'} no total ·{' '}
            <span className={porAtribuir ? 'text-accent' : ''}>{porAtribuir} por atribuir</span> ·{' '}
            {contar('maintenance')} em manutenção
          </p>
        )}
        <p className="mt-1 text-xs text-muted-foreground">
          A medida de cada painel define o tamanho dos vídeos. O ponto junto à resolução mostra se está online.
        </p>
      </header>

      {ehAdmin && <button type="button" onClick={() => setARegistar(true)} className="bg-primary px-4 py-2 text-sm text-primary-foreground">Registar painel</button>}

      <div className="flex flex-wrap items-center gap-2">
        {chip('rede', 'Rede VNNOX · ao vivo')}
        {chip('por-atribuir', `Por atribuir${porAtribuir ? ` (${porAtribuir})` : ''}`)}
        {chip('atribuidos', 'Atribuídos')}
        {paineis && paineis.some((p) => p.online === false && p.conta_nome) && chip('offline', 'Desligados')}
        {chip('manutencao', 'Manutenção')}
        {chip('sem-medida', `Com cliente, sem medida${semMedida ? ` (${semMedida})` : ''}`)}
        {chip('todos', 'Todos')}
      </div>

      {filtro === 'rede' && <RedeVnnox key={versaoRede} ehAdmin={ehAdmin} onRegistar={(p) => { setPlayerARegistar(p); setARegistar(true); }} />}

      {para && filtro === 'por-atribuir' && seleccao.length === 0 && visiveis && visiveis.length > 0 && (
        <p className="border-l-2 border-accent pl-3 text-sm text-foreground">
          Escolhe os painéis a atribuir (o «Atribuir» de cada linha, ou várias caixas e «Atribuir a cliente»): o cliente
          de onde vieste já vem escolhido.
        </p>
      )}

      {/* A barra de selecção só existe quando há selecção — não ocupa espaço a dizer "0". */}
      {seleccao.length > 0 && (
        <div className="flex flex-wrap items-center justify-between gap-3 border border-primary bg-[hsl(var(--primary)/0.08)] px-4 py-3">
          <p className="text-sm text-foreground">
            {seleccao.length} {seleccao.length === 1 ? 'painel seleccionado' : 'painéis seleccionados'}
          </p>
          <div className="flex flex-wrap gap-2">
            <button
              type="button"
              onClick={() => setSeleccao([])}
              className="border border-border px-3 py-1.5 text-xs uppercase tracking-wider text-muted-foreground outline-none hover:text-foreground focus-visible:ring-1 focus-visible:ring-accent"
            >
              Limpar
            </button>
            <button
              type="button"
              onClick={() => setAAtribuir(true)}
              className="bg-primary px-3 py-1.5 text-xs uppercase tracking-wider text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-1 focus-visible:ring-accent"
            >
              Atribuir a cliente
            </button>
          </div>
        </div>
      )}

      {filtro !== 'rede' && <Tabela
        colunas={colunas}
        linhas={visiveis}
        carregando={!paineis}
        chaveLinha={(p) => p.id}
        ordemInicial={{ chave: 'serial', sentido: 'asc' }}
        pesquisa={{ campos: ['serial', 'conta_nome', 'loja_nome'], placeholder: 'Pesquisar painel' }}
        vazio={{
          // Vindo de um cliente que precisa de painel (?para=): diz quem, e o passo seguinte (A5: beco sem saída).
          titulo: filtro === 'por-atribuir' ? (para ? 'Não há painéis livres para este cliente' : 'Todos os painéis têm dono') : 'Sem painéis',
          descricao:
            filtro === 'por-atribuir'
              ? (ehAdmin ? (para ? 'Todos os painéis do stock já têm dono. Regista o painel novo em «Registar painel» (ou a partir de um player em «Rede VNNOX · ao vivo») e atribui-o a seguir: este cliente já vem escolhido.' : 'Não há painéis por atribuir. Regista um em «Registar painel», ou a partir de um player em «Rede VNNOX · ao vivo».') : 'Não há painéis por atribuir. Os painéis entram no stock pelo registo de um Admin da equipa.')
              : 'Nada neste estado.',
        }}
      />}

      <ModalAtribuirPaineis
        aberto={aAtribuir}
        paineis={seleccionados}
        contaInicial={para}
        onFechar={() => { setAAtribuir(false); setSeleccao([]); }}
        onConcluido={(mensagem) => {
          setAAtribuir(false);
          setSeleccao([]);
          setToast({ id: Date.now(), mensagem });
          carregar();
        }}
      />
      <ModalMedidaPainel
        painel={aMedir}
        onFechar={() => setAMedir(null)}
        onConcluido={(mensagem) => {
          setAMedir(null);
          setToast({ id: Date.now(), mensagem });
          carregar();
        }}
      />
      <ModalRegistarPainel aberto={aRegistar} player={playerARegistar} onFechar={() => { setARegistar(false); setPlayerARegistar(null); }} onConcluido={(mensagem) => { setARegistar(false); setPlayerARegistar(null); setToast({ id: Date.now(), mensagem }); setVersaoRede((v) => v + 1); carregar(); }} />
      <ModalGerirPainel
        painel={aGerir}
        onFechar={() => setAGerir(null)}
        onConcluido={(mensagem) => {
          setAGerir(null);
          setToast({ id: Date.now(), mensagem });
          carregar();
        }}
      />
      <Toast toast={toast} onFechar={fecharToast} />
    </div>
  );
}

/**
 * Mudar um painel que já tem cliente, ou está em manutenção (becos de 9 Out, ponto 13): devolvê-lo ao
 * stock, mudá-lo de loja dentro do mesmo cliente, pô-lo ou tirá-lo de manutenção. Um modal só, com a
 * escolha à cabeça: são três gestos raros, e todos pedem o motivo (fica na auditoria).
 */
function ModalGerirPainel({ painel, onFechar, onConcluido }) {
  const [accao, setAccao] = React.useState(null);
  const [lojas, setLojas] = React.useState(null);
  const [loja, setLoja] = React.useState(undefined);
  const [virado, setVirado] = React.useState(undefined);
  const [motivo, setMotivo] = React.useState('');
  const [aGuardar, setAGuardar] = React.useState(false);
  const [erro, setErro] = React.useState(null);

  React.useEffect(() => {
    if (!painel) return;
    setAccao(null);
    setLojas(null);
    setLoja(undefined);
    setVirado(undefined);
    setMotivo('');
    setErro(null);
    setAGuardar(false);
    if (painel.conta_slug) {
      api.get(`/api/admin/accounts/${painel.conta_slug}`)
        .then((d) => setLojas(d.lojas))
        .catch((e) => setErro(e.message || 'Não foi possível ler as lojas.'));
    }
  }, [painel]);

  if (!painel) return <Modal aberto={false} onFechar={onFechar} titulo="Mudar" />;

  const emManutencao = painel.estado === 'maintenance';
  const accoes = [
    // «Dentro de» confundia-se com «dentro da loja» (administrador virgem A3).
    painel.conta_slug && ['loja', 'Mudar de loja', `Entre as lojas de ${painel.conta_nome}. Está em ${painel.loja_nome || 'nenhuma loja'}.`],
    painel.conta_slug && ['virado', 'Para onde está virado', painel.virado_para_rua === true ? 'Hoje: para a rua.' : painel.virado_para_rua === false ? 'Hoje: dentro da loja.' : 'Ainda ninguém disse. A app escreve a chamada do painel para quem passa na rua ou para quem já entrou.'],
    ['manutencao', emManutencao ? 'Tirar de manutenção' : 'Pôr em manutenção',
      emManutencao
        ? (painel.conta_slug ? `Volta a «Atribuído» a ${painel.conta_nome}.` : 'Volta a «Por atribuir».')
        : (painel.conta_slug ? `Continua atribuído a ${painel.conta_nome}, marcado como em manutenção.` : 'Sai dos painéis por atribuir até voltar.')],
    painel.conta_slug && ['devolver', 'Devolver ao stock', `Sai de ${painel.conta_nome} e fica por atribuir. O cliente deixa de o ver.`],
  ].filter(Boolean);
  const lojasDestino = (lojas || []).filter((l) => l.nome !== painel.loja_nome);
  // Para onde está virado não pede motivo: é um facto da instalação.
  const pronto = accao && (accao === 'virado' ? virado !== undefined : motivo.trim() && (accao !== 'loja' || loja !== undefined)) && !aGuardar;

  async function guardar() {
    setAGuardar(true);
    setErro(null);
    try {
      const corpo = { motivo: motivo.trim() };
      let mensagem;
      if (accao === 'virado') {
        await api.patch(`/api/admin/hardware/stock/${painel.id}/virado`, { virado_para_rua: virado });
        mensagem = `${painel.serial}: virado ${virado ? 'para a rua' : 'para dentro da loja'}.`;
      } else if (accao === 'devolver') {
        await api.post(`/api/admin/hardware/stock/${painel.id}/devolver`, corpo);
        mensagem = `${painel.serial} voltou ao stock.`;
      } else if (accao === 'loja') {
        const r = await api.patch(`/api/admin/hardware/stock/${painel.id}/loja`, { ...corpo, loja_id: loja });
        mensagem = r.loja ? `${painel.serial} passou para ${r.loja}.` : `${painel.serial} ficou sem loja.`;
      } else {
        await api.post(`/api/admin/hardware/stock/${painel.id}/manutencao`, { ...corpo, em_manutencao: !emManutencao });
        mensagem = emManutencao ? `${painel.serial} saiu de manutenção.` : `${painel.serial} está em manutenção.`;
      }
      onConcluido(mensagem);
    } catch (e) {
      setErro(e.message || 'Não foi possível mudar.');
      setAGuardar(false);
    }
  }

  const opcao = (activa, aoClicar, titulo, nota) => (
    <button
      type="button"
      aria-pressed={!!activa}
      onClick={aoClicar}
      className={`block w-full bg-card px-4 py-2.5 text-left text-sm outline-none focus-visible:ring-1 focus-visible:ring-accent ${
        activa ? 'text-primary' : 'text-foreground hover:bg-secondary'
      }`}
    >
      {titulo}
      {nota && <span className="mt-0.5 block text-xs text-muted-foreground">{nota}</span>}
    </button>
  );

  return (
    <Modal
      aberto={!!painel}
      onFechar={onFechar}
      titulo={`Mudar ${painel.serial}`}
      acoes={
        <>
          <button
            type="button"
            onClick={onFechar}
            className="border border-border px-4 py-2 text-sm uppercase tracking-wider text-muted-foreground outline-none hover:text-foreground focus-visible:ring-1 focus-visible:ring-accent"
          >
            Cancelar
          </button>
          <button
            type="button"
            disabled={!pronto}
            onClick={guardar}
            className="bg-primary px-4 py-2 text-sm uppercase tracking-wider text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-1 focus-visible:ring-accent disabled:cursor-not-allowed disabled:opacity-40"
          >
            {aGuardar ? 'A guardar…' : 'Confirmar'}
          </button>
        </>
      }
    >
      <p className="text-xs text-muted-foreground">
        {painel.tipo_nome || painel.tipo}
        {painel.conta_nome ? ` · ${painel.conta_nome}${painel.loja_nome ? `, ${painel.loja_nome}` : ''}` : ' · sem cliente'}
      </p>
      {/* Desligado: o que fazer, antes das acções (comercial virgem A5: o diálogo não dizia que estava desligado). */}
      {painel.online === false && (
        <p role="status" className="mt-2 border-l-2 border-accent pl-3 text-xs text-foreground">
          Desligado na VNNOX. Confirma no local se o painel tem corrente e internet; se não voltar, «Pôr em manutenção» avisa o cliente.
        </p>
      )}

      <div className="mt-4 space-y-px bg-border">
        {accoes.map(([chave, titulo, nota]) => (
          <div key={chave}>{opcao(accao === chave, () => setAccao(chave), titulo, nota)}</div>
        ))}
      </div>

      {accao === 'virado' && (
        <div className="mt-5">
          <p className="label-eyebrow">Virado para</p>
          <div className="mt-2 space-y-px bg-border">
            <div>{opcao(virado === false, () => setVirado(false), 'Dentro da loja', 'Vê-o quem já entrou.')}</div>
            <div>{opcao(virado === true, () => setVirado(true), 'A rua', 'Montra ou exterior: vê-o quem passa.')}</div>
          </div>
        </div>
      )}

      {accao === 'loja' && (
        <div className="mt-5">
          <p className="label-eyebrow">Para que loja</p>
          {!lojas && !erro ? (
            <AProcessar className="mt-2" texto="A ler as lojas" />
          ) : (
            <div className="mt-2 max-h-40 space-y-px overflow-y-auto bg-border">
              {lojasDestino.map((l) => (
                <div key={l.id}>{opcao(loja === l.id, () => setLoja(l.id), l.nome)}</div>
              ))}
              {painel.loja_nome && <div>{opcao(loja === null, () => setLoja(null), 'Sem loja por agora')}</div>}
              {!lojasDestino.length && !painel.loja_nome && (
                <p className="bg-card px-4 py-3 text-xs text-muted-foreground">{painel.conta_nome} não tem outras lojas.</p>
              )}
            </div>
          )}
        </div>
      )}

      {accao && accao !== 'virado' && (
        <label className="mt-5 block">
          <span className="label-eyebrow">Porquê</span>
          <input
            value={motivo}
            onChange={(e) => setMotivo(e.target.value)}
            placeholder={accao === 'devolver' ? 'Ex.: fim de contrato, painel recolhido' : accao === 'loja' ? 'Ex.: a loja mudou de morada' : 'Ex.: avaria no módulo, enviado para reparação'}
            className="mt-1 w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus-visible:border-accent"
          />
          <span className="mt-1 block text-xs text-muted-foreground">Fica no registo de auditoria.</span>
        </label>
      )}

      {erro && (
        <div className="mt-5 border border-[hsl(var(--offline))] border-l-2 bg-[hsl(var(--offline)/0.08)] p-3" role="alert">
          <p className="text-xs text-foreground">{erro}</p>
        </div>
      )}
    </Modal>
  );
}

/**
 * A medida de um painel, e o player da VNNOX a que corresponde.
 *
 * Um modal e não edição na linha: são quatro campos que só fazem sentido
 * juntos, e a largura sem a altura não compõe nada — o servidor recusa-a.
 */
function ModalRegistarPainel({ aberto, player = null, onFechar, onConcluido }) {
  const [campos, setCampos] = React.useState({ serial: '', tipo: '', modelo: '', largura_px: '', altura_px: '', garantia_ate: '' });
  const [tipos, setTipos] = React.useState(null);
  const [erro, setErro] = React.useState(null);
  const [ocupado, setOcupado] = React.useState(false);
  React.useEffect(() => {
    if (!aberto) return;
    let activo = true;
    setCampos({ serial: player ? String(player.sn || player.id).replace(/[^A-Za-z0-9._:/-]/g, '').slice(0, 64) : '', tipo: '', modelo: player?.modelo || '', largura_px: player?.largura || '', altura_px: player?.altura || '', garantia_ate: '' }); setErro(null); setTipos(null);
    api.get('/api/admin/hardware/tipos').then((d) => { if (activo) { setTipos(d.tipos); setCampos((c) => ({ ...c, tipo: d.tipos[0]?.codigo || '' })); } }).catch((e) => { if (activo) setErro(e.message || 'Não foi possível ler os tipos de painel. Fecha e tenta novamente.'); });
    return () => { activo = false; };
  }, [aberto, player]);
  const mudar = (chave) => (e) => setCampos((c) => ({ ...c, [chave]: e.target.value }));
  const guardar = async (e) => {
    e.preventDefault(); setOcupado(true); setErro(null);
    try {
      await api.post('/api/admin/hardware/stock', { ...campos, largura_px: Number(campos.largura_px), altura_px: Number(campos.altura_px), ...(player ? { vnnox_player_id: player.id } : {}) });
      onConcluido('Painel registado no stock. Já o podes atribuir a uma conta.');
    } catch (e) { setErro(e.message || 'Não foi possível registar o painel.'); }
    finally { setOcupado(false); }
  };
  return <Modal aberto={aberto} onFechar={() => { if (!ocupado) onFechar(); }} titulo="Registar painel" acoes={<><button type="button" onClick={onFechar} disabled={ocupado} className="border border-border px-4 py-2 text-sm">Cancelar</button><button type="submit" form="registar-painel-form" disabled={ocupado || !tipos?.length} className="bg-primary px-4 py-2 text-sm text-primary-foreground">{ocupado ? 'A registar…' : 'Registar painel'}</button></>}>
    <p className="text-sm text-muted-foreground">{player ? <>Fica ligado ao player <span className="text-foreground">{player.nome || player.id}</span> da VNNOX, com a resolução que ele indica. Confirma o tipo e regista; depois atribui-o ao cliente e à loja.</> : 'Regista o equipamento e a resolução em píxeis. Depois atribui-o ao cliente e à loja. A ligação ao player configura-se em Medida.'}</p>
    <form id="registar-painel-form" onSubmit={guardar} className="mt-4 space-y-4">
      <fieldset disabled={ocupado} className="grid gap-4 md:grid-cols-2">
        <legend className="sr-only">Dados do equipamento</legend>
        <div><label htmlFor="painel-novo-serial" className="text-sm">Número de série</label><input id="painel-novo-serial" required maxLength={64} value={campos.serial} onChange={mudar('serial')} className="mt-1 w-full border border-border bg-background px-3 py-2 text-sm focus-visible:border-primary" /></div>
        <div><label htmlFor="painel-novo-modelo" className="text-sm">Modelo (opcional)</label><input id="painel-novo-modelo" maxLength={128} value={campos.modelo} onChange={mudar('modelo')} className="mt-1 w-full border border-border bg-background px-3 py-2 text-sm focus-visible:border-primary" /></div>
        <div className="md:col-span-2"><label htmlFor="painel-novo-tipo" className="text-sm">Tipo</label><select id="painel-novo-tipo" required disabled={!tipos?.length} value={campos.tipo} onChange={mudar('tipo')} className="mt-1 w-full border border-border bg-background px-3 py-2 text-sm">{!tipos?.length ? <option value="">{tipos ? 'Sem tipos de painel disponíveis' : 'A carregar os tipos…'}</option> : tipos.map((t) => <option key={t.codigo} value={t.codigo}>{t.nome}</option>)}</select></div>
        {['largura_px', 'altura_px'].map((chave) => <div key={chave}><label htmlFor={`painel-novo-${chave}`} className="text-sm">{chave === 'largura_px' ? 'Largura (px)' : 'Altura (px)'}</label><input id={`painel-novo-${chave}`} type="number" min="1" max="16384" required value={campos[chave]} onChange={mudar(chave)} className="mt-1 w-full border border-border bg-background px-3 py-2 text-sm focus-visible:border-primary" /></div>)}
        {/* A garantia, opcional: a lista mostrava-a e o registo não a pedia (agentes, 20 Out). */}
        <div><label htmlFor="painel-novo-garantia" className="text-sm">Garantia até (opcional)</label><input id="painel-novo-garantia" type="date" value={campos.garantia_ate} onChange={mudar('garantia_ate')} className="mt-1 w-full border border-border bg-background px-3 py-2 text-sm focus-visible:border-primary" /></div>
      </fieldset>
      {erro && <p role="alert" className="text-sm">{erro}</p>}
    </form>
  </Modal>;
}

function ModalMedidaPainel({ painel, onFechar, onConcluido }) {
  const [valores, setValores] = React.useState({});
  const [aGuardar, setAGuardar] = React.useState(false);
  const [erro, setErro] = React.useState(null);
  // A pesquisa de players da VNNOX: em vez de copiar o identificador de outra
  // janela, escolhe-se da lista — e a resolução vem com ele.
  const [termo, setTermo] = React.useState('');
  const [players, setPlayers] = React.useState(null);
  const [erroVnnox, setErroVnnox] = React.useState(null);
  const [veioDaVnnox, setVeioDaVnnox] = React.useState(false);

  React.useEffect(() => {
    if (!painel) return undefined;
    let vivo = true;
    const t = setTimeout(() => {
      api.get(`/api/admin/vnnox/players?q=${encodeURIComponent(termo)}`)
        .then((r) => { if (vivo) { setPlayers(r.players); setErroVnnox(null); } })
        .catch((e) => { if (vivo) setErroVnnox(e.message); });
    }, 250);
    return () => { vivo = false; clearTimeout(t); };
  }, [painel, termo]);

  const escolherPlayer = (p) => {
    setValores((v) => ({
      ...v,
      vnnox_player_id: p.id,
      largura_px: p.largura ?? v.largura_px,
      altura_px: p.altura ?? v.altura_px,
    }));
    setVeioDaVnnox(Boolean(p.largura && p.altura));
  };

  React.useEffect(() => {
    if (!painel) return;
    setValores({
      largura_px: painel.largura_px ?? '',
      altura_px: painel.altura_px ?? '',
      pitch_mm: painel.pitch_mm ?? '',
      vnnox_player_id: painel.vnnox_player_id ?? '',
    });
    setErro(null);
    setAGuardar(false);
    setTermo('');
    setPlayers(null);
    setVeioDaVnnox(false);
  }, [painel]);

  async function guardar(e) {
    e.preventDefault();
    setAGuardar(true);
    setErro(null);
    try {
      const vazioANull = (v) => (v === '' ? null : v);
      const r = await api.patch(`/api/admin/hardware/stock/${painel.id}`, {
        largura_px: vazioANull(valores.largura_px),
        altura_px: vazioANull(valores.altura_px),
        pitch_mm: vazioANull(valores.pitch_mm),
        vnnox_player_id: vazioANull(valores.vnnox_player_id),
      });
      onConcluido(r.resolucao ? `${r.serial}: ${r.resolucao}.` : `${r.serial}: medida apagada.`);
    } catch (err) {
      setErro(err.message || 'Não foi possível guardar.');
      setAGuardar(false);
    }
  }

  const campo = (chave, rotulo, props) => (
    <label className="block">
      <span className="label-eyebrow">{rotulo}</span>
      <input
        value={valores[chave] ?? ''}
        onChange={(e) => setValores((v) => ({ ...v, [chave]: e.target.value }))}
        className="mt-1 w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-accent"
        {...props}
      />
    </label>
  );

  return (
    <Modal
      aberto={!!painel}
      onFechar={onFechar}
      titulo={painel ? `Medida de ${painel.serial}` : 'Medida'}
      acoes={
        <>
          <button
            type="button"
            onClick={onFechar}
            className="border border-border px-4 py-2 text-sm uppercase tracking-wider text-muted-foreground outline-none hover:text-foreground focus-visible:ring-1 focus-visible:ring-accent"
          >
            Cancelar
          </button>
          <button
            type="submit"
            form="form-medida-painel"
            disabled={aGuardar}
            className="bg-primary px-4 py-2 text-sm uppercase tracking-wider text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-1 focus-visible:ring-accent disabled:cursor-not-allowed disabled:opacity-40"
          >
            {aGuardar ? 'A guardar…' : 'Guardar'}
          </button>
        </>
      }
    >
      {painel && (
        <form id="form-medida-painel" onSubmit={guardar} className="space-y-4">
          <p className="text-xs text-muted-foreground">
            {painel.tipo_nome || painel.tipo}. A resolução é a do próprio aparelho: o mesmo modelo muda de
            píxeis com o pitch.
          </p>
          <div className="grid grid-cols-2 gap-3">
            {campo('largura_px', 'Largura (px)', { inputMode: 'numeric', 'data-foco-inicial': true })}
            {campo('altura_px', 'Altura (px)', { inputMode: 'numeric' })}
          </div>
          {veioDaVnnox && (
            <p className="-mt-2 text-xs text-muted-foreground">A resolução veio da VNNOX, com o player.</p>
          )}
          {campo('pitch_mm', 'Pitch (mm)', { inputMode: 'decimal' })}
          {campo('vnnox_player_id', 'Player VNNOX', { placeholder: 'Por ligar' })}

          <div>
            <label htmlFor="medida-procurar" className="label-eyebrow">Procurar o player</label>
            <input
              id="medida-procurar"
              type="search"
              value={termo}
              onChange={(e) => setTermo(e.target.value)}
              placeholder="Nome, número de série ou identificador"
              className="mt-1 w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus-visible:border-accent"
            />
            {erroVnnox ? (
              <p className="mt-2 text-xs text-perigo-texto">{erroVnnox}</p>
            ) : !players ? (
              <AProcessar className="mt-2" texto="A ler a VNNOX" />
            ) : players.length === 0 ? (
              <p className="mt-2 text-xs text-muted-foreground">Nenhum player com isso.</p>
            ) : (
              <ul className="mt-2 max-h-48 space-y-px overflow-y-auto bg-border">
                {players.map((p) => {
                  const ocupado = p.ligado_a && p.ligado_a !== painel.serial;
                  const escolhido = valores.vnnox_player_id === p.id;
                  return (
                    <li key={p.id}>
                      <button
                        type="button"
                        disabled={ocupado}
                        aria-pressed={!!escolhido}
                        onClick={() => escolherPlayer(p)}
                        className={`flex w-full items-center justify-between gap-3 bg-card px-3 py-2 text-left text-sm outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:cursor-not-allowed disabled:opacity-50 ${
                          escolhido ? 'text-primary' : 'text-foreground hover:bg-secondary'
                        }`}
                      >
                        <span className="min-w-0">
                          <span className="block truncate">{p.nome || p.id}</span>
                          <span className="block text-xs text-muted-foreground">
                            {p.largura && p.altura ? `${p.largura}×${p.altura}` : 'sem medida'}
                            {p.modelo ? ` · ${p.modelo}` : ''}
                            {ocupado ? ` · já ligado a ${p.ligado_a}` : ''}
                          </span>
                        </span>
                        <span
                          className="h-2 w-2 shrink-0"
                          title={p.online ? 'online' : 'offline'}
                          style={{ background: `hsl(var(${p.online ? '--online' : '--offline'}))` }}
                        />
                      </button>
                    </li>
                  );
                })}
              </ul>
            )}
          </div>
          {erro && (
            <div
              className="border border-[hsl(var(--offline))] border-l-2 bg-[hsl(var(--offline)/0.08)] p-3"
              role="alert"
            >
              <p className="text-xs text-foreground">{erro}</p>
            </div>
          )}
        </form>
      )}
    </Modal>
  );
}

/**
 * Atribuir painéis — um modal.
 *
 * Duas escolhas em cadeia, cliente e depois loja. A loja é opcional: um cliente
 * que ainda não criou lojas recebe o hardware na mesma, e a loja associa-se
 * depois. Sem isto, essas contas nunca teriam painéis.
 */
function ModalAtribuirPaineis({ aberto, paineis, contaInicial, onFechar, onConcluido }) {
  const [contas, setContas] = React.useState(null);
  const [filtro, setFiltro] = React.useState('');
  const [conta, setConta] = React.useState(null);
  const [lojas, setLojas] = React.useState(null);
  // undefined = ainda não escolheu · null = "sem loja" · string = id da loja
  const [loja, setLoja] = React.useState(undefined);
  const [aGuardar, setAGuardar] = React.useState(false);
  const [erro, setErro] = React.useState(null);

  React.useEffect(() => {
    if (!aberto) return;
    setFiltro('');
    setConta(null);
    setLojas(null);
    setLoja(undefined);
    setErro(null);
    setAGuardar(false);
    api
      .get('/api/admin/accounts')
      .then((r) => {
        const abertas = r.contas.filter((c) => c.estado !== 'cancelado');
        setContas(abertas);
        const inicial = contaInicial && abertas.find((c) => c.slug === contaInicial);
        if (inicial) escolherConta(inicial);
      })
      .catch((e) => setErro(e.message || 'Não foi possível ler os clientes.'));
  }, [aberto]);

  function escolherConta(c) {
    setConta(c);
    setLojas(null);
    setLoja(undefined);
    api
      .get(`/api/admin/accounts/${c.slug}`)
      .then((d) => {
        setLojas(d.lojas);
        // Sem lojas não há nada para escolher: fica "sem loja" à partida.
        if (!d.lojas.length) setLoja(null);
      })
      .catch((e) => setErro(e.message || 'Não foi possível ler as lojas.'));
  }

  async function atribuir() {
    setAGuardar(true);
    setErro(null);
    try {
      const r = await api.post('/api/admin/hardware/allocations', {
        hardware_ids: paineis.map((p) => p.id),
        account: conta.slug,
        loja_id: loja || null,
      });
      onConcluido(
        `${r.atribuidos} ${r.atribuidos === 1 ? 'painel atribuído' : 'painéis atribuídos'} a ${r.conta}${(() => { const l = loja && (lojas || []).find((x) => x.id === loja); return l ? `, na loja ${l.nome}` : ''; })()}.`
      );
    } catch (e) {
      setErro(e.message || 'Não foi possível atribuir.');
      setAGuardar(false);
    }
  }

  const termo = filtro.trim().toLowerCase();
  const visiveis = (contas || []).filter(
    (c) => !termo || c.nome.toLowerCase().includes(termo) || c.slug.includes(termo)
  );

  const opcao = (activa, aoClicar, children, extra) => (
    <button
      type="button"
      aria-pressed={!!activa}
      onClick={aoClicar}
      className={`flex w-full items-center justify-between gap-3 bg-card px-4 py-2.5 text-left text-sm outline-none focus-visible:ring-1 focus-visible:ring-accent ${
        activa ? 'text-primary' : 'text-foreground hover:bg-secondary'
      }`}
    >
      <span className="min-w-0 truncate">{children}</span>
      {extra && <span className="shrink-0 text-xs text-muted-foreground">{extra}</span>}
    </button>
  );

  const n = paineis.length;

  return (
    <Modal
      aberto={aberto}
      onFechar={onFechar}
      titulo="Atribuir painéis"
      acoes={
        <>
          <button
            type="button"
            onClick={onFechar}
            className="border border-border px-4 py-2 text-sm uppercase tracking-wider text-muted-foreground outline-none hover:text-foreground focus-visible:ring-1 focus-visible:ring-accent"
          >
            Cancelar
          </button>
          <button
            type="button"
            disabled={!conta || loja === undefined || aGuardar}
            onClick={atribuir}
            className="bg-primary px-4 py-2 text-sm uppercase tracking-wider text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-1 focus-visible:ring-accent disabled:cursor-not-allowed disabled:opacity-40"
          >
            {aGuardar ? 'A atribuir…' : 'Atribuir'}
          </button>
        </>
      }
    >
      <p className="text-xs text-muted-foreground">
        {n} {n === 1 ? 'painel' : 'painéis'}: {paineis.slice(0, 3).map((p) => p.serial).join(', ')}
        {n > 3 ? ` e mais ${n - 3}` : ''}
      </p>

      <div className="mt-5">
        <p className="label-eyebrow">Cliente</p>
        <input
          type="search"
          value={filtro}
          onChange={(e) => setFiltro(e.target.value)}
          placeholder="Pesquisar cliente"
          aria-label="Pesquisar cliente"
          data-foco-inicial
          className="mt-2 w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus-visible:border-accent"
        />
        <div className="mt-2 max-h-48 space-y-px overflow-y-auto bg-border">
          {!contas && !erro && <AProcessar className="bg-card px-4 py-3" texto="A ler os clientes" />}
          {contas && !visiveis.length && (
            <p className="bg-card px-4 py-3 text-xs text-muted-foreground">Nenhum cliente com esse nome.</p>
          )}
          {visiveis.map((c) => (
            <div key={c.slug}>
              {/* Duas contas com o mesmo nome distinguem-se pelo endereço (ensaio de 9 Out: dois «NESCAFÉ / Retalho»). */}
              {opcao(conta && conta.slug === c.slug, () => escolherConta(c), c.nome,
                `${ROTULO_TIPO[c.tipo] || c.tipo}${contas.filter((x) => x.nome === c.nome).length > 1 ? ` · ${c.slug}` : ''}`)}
            </div>
          ))}
        </div>
      </div>

      {conta && (
        <div className="mt-5">
          <p className="label-eyebrow">Loja</p>
          {!lojas ? (
            <AProcessar className="mt-2" texto="A ler as lojas" />
          ) : (
            <>
              {!lojas.length && (
                <p className="mt-2 text-xs text-muted-foreground">
                  {conta.nome} ainda não tem lojas. Os painéis ficam atribuídos à conta e a loja associa-se
                  depois.
                </p>
              )}
              <div className="mt-2 max-h-40 space-y-px overflow-y-auto bg-border">
                {lojas.map((l) => (
                  <div key={l.id}>{opcao(loja === l.id, () => setLoja(l.id), l.nome)}</div>
                ))}
                <div>{opcao(loja === null, () => setLoja(null), 'Sem loja por agora')}</div>
              </div>
            </>
          )}
        </div>
      )}

      {erro && (
        <div
          className="mt-5 border border-[hsl(var(--offline))] border-l-2 bg-[hsl(var(--offline)/0.08)] p-3"
          role="alert"
        >
          <p className="text-xs text-foreground">{erro}</p>
        </div>
      )}
    </Modal>
  );
}

/**
 * A rede como a VNNOX a vê, ao vivo — os players todos da conta NetScreen.
 *
 * Só leitura: nada daqui entra no inventário. Um player passa a painel pelo
 * "Medida" de um painel do stock. O estado do Care liga-se pelo nome (o `sn`
 * do Care é o do ecrã, não o do player); um player sem Care mostra "—", que não é o mesmo que avariado.
 */
const ESTADOS_CARE = {
  1: { texto: 'Normal', variante: 'sucesso' },
  2: { texto: 'Offline', variante: 'contorno' },
  3: { texto: 'Em risco', variante: 'accent' },
  4: { texto: 'Avariado', variante: 'erro' },
};

function RedeVnnox({ ehAdmin = false, onRegistar = () => {} }) {
  const [dados, setDados] = React.useState(null);
  const [erro, setErro] = React.useState(null);
  // «Ao vivo» quer dizer a hora da consulta à vista e um botão para a repetir (Codex, 3 Nov: era um
  // único pedido ao abrir, sem hora).
  const [consultadoEm, setConsultadoEm] = React.useState(null);
  const [aConsultar, setAConsultar] = React.useState(false);
  const consultar = React.useCallback(() => {
    setAConsultar(true);
    api
      .get('/api/admin/vnnox/rede')
      .then((d) => { setDados(d); setErro(null); setConsultadoEm(new Date()); })
      .catch((e) => setErro(e.message || 'A VNNOX não respondeu.'))
      .finally(() => setAConsultar(false));
  }, []);

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

  if (erro) {
    return (
      <div className="border border-border bg-card px-6 py-12 text-center">
        <p className="font-display text-xl tracking-wide">Sem ligação à VNNOX</p>
        <p className="mt-2 text-sm text-muted-foreground">{erro}</p>
        {/* Sem isto, uma falha da VNNOX deixava a lista sem forma de voltar a consultar (Codex X-11, 3 Nov). */}
        <button type="button" onClick={consultar} disabled={aConsultar} className="ns-btn ns-btn-sm mt-4 disabled:opacity-50">
          {aConsultar ? 'A consultar…' : 'Tentar outra vez'}
        </button>
      </div>
    );
  }

  const players = dados ? dados.players : [];
  const online = players.filter((p) => p.online).length;
  const contarCare = (n) => players.filter((p) => p.care_estado === n).length;

  const colunas = [
    {
      chave: 'nome',
      rotulo: 'Player',
      ordenavel: true,
      principal: true,
      render: (p) => (
        <div>
          <p className="text-sm text-foreground">{p.nome || p.id}</p>
          <p className="text-xs text-muted-foreground">{[p.modelo, p.sn].filter(Boolean).join(' · ')}</p>
        </div>
      ),
    },
    {
      chave: 'resolucao',
      rotulo: 'Resolução',
      ordenavel: true,
      valorOrdenacao: (p) => (p.largura || 0) * (p.altura || 0),
      render: (p) => (
        <span className="font-mono text-sm tabular-nums">{p.largura && p.altura ? `${p.largura}×${p.altura}` : '—'}</span>
      ),
    },
    {
      chave: 'online',
      rotulo: 'Ligação',
      ordenavel: true,
      valorOrdenacao: (p) => (p.online ? 1 : 0),
      render: (p) => (
        <span className="inline-flex items-center gap-2 text-sm">
          <span
            className="h-2 w-2"
            style={{ background: `hsl(var(${p.online ? '--online' : '--offline'}))` }}
            aria-hidden="true"
          />
          {p.online ? 'Online' : 'Offline'}
        </span>
      ),
    },
    {
      chave: 'care_estado',
      rotulo: 'Care',
      ordenavel: true,
      render: (p) => {
        const e = ESTADOS_CARE[p.care_estado];
        return e ? <Badge variante={e.variante}>{e.texto}</Badge> : <span className="text-muted-foreground">—</span>;
      },
    },
    {
      chave: 'care_brilho',
      rotulo: 'Brilho',
      ordenavel: true,
      alinhamento: 'direita',
      render: (p) => (
        <span className="font-mono text-sm tabular-nums">{p.care_brilho != null && p.care_brilho >= 0 ? `${p.care_brilho}%` : '—'}</span>
      ),
    },
    {
      chave: 'painel',
      rotulo: 'No inventário',
      ordenavel: true,
      render: (p) =>
        p.painel ? (
          <div>
            <p className="text-sm text-foreground">{p.painel}</p>
            <p className="text-xs text-muted-foreground">{p.conta_nome || 'por atribuir'}</p>
          </div>
        ) : ehAdmin ? (
          <button type="button" onClick={() => onRegistar(p)} className="border border-border px-3 py-1 text-sm hover:border-primary">Registar no stock</button>
        ) : (
          <span className="text-muted-foreground" title="O registo no stock é feito por um Admin da equipa.">—</span>
        ),
    },
  ];

  return (
    <div className="space-y-3">
      {dados && (
        <p className="text-sm text-muted-foreground">
          {dados.total} players · {online} online · Care: {contarCare(1)} normais,{' '}
          <span className={contarCare(3) ? 'text-accent' : ''}>{contarCare(3)} em risco</span>, {contarCare(4)} avariados
          {dados.care_erro && <span className="text-accent"> · o Care não respondeu</span>}
          {consultadoEm && <> · consultado às {consultadoEm.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' })}</>}
          {' '}
          <button type="button" onClick={consultar} disabled={aConsultar} className="ml-1 text-xs underline underline-offset-2 hover:text-foreground disabled:opacity-50">
            {aConsultar ? 'A consultar…' : 'Actualizar'}
          </button>
        </p>
      )}
      <Tabela
        colunas={colunas}
        linhas={players}
        carregando={!dados}
        chaveLinha={(p) => p.id}
        ordemInicial={{ chave: 'online', sentido: 'desc' }}
        pesquisa={{ campos: ['nome', 'sn', 'modelo', 'painel', 'conta_nome'], placeholder: 'Pesquisar player, modelo ou nº de série' }}
        vazio={{ titulo: 'Sem players', descricao: 'A conta VNNOX não devolveu players.' }}
      />
    </div>
  );
}
