Pular para o conteúdo principal

Módulo de Relatórios — Guia de Arquitetura

Documenta a estrutura, padrões de query e convenções do módulo de relatórios de emplacamento (apps/web/app/[locale]/home/[account]/reports/).


Estrutura de pastas

reports/
├── _components/
│ └── report-filter/ # Sistema de filtros reutilizável
│ ├── report-filter-button.tsx
│ ├── report-filter-chips.tsx
│ ├── report-filter-modal.tsx
│ ├── report-filter.utils.ts
│ ├── use-report-filters.ts
│ └── types.ts
├── _lib/
│ └── reports-navigation.config.ts # Configuração do menu lateral

├── daily-report/ # Boletim diário (email)
├── por-marca/ # RF007 — Relatório por marca
│ ├── _components/
│ ├── _lib/
│ │ ├── types.ts
│ │ └── server/brand-service.ts
│ └── page.tsx
├── por-territorio/ # RF005/RF006 — Relatório por território
│ ├── _components/
│ ├── _lib/
│ │ ├── types.ts
│ │ └── server/
│ │ ├── territory-service.ts
│ │ └── supabase-dw-client.ts
│ └── page.tsx
└── por-ranking/ # RF008 — Relatório por ranking (4 tabs)
├── _components/
├── _lib/
│ ├── types.ts
│ ├── ranking-utils.ts # Helpers puros (testáveis)
│ ├── ranking-page-utils.ts
│ ├── __tests__/
│ └── server/ranking-service.ts
└── page.tsx

Conexão com o Data Warehouse

Todos os relatórios consultam o schema dw_emplacamento via cliente Supabase com tagged template literals (postgres):

// apps/web/app/[locale]/home/[account]/reports/por-territorio/_lib/server/supabase-dw-client.ts
import { createClient } from '@supabase/supabase-js';
// Retorna um postgres-tagged-template client apontado para o DW
export function getSupabaseDwClient() { ... }

Tabelas principais:

TabelaDescrição
dw_emplacamento.fato_emplacamentosFato central — um registro por emplacamento
dw_emplacamento.dim_veiculoDimensão veículo (fabricante, modelo, segmento)
dw_emplacamento.dim_localDimensão local (município, estado, região)
dw_emplacamento.dim_concessionariaDimensão concessionária (razão social, grupo, área de influência)
dw_emplacamento.dim_compradorDimensão comprador (tipo pessoa: física/jurídica)

Particionamento: a coluna _ano_particao é obrigatória em todas as queries de varredura ampla. Sempre inclua um filtro WHERE f._ano_particao IN (ano_atual, ano_atual - 1) para evitar full scan.


Padrão dos serviços

Cada relatório tem um serviço do lado do servidor com o padrão factory:

// Uso em page.tsx (Server Component)
import { createRankingService } from './_lib/server/ranking-service';

const service = createRankingService();
const data = await service.getReportData(tab, filters);

O serviço expõe três métodos públicos padrão:

MétodoRetornoDescrição
getLastUpdated()string | nullData do registro mais recente no DW
getReportData(tab, filters)*PageDataDados da aba ativa com card e lastUpdated
getFilterOptions(filters)FilterOption[][]Opções dinâmicas para os selects de filtro

Convenção de datas

A maioria dos relatórios resolve os parâmetros de data a partir de filters.intervalStart (ou lastUpdated como fallback) via resolveDateParams() de ranking-utils.ts:

const dp = resolveDateParams(refDate);
// dp.ano, dp.mes, dp.currentDay, dp.prevYear, dp.prevMonth, dp.anoAnt

Sistema de filtros

Os filtros são gerenciados pelo hook useReportFilters e persistidos em searchParams:

// Campos disponíveis (FilterFieldKey)
type FilterFieldKey = 'intervalStart' | 'state' | 'group' | 'personType';

Fluxo:

  1. page.tsx (Server) lê searchParams e extrai filtros tipados
  2. Passa filtros para o serviço como *Filters
  3. ReportPageHeader (Client) controla ReportFilterButton + ReportFilterModal
  4. Mudanças de filtro atualizam URL via router.push com novos searchParams

Opções de filtro são carregadas dinamicamente por getFilterOptions() — o serviço retorna apenas os valores presentes nos dados, evitando opções sem resultado.


Padrão dos componentes

Cada relatório segue a separação Server/Client:

page.tsx (Server Component)
├── <ReportPageHeader> (Client) — filtros, última atualização
└── <ReportCard> (Server ou Client)
├── <PivotTableCard> — tabela pivô com grupos de colunas
├── <StandardTableCard> — tabela padrão com export CSV
└── <RankingBarChart> (Client) — gráfico de barras horizontal

Regra: componentes com toLocaleString, usePathname, useSearchParams, ou qualquer API de browser devem ter 'use client' no topo.


Adicionando um novo relatório

  1. Crie a pasta reports/por-<nome>/ com a estrutura padrão:

    por-<nome>/
    ├── _components/
    ├── _lib/
    │ ├── types.ts # *Filters, *PageData, *Tab, TABS const
    │ ├── *-utils.ts # helpers puros exportados (testáveis)
    │ └── server/
    │ └── *-service.ts # createService() factory + class
    └── page.tsx
  2. Implemente o serviço seguindo a interface dos serviços existentes:

    • getLastUpdated() — MAX(data_emplacamento) com filtro _ano_particao
    • getReportData(tab, filters) — switch exaustivo nas tabs
    • getFilterOptions(filters) — retorna FilterOption[][]
  3. Extraia helpers puros para *-utils.ts — qualquer função sem I/O é testável.

  4. Adicione ao menu em reports/_lib/reports-navigation.config.ts.

  5. Crie testes em _lib/__tests__/ cobrindo os helpers puros (sem mock de DB).


Queries paralelas

Quando duas queries são independentes dentro do mesmo método, use Promise.all:

// Correto — ambas as queries rodam em paralelo
const [last5Rows, aggRows] = await Promise.all([
dw<LastDatesRow[]>`SELECT ... LIMIT 5`,
dw<BrandAggRow[]>`SELECT ... ORDER BY acum_ano DESC`,
]);

Evite await sequencial quando as queries não dependem uma da outra — cada query serial acrescenta latência desnecessária ao response time da página.


Export de tabelas

StandardTableCard oferece dois formatos de export:

FormatoImplementação
CSVComma + aspas duplas + text/csv
Excel (CSV)Semicolon + BOM UTF-8 + .csv — Excel Windows abre sem configuração

O formato XLSX real (via biblioteca como xlsx ou sheetjs) pode ser adicionado futuramente se necessário.