feat: implementar relatórios de categorias e substituir seleção de período por picker visual

BREAKING CHANGE: Remove feature de seleção de período das preferências do usuário

  Alterações principais:

  - Adiciona sistema completo de relatórios por categoria
    - Cria página /relatorios/categorias com filtros e visualizações
    - Implementa tabela e gráfico de evolução mensal
    - Adiciona funcionalidade de exportação de dados
    - Cria skeleton otimizado para melhor UX de loading

  - Remove feature de seleção de período das preferências
    - Deleta lib/user-preferences/period.ts
    - Remove colunas periodMonthsBefore e periodMonthsAfter do schema
    - Remove todas as referências em 16+ arquivos
    - Atualiza database schema via Drizzle

  - Substitui Select de período por MonthPicker visual
    - Implementa componente PeriodPicker reutilizável
    - Integra shadcn MonthPicker customizado (português, Remix icons)
    - Substitui createMonthOptions em todos os formulários
    - Mantém formato "YYYY-MM" no banco de dados

  - Melhora design da tabela de relatórios
    - Mescla colunas Categoria e Tipo em uma única coluna
    - Substitui badge de tipo por dot colorido discreto
    - Reduz largura da tabela em ~120px
    - Atualiza skeleton para refletir nova estrutura

  - Melhorias gerais de UI
    - Reduz espaçamento entre títulos da sidebar (p-2 → px-2 py-1)
    - Adiciona MonthNavigation para navegação entre períodos
    - Otimiza loading states com skeletons detalhados
This commit is contained in:
Felipe Coutinho
2026-01-04 03:03:09 +00:00
parent d192f47bc7
commit 4237062bde
54 changed files with 2987 additions and 472 deletions

View File

@@ -12,7 +12,7 @@ import type {
LancamentoItem,
SelectOption,
} from "@/components/lancamentos/types";
import MonthPicker from "@/components/month-picker/month-picker";
import MonthNavigation from "@/components/month-picker/month-navigation";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { pagadores } from "@/db/schema";
import { getUserId } from "@/lib/auth/server";
@@ -30,9 +30,8 @@ import {
type SlugMaps,
type SluggedFilters,
} from "@/lib/lancamentos/page-helpers";
import { fetchUserPeriodPreferences } from "@/lib/user-preferences/period";
import { parsePeriodParam } from "@/lib/utils/period";
import { getPagadorAccess } from "@/lib/pagadores/access";
import { parsePeriodParam } from "@/lib/utils/period";
import {
fetchPagadorBoletoStats,
fetchPagadorCardUsage,
@@ -137,7 +136,6 @@ export default async function Page({ params, searchParams }: PageProps) {
boletoStats,
shareRows,
estabelecimentos,
periodPreferences,
] = await Promise.all([
fetchPagadorLancamentos(filters),
fetchPagadorMonthlyBreakdown({
@@ -162,7 +160,6 @@ export default async function Page({ params, searchParams }: PageProps) {
}),
sharesPromise,
getRecentEstablishmentsAction(),
fetchUserPeriodPreferences(dataOwnerId),
]);
const mappedLancamentos = mapLancamentosData(lancamentoRows);
@@ -183,7 +180,12 @@ export default async function Page({ params, searchParams }: PageProps) {
} else {
effectiveSluggedFilters = {
pagadorFiltersRaw: [
{ id: pagador.id, label: pagador.name, slug: pagador.id, role: pagador.role },
{
id: pagador.id,
label: pagador.name,
slug: pagador.id,
role: pagador.role,
},
],
categoriaFiltersRaw: [],
contaFiltersRaw: [],
@@ -240,7 +242,7 @@ export default async function Page({ params, searchParams }: PageProps) {
return (
<main className="flex flex-col gap-6">
<MonthPicker />
<MonthNavigation />
<Tabs defaultValue="profile" className="w-full">
<TabsList className="mb-2">
@@ -296,7 +298,6 @@ export default async function Page({ params, searchParams }: PageProps) {
contaCartaoFilterOptions={optionSets.contaCartaoFilterOptions}
selectedPeriod={selectedPeriod}
estabelecimentos={estabelecimentos}
periodPreferences={periodPreferences}
allowCreate={canEdit}
/>
</section>
@@ -306,8 +307,10 @@ export default async function Page({ params, searchParams }: PageProps) {
);
}
const normalizeOptionLabel = (value: string | null | undefined, fallback: string) =>
value?.trim().length ? value.trim() : fallback;
const normalizeOptionLabel = (
value: string | null | undefined,
fallback: string
) => (value?.trim().length ? value.trim() : fallback);
function buildReadOnlyOptionSets(
items: LancamentoItem[],