Documentação

Montar a UI

Como transformar o payload do relatório em tela: quais campos governam cada card, o que exibir em cada estado e o que nunca deve aparecer.

O Shape do Payload diz o que cada campo é. Esta página diz o que desenhar com ele: para cada campo, o relatório renderizado ao vivo, as variações que aquele campo pode assumir, a resposta JSON correspondente e um prompt pronto para colar na sua ferramenta de IA.

O relatório abaixo é o nosso, o mesmo que o cliente final vê. Os dados são de exemplo, mas passam pelo mesmo cálculo de diagnóstico da produção, e o JSON ao lado sai da mesma função que responde GET /api/v1/reports/{reportId} — então nada aqui pode divergir do que a sua integração recebe.

Troque as variações e observe o que muda.

O modelo mental

A tela tem quatro camadas, de cima para baixo — e esta página segue exatamente essa ordem, para que você possa construir na sequência em que o leitor lê:

  1. O vereditodiagnosis.status resume tudo em uma palavra, com os contadores de alerta ao lado.
  2. O medidor de pontos — o número que decide se há processo de suspensão em curso.
  3. Os quatro indicadores — validade, categoria, exames e bloqueios, cada um com o seu próprio tom.
  4. A análise em texto e os detalhes — a leitura em linguagem natural, e depois cada seção aberta por cartão.

Três regras valem antes de qualquer pixel:

Só desenhe com o relatório pronto. status === "DELIVERED" e diagnosis !== null. É uma escolha conservadora de renderização, não uma afirmação sobre o payload: PROCESSING nem sempre vem vazio — ver estados de carregamento e erro.

Chave ausente não é null. Dentro de diagnosis, as seções points, fines, rollingPoints e autosuspensionRisk não têm chave quando não há dado — testar === null nunca acusa nada. No espelho normalized, a chave existe e vem null. Escolha um dos dois acessos e seja consistente.

Vazio não é zero. Um medidor em 0/20 desenhado sobre ausência de dado afirma algo falso.

Renderize o estado vazio quando não houver dado nenhum a mostrar: nem relatório completo, nem parcial, e o veredito não sendo "sem CNH" (que é uma resposta, não uma ausência).

De/para em uma tabela

Na ordem em que aparecem na tela.

OndeComponenteCampos que o alimentam
PainelCabeçalho do vereditodiagnosis.status, driver.name
PainelContadores de alertadiagnosis.allAlerts[].severity
PainelMedidor de pontosdiagnosis.points.*, diagnosis.fines.totalFines
PainelIndicador de validadediagnosis.validity.*
PainelIndicador de categoriadiagnosis.category.*
PainelIndicador de examesdiagnosis.medicalExams.*, diagnosis.toxicological.*
PainelIndicador de bloqueiosdiagnosis.blocks.*
AnáliseTexto em linguagem naturalaiAnalysis.markdown
DetalhesCartão de pontuaçãodiagnosis.points.*, diagnosis.fines.fines[]
DetalhesLinha do tempo da validadediagnosis.validity.expiryDate
DetalhesCalendário do toxicológicodiagnosis.toxicological.suggestedExamDates
DetalhesLista de bloqueios e suspensõesdiagnosis.blocks.*
DetalhesAlertas de riscodiagnosis.rollingPoints, diagnosis.autosuspensionRisk
DetalhesIdentificação no envelopedriver.*
DetalhesDados pessoais do titulardetails.personal.*
DetalhesHabilitação completadetails.license.*
DetalhesCursos e reciclagensdetails.courses.*
DetalhesHistórico completo de multasdetails.stateLookup.fines[]

Construindo com IA

Há dois prompts, e eles servem a momentos diferentes:

  • O prompt de instruções, no fim do explorador acima, carrega o contrato inteiro: fluxo, seções, valores possíveis, mapa de cores e um exemplo completo de resposta. Use-o para construir a interface do zero.
  • O prompt de cada componente, na aba Prompt para IA de qualquer demonstração desta página, é autocontido: repete o contrato mínimo e traz o recorte real daquele campo. Use-o para acrescentar uma peça a uma interface que já existe.

