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:
| Tabela | Descrição |
|---|---|
dw_emplacamento.fato_emplacamentos | Fato central — um registro por emplacamento |
dw_emplacamento.dim_veiculo | Dimensão veículo (fabricante, modelo, segmento) |
dw_emplacamento.dim_local | Dimensão local (município, estado, região) |
dw_emplacamento.dim_concessionaria | Dimensão concessionária (razão social, grupo, área de influência) |
dw_emplacamento.dim_comprador | Dimensã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étodo | Retorno | Descrição |
|---|---|---|
getLastUpdated() | string | null | Data do registro mais recente no DW |
getReportData(tab, filters) | *PageData | Dados 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:
page.tsx(Server) lêsearchParamse extrai filtros tipados- Passa filtros para o serviço como
*Filters ReportPageHeader(Client) controlaReportFilterButton+ReportFilterModal- Mudanças de filtro atualizam URL via
router.pushcom novossearchParams
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
-
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 -
Implemente o serviço seguindo a interface dos serviços existentes:
getLastUpdated()— MAX(data_emplacamento) com filtro_ano_particaogetReportData(tab, filters)— switch exaustivo nas tabsgetFilterOptions(filters)— retornaFilterOption[][]
-
Extraia helpers puros para
*-utils.ts— qualquer função sem I/O é testável. -
Adicione ao menu em
reports/_lib/reports-navigation.config.ts. -
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:
| Formato | Implementação |
|---|---|
| CSV | Comma + 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.