Iniciativa Cidadã Independente · Transparência Parlamentar
TSX190 linhas8.699 bytes

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

SHA-256

0f708ee32d25c8b5160ddcff84b0afb8993fb655ea8ebc78ecf2f79ddf7edf85

Somente leiturafonte-0531042d9f3e
'use client'

import { Suspense, useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import Link from 'next/link'

const API = process.env.NEXT_PUBLIC_API_URL ?? 'https://api.observasenado.org'

interface Verificacao {
  codigo: string
  situacao: 'valido' | 'revogado' | 'adulterado'
  motivoRevogacao?: string
  revogadoEm?: string
  emitidoEm: string
  edicao: { metodologiaVersao?: string; titulo: string; subtitulo: string; periodo: { inicio: string; fim: string }; apuracaoSha256: string; divulgada: boolean }
  categoria: { titulo: string; descricao: string; ambito: string; concorrentes: number }
  senador: { nome: string; tratamento: string; partido: string; uf: string }
  idsTotal: number
  indicador: { rotulo: string; valor: number; unidade: string; detalhe: string }
  pdfSha256: string | null
}

function data(iso: string): string {
  return new Date(`${iso.slice(0, 10)}T12:00:00`).toLocaleDateString('pt-BR', { day: 'numeric', month: 'long', year: 'numeric' })
}

const SITUACAO = {
  valido: { rotulo: 'Certificado autêntico', cor: 'border-success bg-green-50 text-success' },
  revogado: { rotulo: 'Certificado revogado', cor: 'border-danger bg-red-50 text-danger' },
  adulterado: { rotulo: 'Registro com integridade comprometida', cor: 'border-danger bg-red-50 text-danger' },
} as const

function Verificador() {
  const params = useSearchParams()
  const router = useRouter()
  const codigoUrl = params.get('codigo') ?? ''
  const [codigo, setCodigo] = useState(codigoUrl)
  const [estado, setEstado] = useState<'ocioso' | 'buscando' | 'achou' | 'nao_achou' | 'erro'>('ocioso')
  const [v, setV] = useState<Verificacao | null>(null)
  const [erro, setErro] = useState('')

  useEffect(() => {
    setCodigo(codigoUrl)
    if (!codigoUrl) return
    setEstado('buscando')
    fetch(`${API}/api/premiacao/verificar/${encodeURIComponent(codigoUrl.trim())}`)
      .then(async (r) => {
        if (r.status === 404) return setEstado('nao_achou')
        const body = await r.json()
        if (!r.ok) {
          setErro(body.error ?? `HTTP ${r.status}`)
          return setEstado('erro')
        }
        setV(body)
        setEstado('achou')
      })
      .catch(() => {
        setErro('não foi possível consultar agora')
        setEstado('erro')
      })
  }, [codigoUrl])

  return (
    <div className="max-w-2xl mx-auto py-10">
      <p className="eyebrow">Prêmio Observatório do Senado</p>
      <h1 className="font-serif text-3xl text-ink mt-2">Verificação de certificado</h1>
      <p className="mt-3 text-sm text-muted leading-relaxed">
        Todo certificado do prêmio traz um código no formato <span className="font-mono">OS-L57-XXXX-XXXX</span> e um QR code.
        Digite o código para conferir se o documento foi de fato emitido pelo Observatório.
      </p>

      <form
        className="mt-6 flex gap-2"
        onSubmit={(e) => {
          e.preventDefault()
          if (codigo.trim()) router.replace(`/premiacao/verificar/?codigo=${encodeURIComponent(codigo.trim().toUpperCase())}`)
        }}
      >
        <input
          value={codigo}
          onChange={(e) => setCodigo(e.target.value)}
          placeholder="OS-L57-XXXX-XXXX"
          className="flex-1 rounded-sm border border-border bg-surface px-3 py-2 font-mono text-sm uppercase tracking-wider focus:border-primary focus:outline-none"
          aria-label="Código do certificado"
        />
        <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">
          Verificar
        </button>
      </form>

      <div className="mt-8" aria-live="polite">
        {estado === 'buscando' && <p className="text-sm text-muted">Consultando…</p>}
        {estado === 'nao_achou' && (
          <div className="rounded-sm border border-danger bg-red-50 p-4 text-sm text-danger">
            Nenhum certificado com o código <span className="font-mono">{codigoUrl.toUpperCase()}</span>. Confira se foi digitado corretamente.
            Um documento com código inexistente não foi emitido pelo Observatório do Senado.
          </div>
        )}
        {estado === 'erro' && <p className="text-sm text-danger">Erro na consulta: {erro}.</p>}

        {estado === 'achou' && v && (
          <div className="space-y-5">
            <div className={`rounded-sm border-l-4 p-4 ${SITUACAO[v.situacao].cor}`}>
              <p className="font-semibold">{SITUACAO[v.situacao].rotulo}</p>
              {v.situacao === 'revogado' && (
                <p className="mt-1 text-sm">
                  Revogado em {data(v.revogadoEm!)}. Motivo: {v.motivoRevogacao}
                </p>
              )}
              {v.situacao === 'adulterado' && (
                <p className="mt-1 text-sm">O registro deste código não confere com o selo de integridade. Não considere este certificado.</p>
              )}
            </div>

            <div className="card p-6">
              <p className="text-xs uppercase tracking-institutional text-muted">Conferido a</p>
              <p className="font-serif text-2xl text-ink mt-1">{v.senador.tratamento} {v.senador.nome}</p>
              <p className="text-sm text-muted">{v.senador.partido} · {v.senador.uf}</p>

              <p className="mt-5 text-xs uppercase tracking-institutional text-muted">Primeiro lugar na categoria</p>
              <p className="font-serif text-xl text-accent-hover mt-1">{v.categoria.titulo}</p>
              <p className="text-sm text-muted mt-1">{v.categoria.descricao}</p>

              <dl className="mt-5 grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
                <div>
                  <dt className="text-xs text-subtle">Edição</dt>
                  <dd className="text-ink">{v.edicao.titulo}</dd>
                  <dd className="text-xs text-muted">{v.edicao.subtitulo}</dd>
                </div>
                <div>
                  <dt className="text-xs text-subtle">Emitido em</dt>
                  <dd className="text-ink">{data(v.emitidoEm)}</dd>
                </div>
                <div>
                  <dt className="text-xs text-subtle">Concorrentes na categoria</dt>
                  <dd className="text-ink">{v.categoria.concorrentes}</dd>
                </div>
                <div>
                  <dt className="text-xs text-subtle">{v.indicador.rotulo}</dt>
                  <dd className="text-ink">
                    {v.indicador.valor.toLocaleString('pt-BR')} {v.indicador.unidade}
                  </dd>
                  <dd className="text-xs text-muted">{v.indicador.detalhe}</dd>
                </div>
              </dl>

              <div className="mt-6 border-t border-border pt-4 space-y-2 text-xs text-muted">
                <p className="font-mono break-all">
                  <span className="font-sans text-subtle">Código: </span>{v.codigo}
                </p>
                {v.pdfSha256 && (
                  <p className="font-mono break-all">
                    <span className="font-sans text-subtle">SHA-256 do PDF original: </span>{v.pdfSha256}
                  </p>
                )}
                <p className="font-mono break-all">
                  <span className="font-sans text-subtle">SHA-256 do resultado da edição: </span>{v.edicao.apuracaoSha256}
                </p>
                {v.pdfSha256 && (
                  <p>
                    <a href={`${API}/api/premiacao/certificado/${v.codigo}/pdf`} className="text-primary underline">
                      Baixar o PDF original
                    </a>{' '}
                    para comparar com a cópia recebida.
                  </p>
                )}
              </div>
            </div>

            <p className="text-xs text-muted leading-relaxed">
              O prêmio não tem inscrição, indicação nem votação: o resultado decorre dos dados abertos do Senado Federal aplicados ao
              Índice de Desempenho Senatorial{v.edicao.metodologiaVersao ? ` (${v.edicao.metodologiaVersao})` : ''}. Veja a <Link href="/metodologia/" className="text-primary underline">metodologia</Link> e
              as <Link href="/premiacao/" className="text-primary underline">categorias do prêmio</Link>.
              {!v.edicao.divulgada && ' O resultado completo desta edição ainda não foi divulgado.'}
            </p>
          </div>
        )}
      </div>
    </div>
  )
}

export default function VerificarPage() {
  return (
    <Suspense fallback={<div className="max-w-2xl mx-auto py-10 text-sm text-muted">Carregando…</div>}>
      <Verificador />
    </Suspense>
  )
}