Os dois são agnósticos de stack — descrevem dados, estados e tons semânticos, e deixam o mapeamento para os tokens do seu design system.

Veredito

A conclusão em uma palavra. Vence sempre a condição mais severa: BLOCKED > EXPIRED > WARNING > VALID.

ValorTomRótulo que usamos
VALIDsucessoRegular
WARNINGatençãoAtenção
BLOCKEDperigoBloqueado
EXPIREDperigoBloqueado
UNKNOWNneutroPendente
NO_LICENSEneutroSem CNH

BLOCKED e EXPIRED compartilham tom e mensagem de propósito: nos dois casos a pessoa não pode dirigir. Mantenha ícones distintos, porque a causa e a solução são diferentes. NO_LICENSE é neutro, não é falha — "não há CNH" é o resultado da consulta.

Dois estados sem demonstração ao vivo

UNKNOWN e hasPartialData: true não têm variação nos demos desta página: os dois dependem de uma consulta que não respondeu, e o demo é construído a partir de respostas completas. Trate-os pela regra — UNKNOWN é estado vazio, hasPartialData mostra o que veio e sinaliza o que falta — e não por semelhança com algo que você viu aqui.

Alertas

allAlerts é a lista achatada de tudo o que as seções sinalizaram.

type é string, não um enum fechado: os valores em snake_case vêm das seções (cnh_expired, cnh_expiring_soon, medical_exam_expired, toxicological_exam_required, toxicological_exam_expired, federal_block, unresolved_block, state_suspension, state_revocation, fines_during_suspension, rolling_limit_exceeded, autosuspensive_no_trigger, no_reports_available, partial_data_available, no_license_found) e os em CAIXA ALTA vêm da pontuação (CRITICAL_POINTS, HIGH_POINTS, GRAVISSIMAS_DETECTED). A lista pode crescer sem que isso seja mudança breaking — trate o desconhecido com um default.

Para os contadores do cabeçalho: crítico conta critical e error; atenção conta warning.

Há dado para diagnosticar

hasReportData e hasPartialData são o portão do estado vazio. Com os dois falsos e o veredito diferente de NO_LICENSE, não há o que desenhar — mostre um estado vazio, não zeros.

hasPartialData verdadeiro significa que parte das consultas respondeu. Mostre o que veio e sinalize o que falta, em vez de apresentar um diagnóstico incompleto como se fosse completo.

Pontuação

A seção onde mais se erra.

O limite não é 20

Pela Lei 14.071/2020 o limite é 40 pontos (nenhuma infração gravíssima nos últimos 12 meses, ou condutor EAR), 30 (uma gravíssima) ou 20 (duas ou mais). O campo limit já chega calculado. Fixar 20 no código acusa "acima do limite" em condutor regular.

currentPoints é a soma da janela móvel de 12 meses, não o total histórico. E percentage satura em 100: 45/20 e 60/20 desenham a mesma barra cheia, então a legenda com os números crus é obrigatória, não decorativa.

riskLevelTomRótulo
lowsucessoBaixo Risco
mediumatençãoRisco Moderado
highatençãoAlto Risco
criticalperigoRisco Crítico

Leia limit e riskLevel do payload; não os recalcule. As funções ao lado existem para explicar por que os valores variam — úteis para escrever a legenda, não para substituir o campo.

Multas

severityBreakdown tem exatamente quatro chaves e alimenta o resumo; fines[] alimenta o detalhe. Não reconte a partir da lista.

severityTomRótulo
lightsucessoLeve
mediumatençãoMédia
seriousatençãoGrave
very_seriousperigoGravíssima

value é o valor em reais e pode ser nulo — formate como BRL só quando existir.

