Iniciativa Cidadã Independente · Transparência Parlamentar
TSX770 linhas36.776 bytes

apps/web/src/app/admin/premiacao/page.tsx

SHA-256

8f3bf8329b971eb6210f7ee0d39218e0ada383eb7da2d71478cf2b16c15ee2cc

Somente leiturafonte-0531042d9f3e
'use client'

import { useCallback, useEffect, useMemo, useState } from 'react'

const API = process.env.NEXT_PUBLIC_API_URL ?? 'https://api.observasenado.org'
const SECRET_KEY = 'observa_admin_secret_v1'
const BASE = `${API}/api/premiacao/admin`

type Status = 'rascunho' | 'apurando' | 'apurada' | 'aprovada' | 'divulgada'
type Eixo = 'grande_premio' | 'regional' | 'estadual' | 'tematico'

interface Edicao {
  id: string
  tipo: 'legislatura' | 'ano'
  titulo: string
  subtitulo: string
  periodo: { inicio: string; fim: string }
  periodoEmCurso: boolean
  status: Status
  fonte: string | null
  apuracaoSha256: string | null
  erro: string | null
  apuradoEm: string | null
  aprovadoEm: string | null
  divulgadoEm: string | null
  regua: { mesesMinReferencia: number; mesesMinVitoria: number; minLinhas: number }
}

interface Possivel {
  id: string
  tipo: 'legislatura' | 'ano'
  titulo: string
  periodo: { inicio: string; fim: string }
}

interface Premiado {
  posicao: number
  senadorCod: string
  nome: string
  partido: string
  uf: string
  valor: number
  idsTotal: number
  mesesAtivos: number
  indicador: { rotulo: string; valor: number; unidade: string; detalhe: string }
}

interface Categoria {
  id: string
  eixo: Eixo
  titulo: string
  descricao: string
  elegiveis: number
  vencedor: Premiado
  podio: Premiado[]
  houveDesempate: boolean
}

interface Apuracao {
  metodologiaVersao?: string
  snapshot: string
  apuradoEm: string
  coorte: { avaliados: number; elegiveis: number; concorrentes: number; mesesMinimos: number; mesesMinimosVitoria: number }
  categorias: Categoria[]
}

interface CertResumo {
  codigo: string
  categoria: string
  eixo: Eixo
  pdfGerado: boolean
  pdfSha256: string | null
  enviadoEm: string | null
  envios: number
  envioErro: string | null
  revogadoEm: string | null
  motivoRevogacao: string | null
}

interface SenadorPremiado {
  senadorCod: string
  nome: string
  tratamento: string
  partido: string
  uf: string
  email: string | null
  emailOficial: string | null
  certificados: CertResumo[]
}

interface Snapshot {
  computedAt: string
  total: number
  apto: boolean
}

interface Detalhe {
  edicao: Edicao
  apuracao: Apuracao | null
  snapshots: Snapshot[]
  senadores: SenadorPremiado[]
}

const EIXOS: { id: Eixo; nome: string }[] = [
  { id: 'grande_premio', nome: 'Grande Prêmio' },
  { id: 'regional', nome: 'Regionais' },
  { id: 'estadual', nome: 'Estaduais' },
  { id: 'tematico', nome: 'Temáticos' },
]

const STATUS_COR: Record<Status, string> = {
  rascunho: 'bg-gray-100 text-gray-700',
  apurando: 'bg-blue-100 text-blue-800',
  apurada: 'bg-amber-100 text-amber-800',
  aprovada: 'bg-green-100 text-green-800',
  divulgada: 'bg-primary text-white',
}

function fmtData(s: string | null | undefined): string {
  if (!s) return '—'
  const d = new Date(s.length === 10 ? `${s}T12:00:00` : s)
  return d.toLocaleDateString('pt-BR') + (s.length > 10 ? ' ' + d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '')
}

function fmtNum(v: number): string {
  return v.toLocaleString('pt-BR', { maximumFractionDigits: 1 })
}

function Badge({ status }: { status: Status }) {
  return <span className={`inline-block rounded px-2 py-0.5 text-[11px] font-medium ${STATUS_COR[status]}`}>{status}</span>
}

