Pular para o conteúdo principal

Sistema de Filtros de Relatório — Vona

O sistema de filtros de relatório do Vona sincroniza o estado dos filtros com a URL via search params, exibindo um modal de edição e chips de ação rápida. Todos os componentes são exportados de um único módulo em _components/report-filter/.

Configurar o useReportFilters

O hook useReportFilters lê e escreve os filtros ativos como search params da URL. Recebe um ReportFilterConfig e retorna o estado e as funções de controle.

import { useReportFilters } from '../_components/report-filter';

const filterConfig: ReportFilterConfig = {
fields: ['intervalStart', 'intervalEnd', 'state', 'group'],
stateOptions: [
{ value: 'SP', label: 'São Paulo' },
{ value: 'RJ', label: 'Rio de Janeiro' },
],
};

const {
activeFilters, // { intervalStart?: string, state?: string, ... }
activeCount, // número de filtros ativos
applyFilters, // (filters: ActiveFilters) => void
removeFilter, // (key: FilterFieldKey) => void
clearFilters, // () => void
getChips, // () => FilterChip[]
} = useReportFilters(filterConfig);

Os filtros são persistidos em search params: ?intervalStart=2025-01-01&state=SP. Chamar applyFilters ou removeFilter navega para a nova URL via router.push.

Adicionar o modal e os chips

import { useState } from 'react';
import {
ReportFilterButton,
ReportFilterChips,
ReportFilterModal,
useReportFilters,
} from '../_components/report-filter';

export function ReportHeader() {
const [modalOpen, setModalOpen] = useState(false);
const { activeFilters, activeCount, applyFilters, removeFilter, clearFilters, getChips } =
useReportFilters(filterConfig);

return (
<>
<ReportFilterButton
activeCount={activeCount}
onClick={() => setModalOpen(true)}
/>

<ReportFilterChips
chips={getChips()}
onRemove={removeFilter}
onClearAll={clearFilters}
/>

<ReportFilterModal
open={modalOpen}
onClose={() => setModalOpen(false)}
config={filterConfig}
initialValues={activeFilters}
onConfirm={(filters) => {
applyFilters(filters);
setModalOpen(false);
}}
/>
</>
);
}

Definir o ReportFilterConfig

ReportFilterConfig controla quais campos aparecem no modal e quais opções estão disponíveis em cada select.

interface ReportFilterConfig {
/** Quais campos exibir no modal. */
fields: FilterFieldKey[];
saleModalityOptions?: FilterOption[];
acronymOptions?: FilterOption[];
stateOptions?: FilterOption[];
groupOptions?: FilterOption[];
dealershipOptions?: FilterOption[];
}

Campos disponíveis (FilterFieldKey):

CampoLabel exibidoComponente no modal
intervalStartDeDateRangePicker (compartilhado com intervalEnd)
intervalEndAtéDateRangePicker (compartilhado com intervalStart)
saleModalityModalidade de vendaNativeSelect
acronymsSiglasNativeSelect
stateEstadoSelect (shadcn)
groupGrupoSelect (shadcn)
dealershipsConcessionáriasNativeSelect
personTypeTipo de pessoaNativeSelect (Física / Jurídica / Ambos)
Campos obrigatórios vs. opcionais

Inclua em fields apenas os campos que fazem sentido para o relatório. Um campo em fields que não tenha as options correspondentes simplesmente não aparece no modal (stateOptions, groupOptions, etc. só são renderizados se a array existir).

DateRangePicker

DateRangePicker é o componente de seleção de intervalo de datas usado pelos campos intervalStart e intervalEnd. Pode ser usado de forma independente.

import { DateRangePicker, type DateRange } from '../_components/report-filter/date-range-picker';

const [range, setRange] = useState<DateRange | undefined>();

<DateRangePicker
value={range}
onChange={setRange}
data-test="meu-date-picker"
/>

Props:

PropTipoObrigatórioDescrição
valueDateRange | undefinedNãoIntervalo selecionado ({ from: Date, to?: Date })
onChange(range: DateRange | undefined) => voidNãoCallback chamado ao confirmar
classNamestringNãoClasse CSS adicional no botão trigger
data-teststringNãoAtributo de teste (padrão: "date-range-picker")

O picker abre um Popover com calendário de dois meses em pt-BR. A seleção só é confirmada ao clicar em Confirmar — fechar o Popover descarta a seleção pendente.

Chips de filtro ativo

ReportFilterChips exibe os filtros ativos como tags removíveis. Retorna null quando não há filtros ativos.

<ReportFilterChips
chips={getChips()} // FilterChip[]
onRemove={removeFilter}
onClearAll={clearFilters}
/>

Os chips usam os labels das options quando disponíveis (ex: exibe "São Paulo" em vez de "SP"). Para personType, os labels são Física, Jurídica e Ambos.

Exportações do módulo

// Componentes
export { ReportFilterButton } from './report-filter-button';
export { ReportFilterChips } from './report-filter-chips';
export { ReportFilterModal } from './report-filter-modal';

// Hook
export { useReportFilters } from './use-report-filters';

// Utils
export { ALL_FILTER_KEYS, FILTER_LABELS, PERSON_TYPE_LABELS, getChipsFromFilters } from './report-filter.utils';

// Types
export type { ActiveFilters, FilterChip, FilterFieldKey, FilterOption, ReportFilterConfig } from './types';

Adicionando opções de filtro vindas do servidor

As opções dos selects (stateOptions, groupOptions, etc.) geralmente vêm de uma query ao DW via service. O padrão nas páginas existentes é buscar as opções no Server Component e passá-las para o componente de cabeçalho:

// page.tsx (Server Component)
const { stateOptions, groupOptions } = await createRankingService().getFilterOptions();

return <RankingHeader stateOptions={stateOptions} groupOptions={groupOptions} />;

// RankingHeader (Client Component)
const filterConfig: ReportFilterConfig = {
fields: ['intervalStart', 'intervalEnd', 'state', 'group', 'personType'],
stateOptions,
groupOptions,
};