Dois campos que não existem em `fines[]`

Um guia de integração anterior mostrava ctbArticle e autosuspensive dentro de fines[]. Nenhum dos dois existe no contrato. O artigo do CTB e as infrações que suspendem sozinhas vivem em autosuspensionRisk.

Validade

validityStatusTomRótulo
validsucessoVálida
expiring_soonatençãoA vencer
expiredperigoVencida
unknownneutroIndefinida

expiring_soon é a janela de 30 dias antes do vencimento. unknown não é "válida": é ausência de resposta sobre a data — nunca pinte de verde.

Uma linha do tempo útil sai de um único campo, expiryDate, com três marcos: abertura da renovação (30 dias antes), o vencimento, e o limite legal (30 dias depois). Ordene por data — uma CNH já vencida reordena o trilho.

Categoria

A categoria é um fato, não um veredito: renderize sem cor de status e sem ponto colorido. Os outros três indicadores comunicam situação; este comunica um dado.

requiresToxicological decorre dela (verdadeiro para C, D e E) e é o que torna a seção toxicológica obrigatória — com ela verdadeira, um toxicológico ausente ou vencido leva o veredito a BLOCKED.

Exames clínicos

exams é opcional: teste a existência da chave, não o valor. Um exame vencido leva o veredito a WARNING, não a BLOCKED.

Exame toxicológico

A única seção clínica que bloqueia. Com isRequired verdadeiro, hasExam falso ou isExpired verdadeiro levam o veredito a BLOCKED.

suggestedExamDates é um calendário futuro, não um histórico: cada item traz date, isPast e daysFromNow. Marque o próximo e destaque o atrasado.

Bloqueios e suspensões

Aqui está a maior armadilha do payload.

Ausente e `false` são respostas diferentes

blocks.stateDeclaredBlock ausente significa que a consulta estadual não chegou a rodar. Só isBlocked: false é evidência de que a habilitação está regular no estado. Tratar os dois como iguais faz a sua interface afirmar algo que o dado não sustenta.

O segundo caso a suportar: isBlocked: true com detailedBlocks: 0. O estado acusa bloqueio sem detalhar nenhum, então o veredito é BLOCKED com a lista de bloqueios vazia. A tela precisa aguentar isso sem parecer quebrada.

federalBlocks, suspensions e revocations são arrays opcionais — a chave pode não existir. finesDuringSuspension agrava o processo e merece destaque separado das multas comuns.

Análise em linguagem natural

aiAnalysis.markdown é um texto em markdown pt-BR explicando o diagnóstico. É best-effort: pode vir nulo num relatório já entregue, sem que isso seja erro. Nunca bloqueie a tela esperando por ela.

Renderize como markdown, não como texto puro, e sanitize antes de injetar HTML.

Janela móvel de pontos

O campo que revela o risco que o número de hoje esconde: currentPoints pode estar baixo agora porque multas antigas saíram da janela, mesmo tendo havido um pico acima do limite. everExceeded merece destaque próprio, separado do medidor atual.

Repare que limitAtMoment varia ao longo da série — o limite muda conforme as gravíssimas entram e saem da janela de 12 meses.

Risco autossuspensivo

Infrações que suspendem a habilitação sozinhas, independentemente da pontuação.

atRisk verdadeiro com hasActiveBlock falso é o caso urgente: existe infração que gera suspensão direta e o processo ainda não foi instaurado — é a janela da defesa prévia, e a interface deve tratá-la como ação imediata, não como aviso.

Identificação do condutor

driver é o bloco de identidade do envelope: id, cpf, name, licenseNumber, licenseCategory, expiryDate e issueState — os sete campos que acompanham o relatório desde a criação. O registro completo do titular vive em details: filiação, documento de identidade e naturalidade em details.personal, cursos em details.courses. Veja onde está cada coisa.