function Botao({
  children, onClick, tom = 'primario', disabled,
}: { children: React.ReactNode; onClick: () => void; tom?: 'primario' | 'secundario' | 'perigo'; disabled?: boolean }) {
  const cls = {
    primario: 'bg-primary text-white hover:bg-primary-hover',
    secundario: 'border border-border bg-surface text-ink hover:border-primary',
    perigo: 'border border-danger text-danger hover:bg-danger hover:text-white',
  }[tom]
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`rounded-sm px-3 py-1.5 text-xs font-medium uppercase tracking-wider transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${cls}`}
    >
      {children}
    </button>
  )
}

export default function AdminPremiacaoPage() {
  const [secret, setSecret] = useState('')
  const [authed, setAuthed] = useState(false)
  const [edicoes, setEdicoes] = useState<Edicao[]>([])
  const [possiveis, setPossiveis] = useState<Possivel[]>([])
  const [selecionada, setSelecionada] = useState<string | null>(null)
  const [detalhe, setDetalhe] = useState<Detalhe | null>(null)
  const [snapshot, setSnapshot] = useState('')
  const [eixo, setEixo] = useState<Eixo>('grande_premio')
  const [mensagem, setMensagem] = useState('')
  const [emailTeste, setEmailTeste] = useState('')
  const [emails, setEmails] = useState<Record<string, string>>({})
  const [ocupado, setOcupado] = useState<string | null>(null)
  const [log, setLog] = useState('')
  const [previa, setPrevia] = useState<{ assunto: string; html: string; para: string | null; anexos: string[] } | null>(null)

  const chamar = useCallback(
    async (path: string, init: RequestInit = {}) => {
      const r = await fetch(`${BASE}${path}`, {
        ...init,
        headers: { 'Content-Type': 'application/json', 'X-Admin-Secret': secret, ...(init.headers ?? {}) },
      })
      const data = await r.json().catch(() => ({}))
      if (!r.ok) throw new Error((data as { error?: string }).error ?? `HTTP ${r.status}`)
      return data
    },
    [secret],
  )

  async function entrar(s: string): Promise<boolean> {
    const r = await fetch(`${BASE}/edicoes`, { headers: { 'X-Admin-Secret': s } }).catch(() => null)
    if (!r?.ok) return false
    const data = await r.json()
    setEdicoes(data.edicoes)
    setPossiveis(data.possiveis)
    setAuthed(true)
    localStorage.setItem(SECRET_KEY, s)
    if (data.edicoes[0]) setSelecionada((atual) => atual ?? data.edicoes[0].id)
    return true
  }

  useEffect(() => {
    const s = localStorage.getItem(SECRET_KEY)
    if (s) {
      setSecret(s)
      void entrar(s)
    }
  }, [])

  const carregarLista = useCallback(async () => {
    const data = await chamar('/edicoes')
    setEdicoes(data.edicoes)
    setPossiveis(data.possiveis)
  }, [chamar])

  const carregarDetalhe = useCallback(async (id: string) => {
    const data = (await chamar(`/edicoes/${id}`)) as Detalhe
    setDetalhe(data)
    setEmails(Object.fromEntries(data.senadores.map((s) => [s.senadorCod, s.email ?? ''])))
    return data
  }, [chamar])

  useEffect(() => {
    if (authed && selecionada) void carregarDetalhe(selecionada).catch((e) => setLog(String(e)))
  }, [authed, selecionada, carregarDetalhe])

  // Apuração em andamento: acompanha até terminar.
  useEffect(() => {
    if (detalhe?.edicao.status !== 'apurando') return
    const t = setInterval(async () => {
      const d = await carregarDetalhe(detalhe.edicao.id).catch(() => null)
      if (d && d.edicao.status !== 'apurando') {
        void carregarLista()
        setLog(d.edicao.erro ? `Apuração falhou: ${d.edicao.erro}` : 'Apuração concluída. Revise os vencedores antes de aprovar.')
      }
    }, 3000)
    return () => clearInterval(t)
  }, [detalhe?.edicao.status, detalhe?.edicao.id, carregarDetalhe, carregarLista])

  // Certificados acabaram de ser aprovados: os PDFs saem em segundo plano.
  const pdfsPendentes = detalhe?.senadores.some((s) => s.certificados.some((c) => !c.pdfGerado)) ?? false
  useEffect(() => {
    if (!pdfsPendentes || !detalhe) return
    const t = setInterval(() => void carregarDetalhe(detalhe.edicao.id).catch(() => null), 5000)
    return () => clearInterval(t)
  }, [pdfsPendentes, detalhe?.edicao.id, carregarDetalhe])

  async function acao(rotulo: string, fn: () => Promise<unknown>, sucesso?: string) {
    setOcupado(rotulo)
    setLog('')
    try {
      await fn()
      if (sucesso) setLog(sucesso)
      if (selecionada) await carregarDetalhe(selecionada)
      await carregarLista()
    } catch (err) {
      setLog(`Erro: ${(err as Error).message}`)
    } finally {
      setOcupado(null)
    }
  }

  const ed = detalhe?.edicao
  const categoriasDoEixo = useMemo(
    () => detalhe?.apuracao?.categorias.filter((c) => c.eixo === eixo) ?? [],
    [detalhe, eixo],
  )
  const totalCerts = detalhe?.senadores.reduce((a, s) => a + s.certificados.length, 0) ?? 0
  const enviados = detalhe?.senadores.reduce((a, s) => a + s.certificados.filter((c) => c.enviadoEm).length, 0) ?? 0
  const semEmail = detalhe?.senadores.filter((s) => !s.email).length ?? 0

  async function abrirPdf(codigo: string) {
    const r = await fetch(`${BASE}/certificados/${codigo}/pdf`, { headers: { 'X-Admin-Secret': secret } })
    if (!r.ok) {
      setLog(`Erro ao gerar o PDF: HTTP ${r.status}`)
      return
    }
    window.open(URL.createObjectURL(await r.blob()), '_blank')
    if (selecionada) void carregarDetalhe(selecionada)
  }

  async function verPrevia(s: SenadorPremiado) {
    try {
      const q = new URLSearchParams({ senador: s.senadorCod })
      if (mensagem.trim()) q.set('mensagem', mensagem.trim())
      if (s.certificados.some((c) => c.enviadoEm)) q.set('reenvio', '1')
      setPrevia(await chamar(`/edicoes/${ed!.id}/email-previa?${q}`))
    } catch (err) {
      setLog(`Erro: ${(err as Error).message}`)
    }
  }

  if (!authed) {
    return (
      <div className="max-w-md mx-auto py-12">
        <p className="eyebrow">Restrito</p>
        <h1 className="font-serif text-2xl text-ink mt-2">Admin · Premiação</h1>
        <p className="mt-2 text-sm text-muted">Informe o ADMIN_SECRET para continuar.</p>
        <form
          onSubmit={async (e) => {
            e.preventDefault()
            if (!(await entrar(secret))) alert('Secret inválido.')
          }}
          className="mt-6 flex flex-col gap-3"
        >
          <input
            type="password"
            value={secret}
            onChange={(e) => setSecret(e.target.value)}
            placeholder="ADMIN_SECRET"
            className="rounded-sm border border-border bg-surface px-3 py-2 text-sm font-mono focus:border-primary focus:outline-none"
            autoFocus
          />
          <button type="submit" className="rounded-sm bg-primary px-4 py-2 text-xs font-medium uppercase tracking-wider text-white hover:bg-primary-hover">
            Entrar
          </button>
        </form>
      </div>
    )
  }

  return (
    <div className="max-w-7xl mx-auto py-6">
      <div className="flex items-center justify-between border-b border-border pb-4 mb-6">
        <div>
          <p className="eyebrow">Admin</p>
          <h1 className="font-serif text-2xl text-ink mt-1">Prêmio Observatório do Senado</h1>
        </div>
        <div className="flex items-center gap-4 text-xs">
          <a href="/admin/newsletter/" className="text-muted hover:text-primary underline">newsletter</a>
          <button
            onClick={() => {
              localStorage.removeItem(SECRET_KEY)
              setAuthed(false)
              setSecret('')
            }}
            className="text-muted hover:text-danger underline"
          >
            sair
          </button>
        </div>
      </div>

      <div className="grid grid-cols-1 lg:grid-cols-[240px_1fr] gap-8">
        {/* ── Edições ─────────────────────────────────────────── */}
        <aside className="space-y-4">
          <div>
            <p className="eyebrow mb-2">Edições</p>
            {edicoes.length === 0 && <p className="text-sm text-muted">Nenhuma edição criada.</p>}
            <ul className="space-y-1">
              {edicoes.map((e) => (
                <li key={e.id}>
                  <button
                    onClick={() => setSelecionada(e.id)}
                    className={`w-full text-left rounded-sm px-3 py-2 text-sm border transition-colors ${
                      selecionada === e.id ? 'border-primary bg-panel' : 'border-transparent hover:border-border'
                    }`}
                  >
                    <span className="block text-ink">{e.titulo}</span>
                    <span className="mt-1 flex items-center gap-2">
                      <Badge status={e.status} />
                      {e.periodoEmCurso && <span className="text-[11px] text-warning">em curso</span>}
                    </span>
                  </button>
                </li>
              ))}
            </ul>
          </div>
          {possiveis.length > 0 && (
            <div className="border-t border-border pt-4">
              <p className="eyebrow mb-2">Nova edição</p>
              <div className="space-y-1">
                {possiveis.map((p) => (
                  <button
                    key={p.id}
                    onClick={() =>
                      void acao(`criar-${p.id}`, async () => {
                        await chamar('/edicoes', { method: 'POST', body: JSON.stringify({ id: p.id }) })
                        setSelecionada(p.id)
                      })
                    }
                    className="w-full text-left text-sm text-primary hover:underline"
                  >
                    + {p.titulo}
                  </button>
                ))}
              </div>
            </div>
          )}
        </aside>

        {/* ── Edição selecionada ─────────────────────────────── */}
        <main className="min-w-0">
          {!ed && <p className="text-sm text-muted">Crie ou selecione uma edição.</p>}
          {ed && (
            <div className="space-y-8">
              <section className="rounded-sm border border-border bg-surface p-5">
                <div className="flex flex-wrap items-start justify-between gap-4">
                  <div>
                    <h2 className="font-serif text-xl text-ink">{ed.titulo}</h2>
                    <p className="text-sm text-muted">{ed.subtitulo}</p>
                    <p className="mt-2 text-xs text-subtle">
                      Período {fmtData(ed.periodo.inicio)} a {fmtData(ed.periodo.fim)} · coorte com {ed.regua.mesesMinReferencia}+ meses ·
                      disputa com {ed.regua.mesesMinVitoria}+ meses de exercício
                    </p>
                  </div>
                  <Badge status={ed.status} />
                </div>

                {ed.periodoEmCurso && (
                  <p className="mt-4 rounded-sm bg-amber-50 border border-amber-200 px-3 py-2 text-xs text-amber-900">
                    O período desta edição termina em {fmtData(ed.periodo.fim)}. Uma apuração agora mede um período incompleto.
                  </p>
                )}
                {ed.erro && (
                  <p className="mt-4 rounded-sm bg-red-50 border border-red-200 px-3 py-2 text-xs text-red-900">{ed.erro}</p>
                )}

                <dl className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
                  <div><dt className="text-subtle">Apurada</dt><dd className="text-ink">{fmtData(ed.apuradoEm)}</dd></div>
                  <div><dt className="text-subtle">Aprovada</dt><dd className="text-ink">{fmtData(ed.aprovadoEm)}</dd></div>
                  <div><dt className="text-subtle">Divulgada</dt><dd className="text-ink">{fmtData(ed.divulgadoEm)}</dd></div>
                  <div><dt className="text-subtle">Fonte</dt><dd className="text-ink font-mono break-all">{ed.fonte ?? '—'}</dd></div>
                  {detalhe?.apuracao && (
                    <div><dt className="text-subtle">Metodologia</dt><dd className="text-ink">IDS {detalhe.apuracao.metodologiaVersao ?? 'v4'}</dd></div>
                  )}
                  {ed.apuracaoSha256 && (
                    <div className="col-span-2 md:col-span-4">
                      <dt className="text-subtle">SHA-256 do resultado</dt>
                      <dd className="text-ink font-mono break-all">{ed.apuracaoSha256}</dd>
                    </div>
                  )}
                </dl>

                <div className="mt-5 flex flex-wrap items-center gap-3">
                  {(ed.status === 'rascunho' || ed.status === 'apurada') && (
                    <>
                      {ed.tipo === 'legislatura' && detalhe!.snapshots.length > 0 && (
                        <select
                          value={snapshot}
                          onChange={(e) => setSnapshot(e.target.value)}
                          className="rounded-sm border border-border bg-surface px-2 py-1.5 text-xs"
                        >
                          <option value="">Snapshot mais recente apto</option>
                          {detalhe!.snapshots.map((s) => (
                            <option key={s.computedAt} value={s.computedAt} disabled={!s.apto}>
                              {fmtData(s.computedAt)} · {s.total} senadores{s.apto ? '' : ' (incompleto)'}
                            </option>
                          ))}
                        </select>
                      )}
                      <Botao
                        disabled={ocupado !== null}
                        onClick={() =>
                          void acao('apurar', () =>
                            chamar(`/edicoes/${ed.id}/apurar`, { method: 'POST', body: JSON.stringify({ snapshot: snapshot || undefined }) }),
                            ed.tipo === 'ano' ? 'Apuração iniciada. O IDS do ano é recalculado do zero e pode levar alguns minutos.' : undefined,
                          )
                        }
                      >
                        {ed.status === 'apurada' ? 'Reapurar' : 'Apurar'}
                      </Botao>
                    </>
                  )}
                  {ed.status === 'apurando' && <p className="text-sm text-info">Apurando… a página atualiza sozinha.</p>}
                  {ed.status === 'apurada' && (
                    <Botao
                      disabled={ocupado !== null}
                      onClick={() => {
                        const aviso = ed.periodoEmCurso ? '\n\nATENÇÃO: o período ainda não terminou.' : ''
                        if (!confirm(`Aprovar o resultado de "${ed.titulo}"?\n\nO resultado fica travado e ${detalhe!.apuracao!.categorias.length} certificados são emitidos com código de verificação.${aviso}`)) return
                        void acao('aprovar', () => chamar(`/edicoes/${ed.id}/aprovar`, { method: 'POST' }),
                          'Resultado aprovado. Os PDFs estão sendo gerados; confira os e-mails antes de enviar.')
                      }}
                    >
                      Aprovar resultado
                    </Botao>
                  )}
                  {ed.status === 'aprovada' && (
                    <>
                      <Botao
                        disabled={ocupado !== null}
                        onClick={() => {
                          if (!confirm('Divulgar o resultado publicamente? A API pública passa a mostrar os vencedores desta edição.')) return
                          void acao('divulgar', () => chamar(`/edicoes/${ed.id}/divulgar`, { method: 'POST', body: JSON.stringify({ divulgar: true }) }))
                        }}
                      >
                        Divulgar resultado
                      </Botao>
                      <Botao
                        tom="perigo"
                        disabled={ocupado !== null || enviados > 0}
                        onClick={() => {
                          if (!confirm('Cancelar a aprovação? Os certificados emitidos são apagados (só é possível enquanto nenhum foi enviado).')) return
                          void acao('cancelar', () => chamar(`/edicoes/${ed.id}/cancelar-aprovacao`, { method: 'POST' }))
                        }}
                      >
                        Cancelar aprovação
                      </Botao>
                    </>
                  )}
                  {ed.status === 'divulgada' && (
                    <Botao
                      tom="secundario"
                      disabled={ocupado !== null}
                      onClick={() => void acao('recolher', () => chamar(`/edicoes/${ed.id}/divulgar`, { method: 'POST', body: JSON.stringify({ divulgar: false }) }))}
                    >
                      Recolher divulgação
                    </Botao>
                  )}
                </div>
                {log && <pre className="mt-4 whitespace-pre-wrap rounded-sm bg-panel border border-border px-3 py-2 text-xs text-ink">{log}</pre>}
              </section>

              {/* ── Resultado ────────────────────────────────────── */}
              {detalhe!.apuracao && (
                <section>
                  <div className="flex flex-wrap items-end justify-between gap-3 mb-3">
                    <div>
                      <p className="eyebrow">Resultado {ed.status === 'apurada' ? '(para revisão)' : ''}</p>
                      <p className="text-xs text-muted mt-1">
                        {detalhe!.apuracao.coorte.avaliados} avaliados · {detalhe!.apuracao.coorte.elegiveis} na coorte ·{' '}
                        {detalhe!.apuracao.coorte.concorrentes} concorrentes · {detalhe!.apuracao.categorias.length} categorias
                      </p>
                    </div>
                    <div className="flex gap-1">
                      {EIXOS.map((e) => (
                        <button
                          key={e.id}
                          onClick={() => setEixo(e.id)}
                          className={`px-3 py-1 text-xs border-b-2 ${eixo === e.id ? 'border-primary text-primary' : 'border-transparent text-muted hover:text-ink'}`}
                        >
                          {e.nome}
                        </button>
                      ))}
                    </div>
                  </div>
                  <div className="overflow-x-auto">
                    <table className="w-full text-sm border-collapse">
                      <thead>
                        <tr className="border-b border-border text-left text-xs uppercase tracking-wider text-muted">
                          <th className="py-2 pr-3 font-medium">Categoria</th>
                          <th className="py-2 pr-3 font-medium">Vencedor</th>
                          <th className="py-2 pr-3 font-medium">Nota</th>
                          <th className="py-2 pr-3 font-medium">Evidência</th>
                          <th className="py-2 pr-3 font-medium">2º e 3º</th>
                        </tr>
                      </thead>
                      <tbody>
                        {categoriasDoEixo.map((c) => (
                          <tr key={c.id} className="border-b border-border align-top">
                            <td className="py-2 pr-3">
                              <span className="text-ink">{c.titulo}</span>
                              <span className="block text-[11px] text-subtle">{c.elegiveis} concorrentes</span>
                            </td>
                            <td className="py-2 pr-3">
                              <span className="text-ink font-medium">{c.vencedor.nome}</span>
                              <span className="block text-[11px] text-muted">{c.vencedor.partido}-{c.vencedor.uf} · {c.vencedor.mesesAtivos} meses</span>
                            </td>
                            <td className="py-2 pr-3 tabular-nums">
                              {fmtNum(c.vencedor.valor)}
                              {c.houveDesempate && (
                                <span className="block text-[11px] text-warning" title="Empate na nota; decidido pelo indicador bruto da dimensão">desempate</span>
                              )}
                            </td>
                            <td className="py-2 pr-3 text-xs text-muted">
                              {fmtNum(c.vencedor.indicador.valor)} {c.vencedor.indicador.unidade}
                              <span className="block">{c.vencedor.indicador.detalhe}</span>
                            </td>
                            <td className="py-2 pr-3 text-xs text-muted">
                              {c.podio.slice(1).map((p) => (
                                <span key={p.senadorCod} className="block">
                                  {p.posicao}º {p.nome} ({p.partido}-{p.uf}) · {fmtNum(p.valor)}
                                </span>
                              ))}
                            </td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                </section>
              )}

              {/* ── Certificados e envio ─────────────────────────── */}
              {detalhe!.senadores.length > 0 && (
                <section className="space-y-4">
                  <div>
                    <p className="eyebrow">Certificados</p>
                    <p className="text-xs text-muted mt-1">
                      {totalCerts} certificados para {detalhe!.senadores.length} senadores · {enviados} enviados ·{' '}
                      {semEmail > 0 ? <span className="text-warning">{semEmail} sem e-mail</span> : 'todos com e-mail'}
                      {pdfsPendentes && ' · gerando PDFs…'}
                    </p>
                    <p className="text-xs text-subtle mt-1">
                      O e-mail vem preenchido com o endereço institucional publicado pelo Senado. Troque se quiser enviar para outro endereço do gabinete.
                    </p>
                  </div>

                  <div className="grid grid-cols-1 md:grid-cols-[1fr_280px] gap-4 rounded-sm border border-border bg-panel p-4">
                    <label className="text-xs text-muted">
                      Parágrafo adicional no e-mail (opcional)
                      <textarea
                        value={mensagem}
                        onChange={(e) => setMensagem(e.target.value)}
                        rows={3}
                        placeholder="Ex.: O resultado será divulgado publicamente em 1º de fevereiro de 2027."
                        className="mt-1 w-full rounded-sm border border-border bg-surface px-2 py-1.5 text-sm text-ink"
                      />
                    </label>
                    <div className="space-y-2">
                      <label className="block text-xs text-muted">
                        E-mail para testes
                        <input
                          type="email"
                          value={emailTeste}
                          onChange={(e) => setEmailTeste(e.target.value)}
                          placeholder="seu endereço"
                          className="mt-1 w-full rounded-sm border border-border bg-surface px-2 py-1.5 text-sm text-ink"
                        />
                      </label>
                      <Botao
                        disabled={ocupado !== null}
                        onClick={() => {
                          const pend = detalhe!.senadores.filter((s) => s.email && s.certificados.some((c) => !c.enviadoEm && !c.revogadoEm)).length
                          if (pend === 0) {
                            setLog('Nenhum senador com e-mail e certificado pendente.')
                            return
                          }
                          if (!confirm(`Enviar os certificados a ${pend} gabinetes que ainda não receberam?`)) return
                          void acao('enviar-todos', async () => {
                            const r = await chamar(`/edicoes/${ed.id}/enviar-pendentes`, { method: 'POST', body: JSON.stringify({ mensagem: mensagem.trim() || undefined }) })
                            setLog(`${r.enviados} enviados, ${r.falhas} falhas.${r.falhas ? '\n' + JSON.stringify(r.resultados.filter((x: { ok: boolean }) => !x.ok), null, 2) : ''}`)
                          })
                        }}
                      >
                        Enviar a todos os pendentes
                      </Botao>
                    </div>
                  </div>

                  <div className="space-y-3">
                    {detalhe!.senadores.map((s) => {
                      const algumEnviado = s.certificados.some((c) => c.enviadoEm)
                      const alterado = (emails[s.senadorCod] ?? '') !== (s.email ?? '')
                      return (
                        <div key={s.senadorCod} className="rounded-sm border border-border bg-surface p-4">
                          <div className="flex flex-wrap items-start justify-between gap-3">
                            <div>
                              <p className="text-ink font-medium">{s.tratamento} {s.nome}</p>
                              <p className="text-xs text-muted">{s.partido}-{s.uf} · {s.certificados.length} {s.certificados.length === 1 ? 'certificado' : 'certificados'}</p>
                            </div>
                            <div className="flex flex-wrap items-center gap-2">
                              <input
                                type="email"
                                value={emails[s.senadorCod] ?? ''}
                                onChange={(e) => setEmails({ ...emails, [s.senadorCod]: e.target.value })}
                                placeholder="e-mail do gabinete"
                                className="w-64 rounded-sm border border-border bg-surface px-2 py-1 text-sm"
                              />
                              {alterado && (
                                <Botao
                                  tom="secundario"
                                  disabled={ocupado !== null}
                                  onClick={() =>
                                    void acao(`email-${s.senadorCod}`, () =>
                                      chamar(`/edicoes/${ed.id}/email`, { method: 'POST', body: JSON.stringify({ senadorCod: s.senadorCod, email: emails[s.senadorCod] }) }),
                                      'E-mail salvo.',
                                    )
                                  }
                                >
                                  Salvar
                                </Botao>
                              )}
                              <Botao tom="secundario" onClick={() => void verPrevia(s)}>Prévia</Botao>
                              <Botao
                                tom="secundario"
                                disabled={ocupado !== null || !emailTeste.trim()}
                                onClick={() =>
                                  void acao(`teste-${s.senadorCod}`, () =>
                                    chamar(`/edicoes/${ed.id}/enviar`, {
                                      method: 'POST',
                                      body: JSON.stringify({ senadorCod: s.senadorCod, mensagem: mensagem.trim() || undefined, paraTeste: emailTeste.trim() }),
                                    }),
                                    `Teste enviado para ${emailTeste.trim()}.`,
                                  )
                                }
                              >
                                Teste
                              </Botao>
                              <Botao
                                disabled={ocupado !== null || !s.email || alterado}
                                onClick={() => {
                                  if (!confirm(`${algumEnviado ? 'Reenviar' : 'Enviar'} ${s.certificados.length} certificado(s) para ${s.email}?`)) return
                                  void acao(`enviar-${s.senadorCod}`, () =>
                                    chamar(`/edicoes/${ed.id}/enviar`, { method: 'POST', body: JSON.stringify({ senadorCod: s.senadorCod, mensagem: mensagem.trim() || undefined }) }),
                                    `Enviado para ${s.email}.`,
                                  )
                                }}
                              >
                                {algumEnviado ? 'Reenviar' : 'Enviar'}
                              </Botao>
                            </div>
                          </div>
                          {s.emailOficial && s.email !== s.emailOficial && (
                            <p className="mt-1 text-[11px] text-subtle">Endereço oficial no Senado: {s.emailOficial}</p>
                          )}
                          <ul className="mt-3 divide-y divide-border border-t border-border">
                            {s.certificados.map((c) => (
                              <li key={c.codigo} className="flex flex-wrap items-center justify-between gap-2 py-2 text-xs">
                                <span className="min-w-0">
                                  <span className="text-ink">{c.categoria}</span>
                                  <span className="ml-2 font-mono text-muted">{c.codigo}</span>
                                </span>
                                <span className="flex items-center gap-3">
                                  {c.revogadoEm ? (
                                    <span className="text-danger" title={c.motivoRevogacao ?? ''}>revogado em {fmtData(c.revogadoEm)}</span>
                                  ) : c.enviadoEm ? (
                                    <span className="text-success">enviado {fmtData(c.enviadoEm)}{c.envios > 1 ? ` (${c.envios}×)` : ''}</span>
                                  ) : c.envioErro ? (
                                    <span className="text-danger" title={c.envioErro}>falhou</span>
                                  ) : (
                                    <span className="text-subtle">não enviado</span>
                                  )}
                                  <button onClick={() => void abrirPdf(c.codigo)} className="text-primary underline">
                                    {c.pdfGerado ? 'PDF' : 'gerar PDF'}
                                  </button>
                                  <a href={`/premiacao/verificar/?codigo=${c.codigo}`} target="_blank" rel="noreferrer" className="text-primary underline">
                                    verificação
                                  </a>
                                  {!c.revogadoEm && (
                                    <button
                                      onClick={() => {
                                        const motivo = prompt(`Motivo da revogação de ${c.codigo} (aparece na verificação pública):`)
                                        if (!motivo?.trim()) return
                                        void acao(`revogar-${c.codigo}`, () =>
                                          chamar(`/certificados/${c.codigo}/revogar`, { method: 'POST', body: JSON.stringify({ motivo }) }),
                                          `Certificado ${c.codigo} revogado.`,
                                        )
                                      }}
                                      className="text-danger underline"
                                    >
                                      revogar
                                    </button>
                                  )}
                                </span>
                              </li>
                            ))}
                          </ul>
                        </div>
                      )
                    })}
                  </div>
                </section>
              )}
            </div>
          )}
        </main>
      </div>

      {previa && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={() => setPrevia(null)}>
          <div className="w-full max-w-3xl max-h-[90vh] flex flex-col rounded-sm bg-surface shadow-xl" onClick={(e) => e.stopPropagation()}>
            <div className="flex items-start justify-between gap-4 border-b border-border p-4">
              <div className="text-xs">
                <p><span className="text-subtle">Para:</span> {previa.para ?? '(sem e-mail)'}</p>
                <p className="mt-1"><span className="text-subtle">Assunto:</span> <span className="text-ink">{previa.assunto}</span></p>
                <p className="mt-1"><span className="text-subtle">Anexos:</span> {previa.anexos.join(', ')}</p>
              </div>
              <button onClick={() => setPrevia(null)} className="text-xs text-muted underline">fechar</button>
            </div>
            <iframe title="Prévia do e-mail" srcDoc={previa.html} className="flex-1 min-h-[60vh] w-full" sandbox="" />
          </div>
        </div>
      )}
    </div>
  )
}