Por Modelo — Emplacamento · Requisitos de Backend
Escopo deste documento: backend do relatório Por Modelo. Itens puramente de frontend/UX (sticky, chips, skeleton, badges, paginação visual, modais) só aparecem quando impõem uma regra de dado.
Status (atualizado): ambas as abas — Mês a Mês e Ano a Ano — estão implementadas com dados reais, filtros da barra vêm do banco, e o drawer de chassi por CNPJ funciona nas duas abas (com seletor de mês). Ver §6. Pendências em §7.
Contexto
- Origem dos dados:
dw_emplacamento_mart.emplacamento_kpi_dia(mart agregado, grão diário). Confirmado em 2026-07-29 por introspecção direta: o mart já é a versão consolidada descrita em marts-emplacamento.md (Mart 1), com todas as colunas — inclusivegrupomodeloveiculoemodelo(versão),tipo_pessoa,grupo_economico_*,grupo_empresa_*,segmento,subsegmento,combustivel,ano_fabricacao,is_capital,concessionaria_cadastrada. - Cobertura temporal (confirmada): 2025 inteiro (
2025-01-02→2025-12-31, ~1,68M linhas) + 2026 parcial (2026-01-02→2026-07-27, ~957k linhas). Ou seja, o comparativo YoY (ano vigente vs. ano anterior) funciona de verdade — não é gap. - Atualização: D-1. A âncora do relatório é
MAX(m.data)do mart. - Mapeamento hierárquico do Por Modelo (3 níveis):
- Marca =
m.fabricante - Modelo (família) =
m.grupomodeloveiculo - Versão (folha) =
m.modelo
- Marca =
O relatório Mês a Mês é um único conjunto server-side: KPIs + árvore de 3 níveis (volumes mensais + acumulados de ano) + participações, tudo derivado da mesma base filtrada.
1. Filtros (parâmetros de entrada)
Mesmo padrão do Por Marca (buildDwFilterClauses, alias m do mart), lidos de searchParams:
| Parâmetro (URL) | Campo ModeloFilters | Coluna do mart | Regra de backend |
|---|---|---|---|
intervalStart | intervalStart | m.data (âncora) | Âncora do período. Default = MAX(m.data) (D-1). Data final ≤ D-1. |
intervalEnd | intervalEnd | m.data | Limite superior opcional. |
montadora (CSV) | montadoras[] | m.fabricante | Específico do Por Modelo — aplicado localmente no modelo-service (não faz parte do buildDwFilterClauses). Default: todas. |
categorias (CSV) | categorias[] | m.segmento | "Categoria" mapeada para segmento (padrão herdado do Por Marca). Default: todas. |
modalidades (CSV) | modalidades[] | m.modalidade | Default: todas. |
area (CSV) | areaAbrangencia[] | m.municipio/estado/regiao_operacional/regiao_geografica/regiao_metropolitana/area_influencia_nome | Valores prefixados por tipo (cidade:, estado:, …). Default: Nacional (sem filtro). |
state | state | m.estado | Recorte por UF. |
group | group | m.conc_regiao_abracaf | Grupo ABRACAF. |
Regra de "mesmo período" (YoY)
Espelha o getMonthlyComparison do Por Marca:
WHERE m.ano = ANY([anoVigente, anoAnterior])
AND m.mes <= mesCorrente
AND (m.ano = anoVigente OR m.mes < mesCorrente OR EXTRACT(DAY FROM m.data) <= diaCorrente)
→ ano vigente conta todos os dias até mesCorrente; ano anterior conta meses cheios
anteriores + o mês corrente só até o mesmo dia (diaCorrente), para comparação justa.
2. Contrato de dados (saída) — ModeloMesMesData
Produzido pelo backend exatamente no shape que o frontend já consome
(_lib/types.ts), sem camada de tradução:
interface ModeloMesMesData {
rows: ModeloTreeRow[]; // árvore Marca > Modelo > Versão
meses: string[]; // ['jan','fev',...] até o mês corrente
anoVigente: number; // ex.: 2026
anoAnterior: number; // ex.: 2025
kpis: ModeloKpis;
lastUpdated: string | null; // MAX(m.data)
}
interface ModeloTreeRow {
label: string; // fabricante | grupomodeloveiculo | modelo
level: 'marca' | 'modelo' | 'versao';
children?: ModeloTreeRow[];
volumes: Record<string, number>; // chaves: 'jan'..'dez' (ano vigente) + 'AAAA' (vig) + 'AAAA' (ant)
percentuais: Record<string, number>; // chaves: 'AAAA' (vig) e 'AAAA' (ant)
}
Regras de agregação
- Grão do banco:
GROUP BY fabricante, grupomodeloveiculo, modelo, ano, mes. - Árvore: versões agregam no modelo (família); modelos agregam na marca.
volumes: por linha (em qualquer nível):- uma chave por mês do ano vigente (
'jan'…): volume daquele mês; [anoVigente]: acumulado do ano vigente (soma dos meses);[anoAnterior]: acumulado do ano anterior (mesmo período).
- uma chave por mês do ano vigente (
percentuais: participação relativa ao total global do respectivo ano (marcas somam 100%; modelos e versões são frações menores). Confere com o layout (marca 100% quando filtrada por 1 montadora; modelo/versão % do total).- Corte: só entram entidades com
acumAno (ano vigente) > 0. - Ordenação: desc por volume do ano vigente em cada nível.
KPIs (ModeloKpis)
- Modelo líder:
grupomodeloveiculo(família) com maior acumulado do ano vigente →nome+participacaoPct(share do total global). - Volume total: soma do ano vigente (todos os modelos).
- Δ% ano anterior:
(totalVigente − totalAnterior) / totalAnterior × 100.
3. Cache
cacheService.wrap(['relatorio-por-modelo','comparativo-mensal'], fn, { tags: [REPORT_TAGS.MODELO], revalidate: CACHE_TTL.HISTORICO }).- Nova tag
REPORT_TAGS.MODELO = 'relatorio-por-modelo'adicionada emlib/cache/report-cache-keys.ts. MAX(data)memoizado por request comreact cache()(mesmo padrão do Por Marca).
4. Validação com dados reais (2026-07-29, âncora 2026-07-27)
| Métrica | Valor |
|---|---|
| Total 2026 (acum. jan→jul) | 1.486.783 |
| Total 2025 (mesmo período) | 1.307.725 |
| Δ% YoY | +13,7% |
| Modelo líder | STRADA — 91.449 (6,2%) |
| Marcas / Modelos(família) / Versões | 105 / 609 / 2.868 |
(Sem filtro de montadora → todo o mercado. Com filtro FIAT, o layout de referência mostra ARGO 16,8% etc. — consistente com participação relativa ao total filtrado.)
4b. Contrato Ano a Ano — ModeloAnoAanoData
Mesmo shape hierárquico (ModeloTreeRow), com anos: number[] (crescente) e
anoVigente. Por ano selecionado (default = [vigente-1, vigente]):
volumes['AAAA']= total do ano (parcial no ano vigente, até D-1);volumes['AAAA_mp']= volume até o mesmo período (paridade de dia com o D-1) — emitido só para anos completos (no vigente, mesmo período == total);percentuais['AAAA']= participação sobre o total do ano (base decidida com o Hugo).
Coluna Δ (frontend): vigente − anterior(mesmo período), verde/vermelho conforme
sinal. deltaAnoAnteriorPct (KPI) usa a mesma base. Sem PF/PJ. Query em
getYearComparison (GROUP BY fabricante, grupomodeloveiculo, modelo, ano).
Validação (âncora 2026-07-27): 2025 total 2.549.118 / mesmo período 1.307.725; 2026 parcial 1.500.160; Δ +14,7%; líder STRADA.
4c. Drawer de chassi — getModeloChassiDetails
Server action que consulta dw_emplacamento_mart.emplacamento_detalhe_drawer por
intervalo de data (mês selecionado) + fabricante + grupomodeloveiculo +
modelo (versão) + filtros compartilhados, agrupando por CNPJ. Cada linha da árvore
carrega meta (fabricante/grupomodelo/versao) para o drawer filtrar direto.
- Disponível nas duas abas. Como o Ano a Ano não tem mês na tabela, o drawer tem um seletor de mês que abre no mais recente (D-1) e re-consulta ao trocar.
- Strings de
modelo(versão) batem exatamente entreemplacamento_kpi_diae a view de chassi — o filtro por versão casa (validado; inclui espaços em branco à direita).
5. Fora de escopo (backend) — referência
- Colunas PF/PJ (RF023-V): o mart tem
tipo_pessoa, mas nenhuma aba renderiza PF/PJ hoje e os shapes não carregamvolPF/volPJ. Capacidade disponível, não fiada (§7). - Render de sticky, chips, badges, skeleton, paginação, modal de exportação — frontend.
6. O que foi implementado nesta branch
| Arquivo | Mudança |
|---|---|
_lib/server/modelo-service.ts | Novo. getMonthlyComparison (Mês a Mês) + getYearComparison (Ano a Ano) → dados reais, árvore de 3 níveis, cache; carimba meta por linha. |
_lib/server/chassi-service.ts | Novo. getModeloChassiDetails (chassi por CNPJ, filtro por mês) + getModeloMonthOptions. |
_lib/types.ts | ModeloFilters +categorias/state/group/anos; ModeloTreeRow +meta; ModeloAnoAanoData +anoVigente. |
page.tsx | Parseia searchParams, chama os dois services + opções de filtro reais em paralelo, alimenta KPIs e passa ambos os datasets. |
_components/modelo-filter-bar.tsx | Recebe opções reais (categorias/modalidades/área) em vez dos mocks chumbados. |
_components/modelo-report-view.tsx | Header/contadores/export cientes da aba, sobre dados reais das duas abas. |
_components/modelo-mes-a-mes-table.tsx | Consome ModeloMesMesData real. |
_components/modelo-ano-a-ano-table.tsx | Colunas Total / Mesmo per. / % + Δ colorida; abre o drawer de chassi; sem PF/PJ. |
_components/modelo-drawer.tsx | Reformulado: seletor de mês + busca + lista de chassi por CNPJ (reusa BrandDrawerTable). |
lib/cache/report-cache-keys.ts | Nova tag REPORT_TAGS.MODELO. |
| removidos | mocked-data.ts, mocked-mes-mes-data.ts (dead code). |
pnpm --filter web typecheck ✅ · oxlint ✅ · validações contra o DW ✅.
7. Pendências para revisão (Hugo)
-
Filtros avançados no frontend. O backend aceita
?montadora=,?anos=e os filtros compartilhados, mas omodelo-filter-bar.tsxusa o bar genérico e não emitemontadoranemanos. Falta o select de Montadora (RF005) e o multi-select de Anos (RF010), além dos "Mais filtros" (Tipo de Pessoa, Grupo Econômico, Segmento, Combustível, Ano de Fabricação — todos existem no mart, nenhum fiado na URL). -
Seletor de mês no nível do relatório. As tabelas ainda ancoram sempre no
MAX(data)(o service já resolve qualquerintervalStart). Falta portar umBrandMonthFilter(parammes) se quiser navegar meses nas tabelas. (O drawer já tem seu próprio seletor de mês, conforme pedido.) -
Colunas PF/PJ. Decidir se as tabelas exibem Vol PF / Vol PJ quando "Tipo de Pessoa = Todas". Se sim: frontend renderiza + service popula
volPF/volPJ(pivot sobrem.tipo_pessoa). Hoje nenhum dos dois. -
"Categoria" =
segmento? Herdamos o mapeamento Categoria→m.segmentodo Por Marca. Confirmar (ver gap #2 de marts-emplacamento.md). -
Tamanho do payload. Sem filtro, a árvore tem ~2.868 versões (105 marcas). Estimativa < 2MB do cache, mas convém confirmar sob carga (ou lazy-load das versões ao expandir).
-
Ano a Ano. Continua 100% mockado. Quando entrar no escopo, o mesmo mart serve (multi-ano via
m.ano = ANY([...anosSelecionados]),GROUP BY ano).