Num relatório NO_LICENSE, name é o nome que você enviou, não um nome confirmado em base oficial. Um nome preenchido ao lado de "sem CNH" diz que aquele CPF não tem habilitação emitida; não diz que a pessoa foi identificada.

O espelho normalized

Os mesmos dados num formato de acesso diferente: aqui a chave sempre existe e a ausência é null. Ele não espelha tudo — rollingPoints e autosuspensionRisk só existem em diagnosis.

Estados de carregamento e erro

statusO que mostrar
PROCESSINGDois casos diferentes atrás do mesmo valor — ver abaixo.
DELIVEREDO diagnóstico completo.
ERRORFalha após todas as tentativas; o crédito foi estornado.

`PROCESSING` não significa payload vazio

O estado interno de entrega parcial — a consulta federal respondeu, a estadual não — também reporta status: "PROCESSING", e é o corpo do webhook report.partially_delivered. Nele diagnosis e details já vêm preenchidos com o que foi apurado.

Descartar o payload com if (status !== "DELIVERED") return; joga fora um relatório que tem dado. Teste diagnosis !== null / details !== null para saber se há o que ler, e o status para saber se ainda vem mais.

Datas: duas classes, e confundir custa um dia

ClasseCamposLeia em
CalendárioexpiryDate, datas de multa, de exame e de bloqueioUTC
InstantecreatedAt, deliveredAt, diagnosisDate, generatedAtfuso de São Paulo

Uma data de calendário chega em UTC à meia-noite. Formatá-la no fuso local — e o Brasil é UTC−3 — recua o dia: uma CNH que vence em 1º de janeiro aparece vencendo em 31 de dezembro.

Nunca use formatação que dependa do fuso do ambiente. Passe o fuso explicitamente: UTC para datas de calendário, America/Sao_Paulo para instantes.

Tons: um mapa só

Não invente um segundo mapa de cores. Cinco dimensões, os mesmos cinco tons.

Veredito, validade, risco de pontos, gravidade de multa e severidade de alerta são as cinco dimensões com cor. Mapeie cada uma para os tokens do seu próprio design system e nunca comunique estado apenas por cor — acompanhe de rótulo ou ícone.

Onde está cada coisa

Desde 2026-08-06 o payload tem paridade de campos com o relatório. O que faltava vive em details, ao lado de diagnosis:

O que você quer desenharOnde está
Veredito, indicadores, pontuação da janela de 12 mesesdiagnosis
Dados pessoais — filiação, documento de identidade, naturalidadedetails.personal
Habilitação completa — RENACH, 1ª habilitação, local de emissão, situação, EARdetails.license
Exames com categoria permitida, cidade e UFdetails.medicalExams
Toxicológico com data de uso e situaçãodetails.toxicological
Cursos e reciclagensdetails.courses
Histórico completo de multas, com status de pagamento, e o saldo oficial de pontosdetails.stateLookup

Três coisas que vale saber antes de escolher de onde ler:

  • details acompanha a existência do dado, não o status, como diagnosis — ver estados de carregamento e erro.
  • details.stateLookup é null quando a consulta estadual não rodou — a mesma distinção de stateDeclaredBlock: ausência não é "sem multas".
  • Duas listas de multas, propósitos diferentes. diagnosis.fines é a janela móvel de 12 meses, que decide a pontuação pela Lei 14.071. details.stateLookup.fines é o histórico inteiro. Não use a segunda para calcular pontos.

O demo acima continua alimentado apenas pelo que a API expõe — a diferença é que agora isso é tudo. Ver Shape do Payload para os tipos de cada seção de details, e o Changelog para a entrada dessa mudança.

O que continua fora, e por quê

A garantia anti-leak não mudou: payload bruto do provedor, custo, identificador de requisição, nome do provedor, URLs internas e contadores de pipeline nunca saem. Também ficam fora o código numérico de status de cada multa (o status já vem parseado) e a origem interna do cálculo de pontos — detalhes da nossa extração, não do seu relatório.

Ver também