From 60b2612e8a476403873f7aecba807b44490cabb0 Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Thu, 28 May 2026 10:59:36 -0300 Subject: [PATCH] feat(relatorios): refina indicadores e filtros --- .../components/category-detail-header.tsx | 78 ++++++++++++++----- .../category-breakdown-list-item.tsx | 7 +- .../installment-expense-list-item.tsx | 26 ++++--- .../components/metrics-card-info-button.tsx | 29 ++++--- .../widgets/widget-settings-dialog.tsx | 1 + .../expenses/installment-expenses-helpers.ts | 33 ++++++-- .../expenses/installment-expenses-queries.ts | 1 + .../current-period-overview-queries.ts | 1 + .../components/category-report-filters.tsx | 66 ++++++++-------- .../reports/components/category-table.tsx | 18 ++--- .../widgets/expandable-widget-card.tsx | 9 ++- 11 files changed, 168 insertions(+), 101 deletions(-) diff --git a/src/features/categories/components/category-detail-header.tsx b/src/features/categories/components/category-detail-header.tsx index 8cd5181..fbdfebd 100644 --- a/src/features/categories/components/category-detail-header.tsx +++ b/src/features/categories/components/category-detail-header.tsx @@ -5,6 +5,7 @@ import { Card } from "@/shared/components/ui/card"; import type { CategoryType } from "@/shared/lib/categories/constants"; import { currencyFormatter } from "@/shared/utils/currency"; import { formatPercentage } from "@/shared/utils/percentage"; +import { cn } from "@/shared/utils/ui"; type CategorySummary = { id: string; @@ -32,19 +33,40 @@ export function CategoryDetailHeader({ percentageChange, transactionCount, }: CategoryDetailHeaderProps) { + const absoluteChange = currentTotal - previousTotal; const variationLabel = typeof percentageChange === "number" ? formatPercentage(percentageChange, { minimumFractionDigits: 1, maximumFractionDigits: 1, absolute: true, - signDisplay: percentageChange === 0 ? "auto" : "always", }) : "—"; + const hasComparison = typeof percentageChange === "number"; + const isFlat = absoluteChange === 0; + const changeDirection = + absoluteChange > 0 ? "increase" : absoluteChange < 0 ? "decrease" : "flat"; + const comparisonTone = + isFlat || !hasComparison + ? "neutral" + : category.type === "receita" + ? changeDirection === "increase" + ? "positive" + : "negative" + : changeDirection === "decrease" + ? "positive" + : "negative"; + const statusLabel = !hasComparison + ? "Sem comparação" + : isFlat + ? "Estável" + : changeDirection === "increase" + ? "Aumento" + : "Queda"; return ( - -
+ +
{transactionCount}{" "} - {transactionCount === 1 ? "lançamento" : "lançamentos"} no{" "} - período + {transactionCount === 1 ? "lançamento" : "lançamentos"} em{" "} + {currentPeriodLabel}
-
-
+
+

Total em {currentPeriodLabel}

-

+

{currencyFormatter.format(currentTotal)}

-
+ +

Total em {previousPeriodLabel}

-

+

{currencyFormatter.format(previousTotal)}

-
+ +

- Variação vs mês anterior + Variação

- +
+ + {statusLabel} + + +
diff --git a/src/features/dashboard/components/category-breakdown/category-breakdown-list-item.tsx b/src/features/dashboard/components/category-breakdown/category-breakdown-list-item.tsx index 103acfa..0fd0396 100644 --- a/src/features/dashboard/components/category-breakdown/category-breakdown-list-item.tsx +++ b/src/features/dashboard/components/category-breakdown/category-breakdown-list-item.tsx @@ -1,4 +1,4 @@ -import { RiExternalLinkLine, RiWallet3Line } from "@remixicon/react"; +import { RiExternalLinkLine } from "@remixicon/react"; import Link from "next/link"; import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers"; import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator"; @@ -63,7 +63,7 @@ export function CategoryBreakdownListItem({ />
-
+
{formatPercentage( category.percentageOfTotal, @@ -77,10 +77,9 @@ export function CategoryBreakdownListItem({ - {budgetExceeded ? ( <> - excedeu{" "} + Excedeu{" "} {formatCurrency(exceededAmount)} diff --git a/src/features/dashboard/components/installment-expenses/installment-expense-list-item.tsx b/src/features/dashboard/components/installment-expenses/installment-expense-list-item.tsx index 016f73b..1f5987c 100644 --- a/src/features/dashboard/components/installment-expenses/installment-expense-list-item.tsx +++ b/src/features/dashboard/components/installment-expenses/installment-expense-list-item.tsx @@ -20,6 +20,7 @@ export function InstallmentExpenseListItem({ const { compactLabel, isLast, + remainingLabel, remainingInstallments, remainingAmount, endDate, @@ -65,15 +66,22 @@ export function InstallmentExpenseListItem({ />
-

- {endDate ? `Termina em ${endDate}` : null} - {" · Restante "} - {" "} - ({remainingInstallments}) -

+ {remainingInstallments === 0 ? ( +

+ {endDate ? `Termina em ${endDate}` : null} + {" · Quitado"} +

+ ) : ( +

+ {endDate ? `Termina em ${endDate}` : null} + {` · ${remainingLabel}: `} + {" "} + ({remainingInstallments}x) +

+ )}
diff --git a/src/features/dashboard/components/metrics-card-info-button.tsx b/src/features/dashboard/components/metrics-card-info-button.tsx index d906190..e3b8a0f 100644 --- a/src/features/dashboard/components/metrics-card-info-button.tsx +++ b/src/features/dashboard/components/metrics-card-info-button.tsx @@ -2,10 +2,10 @@ import { RiInformationLine } from "@remixicon/react"; import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@/shared/components/ui/hover-card"; + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/shared/components/ui/tooltip"; type MetricsCardInfoButtonProps = { label: string; @@ -19,8 +19,8 @@ export function MetricsCardInfoButton({ helpLines, }: MetricsCardInfoButtonProps) { return ( - - + + - - + +
-

{helpTitle}

+

{helpTitle}

-
    +
      {helpLines.map((line) => (
    • {line}
    • ))}
    - - + + ); } diff --git a/src/features/dashboard/components/widgets/widget-settings-dialog.tsx b/src/features/dashboard/components/widgets/widget-settings-dialog.tsx index 3e08720..52f5598 100644 --- a/src/features/dashboard/components/widgets/widget-settings-dialog.tsx +++ b/src/features/dashboard/components/widgets/widget-settings-dialog.tsx @@ -73,6 +73,7 @@ export function WidgetSettingsDialog({
onToggleWidget(widget.id)} /> diff --git a/src/features/dashboard/expenses/installment-expenses-helpers.ts b/src/features/dashboard/expenses/installment-expenses-helpers.ts index a827b9b..9733b35 100644 --- a/src/features/dashboard/expenses/installment-expenses-helpers.ts +++ b/src/features/dashboard/expenses/installment-expenses-helpers.ts @@ -1,12 +1,11 @@ import type { InstallmentExpense } from "@/features/dashboard/expenses/installment-expenses-queries"; -import { - calculateLastInstallmentDate, - formatLastInstallmentDate, -} from "@/shared/lib/installments/utils"; +import { calculateLastInstallmentDate } from "@/shared/lib/installments/utils"; +import { capitalize } from "@/shared/utils/string"; type InstallmentExpenseDisplay = { compactLabel: string | null; isLast: boolean; + remainingLabel: "Próx." | "Aberto"; remainingInstallments: number; remainingAmount: number; endDate: string | null; @@ -38,21 +37,30 @@ const isInstallmentLast = ( const calculateInstallmentRemainingCount = ( currentInstallment: number | null, installmentCount: number | null, + isSettled: boolean | null, ) => { if (!currentInstallment || !installmentCount) { return 0; } - return Math.max(0, installmentCount - currentInstallment); + const includeCurrentInstallment = isSettled !== true; + const currentOffset = includeCurrentInstallment ? 1 : 0; + + return Math.max(0, installmentCount - currentInstallment + currentOffset); }; const calculateInstallmentRemainingAmount = ( amount: number, currentInstallment: number | null, installmentCount: number | null, + isSettled: boolean | null, ) => amount * - calculateInstallmentRemainingCount(currentInstallment, installmentCount); + calculateInstallmentRemainingCount( + currentInstallment, + installmentCount, + isSettled, + ); const formatInstallmentEndDate = ( period: string, @@ -69,7 +77,12 @@ const formatInstallmentEndDate = ( installmentCount, ); - return formatLastInstallmentDate(lastDate); + const month = new Intl.DateTimeFormat("pt-BR", { + month: "short", + timeZone: "UTC", + }).format(lastDate); + + return `${capitalize(month)} de ${lastDate.getFullYear()}`; }; const buildInstallmentProgress = ( @@ -89,7 +102,8 @@ const buildInstallmentProgress = ( export const buildInstallmentExpenseDisplay = ( expense: InstallmentExpense, ): InstallmentExpenseDisplay => { - const { amount, currentInstallment, installmentCount, period } = expense; + const { amount, currentInstallment, installmentCount, isSettled, period } = + expense; return { compactLabel: buildInstallmentCompactLabel( @@ -97,14 +111,17 @@ export const buildInstallmentExpenseDisplay = ( installmentCount, ), isLast: isInstallmentLast(currentInstallment, installmentCount), + remainingLabel: isSettled === true ? "Próx." : "Aberto", remainingInstallments: calculateInstallmentRemainingCount( currentInstallment, installmentCount, + isSettled, ), remainingAmount: calculateInstallmentRemainingAmount( amount, currentInstallment, installmentCount, + isSettled, ), endDate: formatInstallmentEndDate( period, diff --git a/src/features/dashboard/expenses/installment-expenses-queries.ts b/src/features/dashboard/expenses/installment-expenses-queries.ts index c52c7c0..701c198 100644 --- a/src/features/dashboard/expenses/installment-expenses-queries.ts +++ b/src/features/dashboard/expenses/installment-expenses-queries.ts @@ -8,6 +8,7 @@ export type InstallmentExpense = { dueDate: Date | null; purchaseDate: Date; period: string; + isSettled: boolean | null; }; export type InstallmentExpensesData = { diff --git a/src/features/dashboard/overview/current-period-overview-queries.ts b/src/features/dashboard/overview/current-period-overview-queries.ts index 5bd8840..7c08019 100644 --- a/src/features/dashboard/overview/current-period-overview-queries.ts +++ b/src/features/dashboard/overview/current-period-overview-queries.ts @@ -405,6 +405,7 @@ const buildInstallmentExpensesData = ( dueDate: row.dueDate, purchaseDate: row.purchaseDate, period: row.period, + isSettled: row.isSettled, })) .sort((a, b) => { const remainingA = diff --git a/src/features/reports/components/category-report-filters.tsx b/src/features/reports/components/category-report-filters.tsx index 6bdb720..248f79c 100644 --- a/src/features/reports/components/category-report-filters.tsx +++ b/src/features/reports/components/category-report-filters.tsx @@ -31,8 +31,12 @@ import { getCurrentPeriod, periodToDate, } from "@/shared/utils/period"; +import { slugify } from "@/shared/utils/string"; import type { CategoryReportFiltersProps } from "./types"; +const getCategorySearchValue = (name: string, id: string) => + `${name} ${slugify(name)} ${id}`; + /** * Category Report Filters Component * Provides filters for categories selection and date range @@ -53,7 +57,14 @@ export function CategoryReportFilters({ const filteredCategories = useMemo(() => { if (!searchValue) return categories; const search = searchValue.toLowerCase(); - return categories.filter((cat) => cat.name.toLowerCase().includes(search)); + const normalizedSearch = slugify(searchValue); + return categories.filter((cat) => { + const categorySearchValue = getCategorySearchValue(cat.name, cat.id); + return ( + categorySearchValue.toLowerCase().includes(search) || + categorySearchValue.includes(normalizedSearch) + ); + }); }, [categories, searchValue]); // Get selected categories for display @@ -76,15 +87,6 @@ export function CategoryReportFilters({ }); }; - // Handle select all - const handleSelectAll = () => { - onFiltersChange({ - ...filters, - selectedCategories: categories.map((cat) => cat.id), - }); - setOpen(false); - }; - // Handle clear all const handleClearAll = () => { onFiltersChange({ @@ -130,11 +132,9 @@ export function CategoryReportFilters({ const selectedText = selectedCategories.length === 0 ? "Categoria" - : selectedCategories.length === categories.length - ? "Todas" - : selectedCategories.length === 1 - ? selectedCategories[0].name - : `${selectedCategories.length} selecionadas`; + : selectedCategories.length === 1 + ? selectedCategories[0].name + : `${selectedCategories.length} selecionadas`; return (
@@ -168,25 +168,18 @@ export function CategoryReportFilters({ Nenhuma categoria encontrada. - {/* Select All / Clear All */} -
- - -
+ {filters.selectedCategories.length > 0 ? ( +
+ +
+ ) : null} {/* Category List */} {filteredCategories.map((category) => { @@ -200,7 +193,10 @@ export function CategoryReportFilters({ return ( handleCategoryToggle(category.id)} className="cursor-pointer" > diff --git a/src/features/reports/components/category-table.tsx b/src/features/reports/components/category-table.tsx index 0327fec..1b21f64 100644 --- a/src/features/reports/components/category-table.tsx +++ b/src/features/reports/components/category-table.tsx @@ -5,7 +5,6 @@ import Link from "next/link"; import { useMemo } from "react"; import { formatPeriodLabel } from "@/features/reports/lib/utils"; import { CategoryIconBadge } from "@/shared/components/entity-avatar"; -import StatusDot from "@/shared/components/feedback/status-dot"; import { Card } from "@/shared/components/ui/card"; import { Table, @@ -37,6 +36,13 @@ export function CategoryTable({ categories, periods, }: CategoryTableProps) { + const categoryColumnLabel = + title === "Despesas" + ? "Categoria Despesa" + : title === "Receitas" + ? "Categoria Receita" + : "Categoria"; + // Calculate section totals const sectionTotals = useMemo(() => { const totalsMap = new Map(); @@ -73,7 +79,7 @@ export function CategoryTable({ - Categoria + {categoryColumnLabel} {periods.map((period) => (
- - +
@@ -94,7 +95,7 @@ export function ExpandableWidgetCard({

{subtitle}

) : null} -
+
{children}