Pular para o conteúdo principal

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 — inclusive grupomodeloveiculo e modelo (versão), tipo_pessoa, grupo_economico_*, grupo_empresa_*, segmento, subsegmento, combustivel, ano_fabricacao, is_capital, concessionaria_cadastrada.
  • Cobertura temporal (confirmada): 2025 inteiro (2025-01-022025-12-31, ~1,68M linhas) + 2026 parcial (2026-01-022026-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

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 ModeloFiltersColuna do martRegra de backend
intervalStartintervalStartm.data (âncora)Âncora do período. Default = MAX(m.data) (D-1). Data final ≤ D-1.
intervalEndintervalEndm.dataLimite superior opcional.
montadora (CSV)montadoras[]m.fabricanteEspecí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.modalidadeDefault: todas.
area (CSV)areaAbrangencia[]m.municipio/estado/regiao_operacional/regiao_geografica/regiao_metropolitana/area_influencia_nomeValores prefixados por tipo (cidade:, estado:, …). Default: Nacional (sem filtro).
statestatem.estadoRecorte por UF.
groupgroupm.conc_regiao_abracafGrupo 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).
  • 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 em lib/cache/report-cache-keys.ts.
  • MAX(data) memoizado por request com react cache() (mesmo padrão do Por Marca).

4. Validação com dados reais (2026-07-29, âncora 2026-07-27)

MétricaValor
Total 2026 (acum. jan→jul)1.486.783
Total 2025 (mesmo período)1.307.725
Δ% YoY+13,7%
Modelo líderSTRADA — 91.449 (6,2%)
Marcas / Modelos(família) / Versões105 / 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 entre emplacamento_kpi_dia e 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 carregam volPF/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

ArquivoMudança
_lib/server/modelo-service.tsNovo. 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.tsNovo. getModeloChassiDetails (chassi por CNPJ, filtro por mês) + getModeloMonthOptions.
_lib/types.tsModeloFilters +categorias/state/group/anos; ModeloTreeRow +meta; ModeloAnoAanoData +anoVigente.
page.tsxParseia searchParams, chama os dois services + opções de filtro reais em paralelo, alimenta KPIs e passa ambos os datasets.
_components/modelo-filter-bar.tsxRecebe opções reais (categorias/modalidades/área) em vez dos mocks chumbados.
_components/modelo-report-view.tsxHeader/contadores/export cientes da aba, sobre dados reais das duas abas.
_components/modelo-mes-a-mes-table.tsxConsome ModeloMesMesData real.
_components/modelo-ano-a-ano-table.tsxColunas Total / Mesmo per. / % + Δ colorida; abre o drawer de chassi; sem PF/PJ.
_components/modelo-drawer.tsxReformulado: seletor de mês + busca + lista de chassi por CNPJ (reusa BrandDrawerTable).
lib/cache/report-cache-keys.tsNova tag REPORT_TAGS.MODELO.
removidosmocked-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)

  1. Filtros avançados no frontend. O backend aceita ?montadora=, ?anos= e os filtros compartilhados, mas o modelo-filter-bar.tsx usa o bar genérico e não emite montadora nem anos. 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).

  2. Seletor de mês no nível do relatório. As tabelas ainda ancoram sempre no MAX(data) (o service já resolve qualquer intervalStart). Falta portar um BrandMonthFilter (param mes) se quiser navegar meses nas tabelas. (O drawer já tem seu próprio seletor de mês, conforme pedido.)

  3. 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 sobre m.tipo_pessoa). Hoje nenhum dos dois.

  4. "Categoria" = segmento? Herdamos o mapeamento Categoria→m.segmento do Por Marca. Confirmar (ver gap #2 de marts-emplacamento.md).

  5. 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).

  6. Ano a Ano. Continua 100% mockado. Quando entrar no escopo, o mesmo mart serve (multi-ano via m.ano = ANY([...anosSelecionados]), GROUP BY ano).