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

@@ -32,11 +32,10 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { PeriodPicker } from "@/components/period-picker";
import { useControlledState } from "@/hooks/use-controlled-state";
import { useFormState } from "@/hooks/use-form-state";
import type { EligibleInstallment } from "@/lib/installments/anticipation-types";
import type { PeriodPreferences } from "@/lib/user-preferences/period";
import { createMonthOptions } from "@/lib/utils/period";
import { RiLoader4Line } from "@remixicon/react";
import {
useCallback,
@@ -55,7 +54,6 @@ interface AnticipateInstallmentsDialogProps {
categorias: Array<{ id: string; name: string; icon: string | null }>;
pagadores: Array<{ id: string; name: string }>;
defaultPeriod: string;
periodPreferences: PeriodPreferences;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
@@ -75,7 +73,6 @@ export function AnticipateInstallmentsDialog({
categorias,
pagadores,
defaultPeriod,
periodPreferences,
open,
onOpenChange,
}: AnticipateInstallmentsDialogProps) {
@@ -104,16 +101,6 @@ export function AnticipateInstallmentsDialog({
note: "",
});
const periodOptions = useMemo(
() =>
createMonthOptions(
formState.anticipationPeriod,
periodPreferences.monthsBefore,
periodPreferences.monthsAfter
),
[formState.anticipationPeriod, periodPreferences.monthsBefore, periodPreferences.monthsAfter]
);
// Buscar parcelas elegíveis ao abrir o dialog
useEffect(() => {
if (dialogOpen) {
@@ -262,24 +249,14 @@ export function AnticipateInstallmentsDialog({
<Field className="gap-1">
<FieldLabel htmlFor="anticipation-period">Período</FieldLabel>
<FieldContent>
<Select
<PeriodPicker
value={formState.anticipationPeriod}
onValueChange={(value) =>
onChange={(value) =>
updateField("anticipationPeriod", value)
}
disabled={isPending}
>
<SelectTrigger id="anticipation-period" className="w-full">
<SelectValue placeholder="Selecione o período" />
</SelectTrigger>
<SelectContent>
{periodOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
className="w-full"
/>
</FieldContent>
</Field>

View File

@@ -2,13 +2,7 @@
import { Label } from "@/components/ui/label";
import { DatePicker } from "@/components/ui/date-picker";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PeriodPicker } from "@/components/period-picker";
import { CurrencyInput } from "@/components/ui/currency-input";
import { CalculatorDialogButton } from "@/components/calculadora/calculator-dialog";
import { RiCalculatorLine } from "@remixicon/react";
@@ -19,8 +13,7 @@ export function BasicFieldsSection({
formState,
onFieldChange,
estabelecimentos,
monthOptions,
}: BasicFieldsSectionProps) {
}: Omit<BasicFieldsSectionProps, "monthOptions">) {
return (
<>
<div className="flex w-full flex-col gap-2 md:flex-row">
@@ -37,21 +30,11 @@ export function BasicFieldsSection({
<div className="w-1/2 space-y-1">
<Label htmlFor="period">Período</Label>
<Select
<PeriodPicker
value={formState.period}
onValueChange={(value) => onFieldChange("period", value)}
>
<SelectTrigger id="period" className="w-full">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{monthOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
onChange={(value) => onFieldChange("period", value)}
className="w-full"
/>
</div>
</div>

View File

@@ -1,5 +1,4 @@
import type { LancamentoFormState } from "@/lib/lancamentos/form-helpers";
import type { PeriodPreferences } from "@/lib/user-preferences/period";
import type { LancamentoItem, SelectOption } from "../../types";
export type FormState = LancamentoFormState;
@@ -18,7 +17,6 @@ export interface LancamentoDialogProps {
estabelecimentos: string[];
lancamento?: LancamentoItem;
defaultPeriod?: string;
periodPreferences: PeriodPreferences;
defaultCartaoId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;
@@ -48,7 +46,6 @@ export interface BaseFieldSectionProps {
export interface BasicFieldsSectionProps extends BaseFieldSectionProps {
estabelecimentos: string[];
monthOptions: Array<{ value: string; label: string }>;
}
export interface CategorySectionProps extends BaseFieldSectionProps {

View File

@@ -22,7 +22,6 @@ import {
applyFieldDependencies,
buildLancamentoInitialState,
} from "@/lib/lancamentos/form-helpers";
import { createMonthOptions } from "@/lib/utils/period";
import {
useCallback,
useEffect,
@@ -58,7 +57,6 @@ export function LancamentoDialog({
estabelecimentos,
lancamento,
defaultPeriod,
periodPreferences,
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
@@ -125,15 +123,6 @@ export function LancamentoDialog({
return groupAndSortCategorias(filtered);
}, [categoriaOptions, formState.transactionType]);
const monthOptions = useMemo(
() =>
createMonthOptions(
formState.period,
periodPreferences.monthsBefore,
periodPreferences.monthsAfter
),
[formState.period, periodPreferences.monthsBefore, periodPreferences.monthsAfter]
);
const handleFieldChange = useCallback(
<Key extends keyof FormState>(key: Key, value: FormState[Key]) => {
@@ -352,7 +341,6 @@ export function LancamentoDialog({
formState={formState}
onFieldChange={handleFieldChange}
estabelecimentos={estabelecimentos}
monthOptions={monthOptions}
/>
<CategorySection

View File

@@ -23,11 +23,10 @@ import {
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Spinner } from "@/components/ui/spinner";
import { PeriodPicker } from "@/components/period-picker";
import { groupAndSortCategorias } from "@/lib/lancamentos/categoria-helpers";
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
import { getTodayDateString } from "@/lib/utils/date";
import { createMonthOptions } from "@/lib/utils/period";
import type { PeriodPreferences } from "@/lib/user-preferences/period";
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
@@ -52,7 +51,6 @@ interface MassAddDialogProps {
categoriaOptions: SelectOption[];
estabelecimentos: string[];
selectedPeriod: string;
periodPreferences: PeriodPreferences;
defaultPagadorId?: string | null;
}
@@ -93,7 +91,6 @@ export function MassAddDialog({
categoriaOptions,
estabelecimentos,
selectedPeriod,
periodPreferences,
defaultPagadorId,
}: MassAddDialogProps) {
const [loading, setLoading] = useState(false);
@@ -120,17 +117,6 @@ export function MassAddDialog({
},
]);
// Period options
const periodOptions = useMemo(
() =>
createMonthOptions(
selectedPeriod,
periodPreferences.monthsBefore,
periodPreferences.monthsAfter
),
[selectedPeriod, periodPreferences.monthsBefore, periodPreferences.monthsAfter]
);
// Categorias agrupadas e filtradas por tipo de transação
const groupedCategorias = useMemo(() => {
const filtered = categoriaOptions.filter(
@@ -336,18 +322,11 @@ export function MassAddDialog({
{/* Period */}
<div className="space-y-2">
<Label htmlFor="period">Período</Label>
<Select value={period} onValueChange={setPeriod}>
<SelectTrigger id="period" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{periodOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<PeriodPicker
value={period}
onChange={setPeriod}
className="w-full truncate"
/>
</div>
{/* Conta/Cartao */}

View File

@@ -25,7 +25,6 @@ import type {
LancamentoItem,
SelectOption,
} from "../types";
import type { PeriodPreferences } from "@/lib/user-preferences/period";
interface LancamentosPageProps {
lancamentos: LancamentoItem[];
@@ -40,7 +39,6 @@ interface LancamentosPageProps {
contaCartaoFilterOptions: ContaCartaoFilterOption[];
selectedPeriod: string;
estabelecimentos: string[];
periodPreferences: PeriodPreferences;
allowCreate?: boolean;
defaultCartaoId?: string | null;
defaultPaymentMethod?: string | null;
@@ -61,7 +59,6 @@ export function LancamentosPage({
contaCartaoFilterOptions,
selectedPeriod,
estabelecimentos,
periodPreferences,
allowCreate = true,
defaultCartaoId,
defaultPaymentMethod,
@@ -357,7 +354,6 @@ export function LancamentosPage({
categoriaOptions={categoriaOptions}
estabelecimentos={estabelecimentos}
defaultPeriod={selectedPeriod}
periodPreferences={periodPreferences}
defaultCartaoId={defaultCartaoId}
defaultPaymentMethod={defaultPaymentMethod}
lockCartaoSelection={lockCartaoSelection}
@@ -383,7 +379,6 @@ export function LancamentosPage({
estabelecimentos={estabelecimentos}
lancamento={lancamentoToCopy ?? undefined}
defaultPeriod={selectedPeriod}
periodPreferences={periodPreferences}
/>
<LancamentoDialog
@@ -399,7 +394,6 @@ export function LancamentosPage({
estabelecimentos={estabelecimentos}
lancamento={selectedLancamento ?? undefined}
defaultPeriod={selectedPeriod}
periodPreferences={periodPreferences}
onBulkEditRequest={handleBulkEditRequest}
/>
@@ -479,7 +473,6 @@ export function LancamentosPage({
categoriaOptions={categoriaOptions}
estabelecimentos={estabelecimentos}
selectedPeriod={selectedPeriod}
periodPreferences={periodPreferences}
defaultPagadorId={defaultPagadorId}
/>
) : null}
@@ -515,7 +508,6 @@ export function LancamentosPage({
name: p.label,
}))}
defaultPeriod={selectedPeriod}
periodPreferences={periodPreferences}
/>
)}