From fd84a0d1aca44574885bd4bf17fd2645682007f7 Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Fri, 30 Jan 2026 14:52:11 +0000 Subject: [PATCH] =?UTF-8?q?feat(relatorios):=20reorganizar=20p=C3=A1ginas?= =?UTF-8?q?=20e=20criar=20componente=20CategoryIconBadge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Renomear /relatorios/categorias para /relatorios/tendencias - Renomear /relatorios/cartoes para /relatorios/uso-cartoes - Criar componente CategoryIconBadge unificado com cores dinâmicas - Atualizar cards de categorias com novo layout (ações no footer) - Atualizar cards de orçamentos com CategoryIconBadge - Adicionar tooltip detalhado nas células de tendências (valor anterior e diferença) - Adicionar dot colorido (verde/vermelho) para indicar tipo de categoria Co-Authored-By: Claude Opus 4.5 --- .../{categorias => tendencias}/data.ts | 0 .../{categorias => tendencias}/layout.tsx | 0 .../{categorias => tendencias}/loading.tsx | 0 .../{categorias => tendencias}/page.tsx | 2 +- .../{cartoes => uso-cartoes}/layout.tsx | 0 .../{cartoes => uso-cartoes}/loading.tsx | 0 .../{cartoes => uso-cartoes}/page.tsx | 0 components/categorias/categories-page.tsx | 3 +- components/categorias/category-card.tsx | 139 +++++++------ .../categorias/category-detail-header.tsx | 34 +--- components/categorias/category-icon-badge.tsx | 78 +++++++ ...expenses-by-category-widget-with-chart.tsx | 33 +-- .../income-by-category-widget-with-chart.tsx | 33 +-- components/orcamentos/budget-card.tsx | 16 +- components/orcamentos/budgets-page.tsx | 3 +- .../cartoes/card-category-breakdown.tsx | 92 ++++----- .../relatorios/cartoes/card-top-expenses.tsx | 2 +- .../relatorios/cartoes/cards-overview.tsx | 4 +- components/relatorios/category-cell.tsx | 63 ++++-- .../relatorios/category-report-cards.tsx | 190 +++++++++++++----- .../relatorios/category-report-table.tsx | 133 ++++-------- components/relatorios/category-table.tsx | 150 ++++++++++++++ components/relatorios/index.ts | 1 + components/sidebar/nav-link.tsx | 4 +- .../top-estabelecimentos/top-categories.tsx | 35 +--- 25 files changed, 611 insertions(+), 404 deletions(-) rename app/(dashboard)/relatorios/{categorias => tendencias}/data.ts (100%) rename app/(dashboard)/relatorios/{categorias => tendencias}/layout.tsx (100%) rename app/(dashboard)/relatorios/{categorias => tendencias}/loading.tsx (100%) rename app/(dashboard)/relatorios/{categorias => tendencias}/page.tsx (98%) rename app/(dashboard)/relatorios/{cartoes => uso-cartoes}/layout.tsx (100%) rename app/(dashboard)/relatorios/{cartoes => uso-cartoes}/loading.tsx (100%) rename app/(dashboard)/relatorios/{cartoes => uso-cartoes}/page.tsx (100%) create mode 100644 components/categorias/category-icon-badge.tsx create mode 100644 components/relatorios/category-table.tsx diff --git a/app/(dashboard)/relatorios/categorias/data.ts b/app/(dashboard)/relatorios/tendencias/data.ts similarity index 100% rename from app/(dashboard)/relatorios/categorias/data.ts rename to app/(dashboard)/relatorios/tendencias/data.ts diff --git a/app/(dashboard)/relatorios/categorias/layout.tsx b/app/(dashboard)/relatorios/tendencias/layout.tsx similarity index 100% rename from app/(dashboard)/relatorios/categorias/layout.tsx rename to app/(dashboard)/relatorios/tendencias/layout.tsx diff --git a/app/(dashboard)/relatorios/categorias/loading.tsx b/app/(dashboard)/relatorios/tendencias/loading.tsx similarity index 100% rename from app/(dashboard)/relatorios/categorias/loading.tsx rename to app/(dashboard)/relatorios/tendencias/loading.tsx diff --git a/app/(dashboard)/relatorios/categorias/page.tsx b/app/(dashboard)/relatorios/tendencias/page.tsx similarity index 98% rename from app/(dashboard)/relatorios/categorias/page.tsx rename to app/(dashboard)/relatorios/tendencias/page.tsx index 8ffb4b7..5d7f5dd 100644 --- a/app/(dashboard)/relatorios/categorias/page.tsx +++ b/app/(dashboard)/relatorios/tendencias/page.tsx @@ -58,7 +58,7 @@ export default async function Page({ searchParams }: PageProps) { if (!validation.isValid) { // Redirect to default if validation fails redirect( - `/relatorios/categorias?inicio=${defaultStartPeriod}&fim=${currentPeriod}`, + `/relatorios/tendencias?inicio=${defaultStartPeriod}&fim=${currentPeriod}`, ); } diff --git a/app/(dashboard)/relatorios/cartoes/layout.tsx b/app/(dashboard)/relatorios/uso-cartoes/layout.tsx similarity index 100% rename from app/(dashboard)/relatorios/cartoes/layout.tsx rename to app/(dashboard)/relatorios/uso-cartoes/layout.tsx diff --git a/app/(dashboard)/relatorios/cartoes/loading.tsx b/app/(dashboard)/relatorios/uso-cartoes/loading.tsx similarity index 100% rename from app/(dashboard)/relatorios/cartoes/loading.tsx rename to app/(dashboard)/relatorios/uso-cartoes/loading.tsx diff --git a/app/(dashboard)/relatorios/cartoes/page.tsx b/app/(dashboard)/relatorios/uso-cartoes/page.tsx similarity index 100% rename from app/(dashboard)/relatorios/cartoes/page.tsx rename to app/(dashboard)/relatorios/uso-cartoes/page.tsx diff --git a/components/categorias/categories-page.tsx b/components/categorias/categories-page.tsx index 8498e5c..74b304f 100644 --- a/components/categorias/categories-page.tsx +++ b/components/categorias/categories-page.tsx @@ -130,10 +130,11 @@ export function CategoriesPage({ categories }: CategoriesPageProps) { ) : (
- {categoriesByType[type].map((category) => ( + {categoriesByType[type].map((category, index) => ( diff --git a/components/categorias/category-card.tsx b/components/categorias/category-card.tsx index 2fcfb03..f129a5d 100644 --- a/components/categorias/category-card.tsx +++ b/components/categorias/category-card.tsx @@ -1,94 +1,103 @@ "use client"; -import { RiDeleteBin5Line, RiMore2Fill, RiPencilLine } from "@remixicon/react"; -import Link from "next/link"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { TypeBadge } from "../type-badge"; -import { CategoryIcon } from "./category-icon"; + RiDeleteBin5Line, + RiFileList2Line, + RiPencilLine, +} from "@remixicon/react"; +import Link from "next/link"; +import { Card, CardContent, CardFooter } from "@/components/ui/card"; +import { cn } from "@/lib/utils/ui"; +import { CategoryIconBadge } from "./category-icon-badge"; import type { Category } from "./types"; interface CategoryCardProps { category: Category; + colorIndex: number; onEdit: (category: Category) => void; onRemove: (category: Category) => void; } export function CategoryCard({ category, + colorIndex, onEdit, onRemove, }: CategoryCardProps) { - // Categorias protegidas que não podem ser editadas ou removidas const categoriasProtegidas = [ "Transferência interna", "Saldo inicial", "Pagamentos", ]; const isProtegida = categoriasProtegidas.includes(category.name); - const canEdit = !isProtegida; - const canRemove = !isProtegida; + + const actions = [ + { + label: "editar", + icon: , + onClick: () => onEdit(category), + variant: "default" as const, + disabled: isProtegida, + }, + { + label: "detalhes", + icon: , + href: `/categorias/${category.id}`, + variant: "default" as const, + disabled: false, + }, + { + label: "remover", + icon: , + onClick: () => onRemove(category), + variant: "destructive" as const, + disabled: isProtegida, + }, + ].filter((action) => !action.disabled); return ( - - -
-
- - - -
-

- - {category.name} - -

-
- -
-
-
- - - - - - - onEdit(category)} - disabled={!canEdit} - > - - Editar - - onRemove(category)} - disabled={!canRemove} - > - - Remover - - - + + +
+ +

{category.name}

+ + + {actions.map(({ label, icon, onClick, href, variant }) => { + const className = cn( + "flex items-center gap-1 font-medium transition-opacity hover:opacity-80", + variant === "destructive" ? "text-destructive" : "text-primary", + ); + + if (href) { + return ( + + {icon} + {label} + + ); + } + + return ( + + ); + })} +
); } diff --git a/components/categorias/category-detail-header.tsx b/components/categorias/category-detail-header.tsx index f29cea6..0c7e888 100644 --- a/components/categorias/category-detail-header.tsx +++ b/components/categorias/category-detail-header.tsx @@ -1,25 +1,11 @@ import { RiArrowDownLine, RiArrowUpLine } from "@remixicon/react"; import type { CategoryType } from "@/lib/categorias/constants"; import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers"; -import { getIconComponent } from "@/lib/utils/icons"; import { cn } from "@/lib/utils/ui"; +import { CategoryIconBadge } from "./category-icon-badge"; import { TypeBadge } from "../type-badge"; import { Card } from "../ui/card"; -const buildInitials = (value: string) => { - const parts = value.trim().split(/\s+/).filter(Boolean); - if (parts.length === 0) { - return "CT"; - } - if (parts.length === 1) { - const firstPart = parts[0]; - return firstPart ? firstPart.slice(0, 2).toUpperCase() : "CT"; - } - const firstChar = parts[0]?.[0] ?? ""; - const secondChar = parts[1]?.[0] ?? ""; - return `${firstChar}${secondChar}`.toUpperCase() || "CT"; -}; - type CategorySummary = { id: string; name: string; @@ -46,9 +32,6 @@ export function CategoryDetailHeader({ percentageChange, transactionCount, }: CategoryDetailHeaderProps) { - const IconComponent = category.icon ? getIconComponent(category.icon) : null; - const initials = buildInitials(category.name); - const isIncrease = typeof percentageChange === "number" && percentageChange > 0; const isDecrease = @@ -87,15 +70,12 @@ export function CategoryDetailHeader({
- - {IconComponent ? ( - - ) : ( - - {initials} - - )} - +

{category.name} diff --git a/components/categorias/category-icon-badge.tsx b/components/categorias/category-icon-badge.tsx new file mode 100644 index 0000000..6afe675 --- /dev/null +++ b/components/categorias/category-icon-badge.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { + buildCategoryInitials, + getCategoryBgColor, + getCategoryColor, +} from "@/lib/utils/category-colors"; +import { getIconComponent } from "@/lib/utils/icons"; +import { cn } from "@/lib/utils/ui"; + +const sizeVariants = { + sm: { + container: "size-8", + icon: "size-4", + text: "text-[10px]", + }, + md: { + container: "size-9", + icon: "size-5", + text: "text-xs", + }, + lg: { + container: "size-12", + icon: "size-6", + text: "text-sm", + }, +} as const; + +export type CategoryIconBadgeSize = keyof typeof sizeVariants; + +export interface CategoryIconBadgeProps { + /** Nome do ícone Remix (ex: "RiShoppingBag3Line") */ + icon?: string | null; + /** Nome da categoria (usado para gerar iniciais como fallback) */ + name: string; + /** Índice para determinar a cor (cicla entre as cores disponíveis) */ + colorIndex: number; + /** Tamanho do badge: sm (32px), md (36px), lg (48px) */ + size?: CategoryIconBadgeSize; + /** Classes adicionais para o container */ + className?: string; +} + +export function CategoryIconBadge({ + icon, + name, + colorIndex, + size = "md", + className, +}: CategoryIconBadgeProps) { + const IconComponent = icon ? getIconComponent(icon) : null; + const initials = buildCategoryInitials(name); + const color = getCategoryColor(colorIndex); + const bgColor = getCategoryBgColor(colorIndex); + const variant = sizeVariants[size]; + + return ( +
+ {IconComponent ? ( + + ) : ( + + {initials} + + )} +
+ ); +} diff --git a/components/dashboard/expenses-by-category-widget-with-chart.tsx b/components/dashboard/expenses-by-category-widget-with-chart.tsx index dfe659a..d550c61 100644 --- a/components/dashboard/expenses-by-category-widget-with-chart.tsx +++ b/components/dashboard/expenses-by-category-widget-with-chart.tsx @@ -12,15 +12,10 @@ import { import Link from "next/link"; import { useMemo, useState } from "react"; import { Pie, PieChart, Tooltip } from "recharts"; +import { CategoryIconBadge } from "@/components/categorias/category-icon-badge"; import MoneyValues from "@/components/money-values"; import { type ChartConfig, ChartContainer } from "@/components/ui/chart"; import type { ExpensesByCategoryData } from "@/lib/dashboard/categories/expenses-by-category"; -import { - buildCategoryInitials, - getCategoryBgColor, - getCategoryColor, -} from "@/lib/utils/category-colors"; -import { getIconComponent } from "@/lib/utils/icons"; import { formatPeriodForUrl } from "@/lib/utils/period"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { WidgetEmptyState } from "../widget-empty-state"; @@ -162,12 +157,6 @@ export function ExpensesByCategoryWidgetWithChart({
{data.categories.map((category, index) => { - const IconComponent = category.categoryIcon - ? getIconComponent(category.categoryIcon) - : null; - const initials = buildCategoryInitials(category.categoryName); - const color = getCategoryColor(index); - const bgColor = getCategoryBgColor(index); const hasIncrease = category.percentageChange !== null && category.percentageChange > 0; @@ -192,21 +181,11 @@ export function ExpensesByCategoryWidgetWithChart({ >
-
- {IconComponent ? ( - - ) : ( - - {initials} - - )} -
+
diff --git a/components/dashboard/income-by-category-widget-with-chart.tsx b/components/dashboard/income-by-category-widget-with-chart.tsx index 0b19b51..2418d06 100644 --- a/components/dashboard/income-by-category-widget-with-chart.tsx +++ b/components/dashboard/income-by-category-widget-with-chart.tsx @@ -12,15 +12,10 @@ import { import Link from "next/link"; import { useMemo, useState } from "react"; import { Pie, PieChart, Tooltip } from "recharts"; +import { CategoryIconBadge } from "@/components/categorias/category-icon-badge"; import MoneyValues from "@/components/money-values"; import { type ChartConfig, ChartContainer } from "@/components/ui/chart"; import type { IncomeByCategoryData } from "@/lib/dashboard/categories/income-by-category"; -import { - buildCategoryInitials, - getCategoryBgColor, - getCategoryColor, -} from "@/lib/utils/category-colors"; -import { getIconComponent } from "@/lib/utils/icons"; import { formatPeriodForUrl } from "@/lib/utils/period"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { WidgetEmptyState } from "../widget-empty-state"; @@ -162,12 +157,6 @@ export function IncomeByCategoryWidgetWithChart({
{data.categories.map((category, index) => { - const IconComponent = category.categoryIcon - ? getIconComponent(category.categoryIcon) - : null; - const initials = buildCategoryInitials(category.categoryName); - const color = getCategoryColor(index); - const bgColor = getCategoryBgColor(index); const hasIncrease = category.percentageChange !== null && category.percentageChange > 0; @@ -192,21 +181,11 @@ export function IncomeByCategoryWidgetWithChart({ >
-
- {IconComponent ? ( - - ) : ( - - {initials} - - )} -
+
diff --git a/components/orcamentos/budget-card.tsx b/components/orcamentos/budget-card.tsx index 3f59ed0..0f880e5 100644 --- a/components/orcamentos/budget-card.tsx +++ b/components/orcamentos/budget-card.tsx @@ -1,7 +1,7 @@ "use client"; import { RiDeleteBin5Line, RiPencilLine } from "@remixicon/react"; -import { CategoryIcon } from "@/components/categorias/category-icon"; +import { CategoryIconBadge } from "@/components/categorias/category-icon-badge"; import MoneyValues from "@/components/money-values"; import { Card, CardContent, CardFooter } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; @@ -10,6 +10,7 @@ import type { Budget } from "./types"; interface BudgetCardProps { budget: Budget; + colorIndex: number; periodLabel: string; onEdit: (budget: Budget) => void; onRemove: (budget: Budget) => void; @@ -28,6 +29,7 @@ const formatCategoryName = (budget: Budget) => export function BudgetCard({ budget, + colorIndex, periodLabel, onEdit, onRemove, @@ -41,12 +43,12 @@ export function BudgetCard({
- - - +

{formatCategoryName(budget)} diff --git a/components/orcamentos/budgets-page.tsx b/components/orcamentos/budgets-page.tsx index e12682a..eff1828 100644 --- a/components/orcamentos/budgets-page.tsx +++ b/components/orcamentos/budgets-page.tsx @@ -129,10 +129,11 @@ export function BudgetsPage({ {hasBudgets ? (
- {budgets.map((budget) => ( + {budgets.map((budget, index) => (
- {data.map((category, index) => { - const IconComponent = category.icon - ? getIconComponent(category.icon) - : null; - const color = getCategoryColor(index); - const bgColor = getCategoryBgColor(index); - const initials = buildCategoryInitials(category.name); + {data.map((category, index) => ( +
+
+
+ - return ( -
-
-
-
- {IconComponent ? ( - - ) : ( - - {initials} - - )} -
- - {/* Name and percentage */} -
- - {category.name} - - - {category.percent.toFixed(0)}% do total - -
-
- - {/* Value */} -
- + {/* Name and percentage */} +
+ + {category.name} + + + {category.percent.toFixed(0)}% do total +
- {/* Progress bar */} -
- + {/* Value */} +
+
- ); - })} + + {/* Progress bar */} +
+ +
+
+ ))}
diff --git a/components/relatorios/cartoes/card-top-expenses.tsx b/components/relatorios/cartoes/card-top-expenses.tsx index 8194c52..7536b42 100644 --- a/components/relatorios/cartoes/card-top-expenses.tsx +++ b/components/relatorios/cartoes/card-top-expenses.tsx @@ -60,7 +60,7 @@ export function CardTopExpenses({ data }: CardTopExpensesProps) {
{/* Rank number */} -
+
{index + 1} diff --git a/components/relatorios/cartoes/cards-overview.tsx b/components/relatorios/cartoes/cards-overview.tsx index 5f0f156..c4f71f2 100644 --- a/components/relatorios/cartoes/cards-overview.tsx +++ b/components/relatorios/cartoes/cards-overview.tsx @@ -67,7 +67,7 @@ export function CardsOverview({ data }: CardsOverviewProps) { const params = new URLSearchParams(); if (periodoParam) params.set("periodo", periodoParam); params.set("cartao", cardId); - return `/relatorios/cartoes?${params.toString()}`; + return `/relatorios/uso-cartoes?${params.toString()}`; }; const summaryCards = [ @@ -140,7 +140,7 @@ export function CardsOverview({ data }: CardsOverviewProps) { alt={card.name} width={32} height={32} - className="rounded object-contain" + className="rounded-sm object-contain" /> ) : ( diff --git a/components/relatorios/category-cell.tsx b/components/relatorios/category-cell.tsx index 94fe252..dc9ec96 100644 --- a/components/relatorios/category-cell.tsx +++ b/components/relatorios/category-cell.tsx @@ -1,6 +1,11 @@ "use client"; import { RiArrowDownLine, RiArrowUpLine } from "@remixicon/react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { formatCurrency, formatPercentageChange } from "@/lib/relatorios/utils"; import { cn } from "@/lib/utils/ui"; @@ -22,25 +27,55 @@ export function CategoryCell({ ? ((value - previousValue) / previousValue) * 100 : null; + const absoluteChange = !isFirstMonth ? value - previousValue : null; + const isIncrease = percentageChange !== null && percentageChange > 0; const isDecrease = percentageChange !== null && percentageChange < 0; return ( -
- {formatCurrency(value)} - {!isFirstMonth && percentageChange !== null && ( -
+ +
+ {formatCurrency(value)} + {!isFirstMonth && percentageChange !== null && ( +
+ {isIncrease && } + {isDecrease && } + {formatPercentageChange(percentageChange)} +
)} - > - {isIncrease && } - {isDecrease && } - {formatPercentageChange(percentageChange)}
- )} -
+ + +
+
{formatCurrency(value)}
+ {!isFirstMonth && absoluteChange !== null && ( + <> +
+ Mês anterior: {formatCurrency(previousValue)} +
+
+ Diferença:{" "} + {absoluteChange >= 0 + ? `+${formatCurrency(absoluteChange)}` + : formatCurrency(absoluteChange)} +
+ + )} +
+
+ ); } diff --git a/components/relatorios/category-report-cards.tsx b/components/relatorios/category-report-cards.tsx index d470857..af37930 100644 --- a/components/relatorios/category-report-cards.tsx +++ b/components/relatorios/category-report-cards.tsx @@ -1,63 +1,161 @@ "use client"; -import { TypeBadge } from "@/components/type-badge"; +import Link from "next/link"; +import { useMemo } from "react"; +import { CategoryIconBadge } from "@/components/categorias/category-icon-badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import type { CategoryReportData } from "@/lib/relatorios/types"; +import type { + CategoryReportData, + CategoryReportItem, +} from "@/lib/relatorios/types"; import { formatCurrency, formatPeriodLabel } from "@/lib/relatorios/utils"; -import { getIconComponent } from "@/lib/utils/icons"; +import { formatPeriodForUrl } from "@/lib/utils/period"; import { CategoryCell } from "./category-cell"; interface CategoryReportCardsProps { data: CategoryReportData; } -export function CategoryReportCards({ data }: CategoryReportCardsProps) { - const { categories, periods } = data; +interface CategoryCardProps { + category: CategoryReportItem; + periods: string[]; + colorIndex: number; +} + +function CategoryCard({ category, periods, colorIndex }: CategoryCardProps) { + const periodParam = formatPeriodForUrl(periods[periods.length - 1]); return ( -
- {categories.map((category) => { - const Icon = category.icon ? getIconComponent(category.icon) : null; + + + + + + {category.name} + + + + + {periods.map((period, periodIndex) => { + const monthData = category.monthlyData.get(period); + const isFirstMonth = periodIndex === 0; - return ( - - - - {Icon && } - {category.name} - - - - - {periods.map((period, periodIndex) => { - const monthData = category.monthlyData.get(period); - const isFirstMonth = periodIndex === 0; + return ( +
+ + {formatPeriodLabel(period)} + + +
+ ); + })} +
+ Total + {formatCurrency(category.total)} +
+
+
+ ); +} - return ( -
- - {formatPeriodLabel(period)} - - -
- ); - })} -
- Total - {formatCurrency(category.total)} -
-
-
- ); - })} +interface SectionProps { + title: string; + categories: CategoryReportItem[]; + periods: string[]; + colorIndexOffset: number; + total: number; +} + +function Section({ + title, + categories, + periods, + colorIndexOffset, + total, +}: SectionProps) { + if (categories.length === 0) { + return null; + } + + return ( +
+
+ + {title} + + + {formatCurrency(total)} + +
+ {categories.map((category, index) => ( + + ))} +
+ ); +} + +export function CategoryReportCards({ data }: CategoryReportCardsProps) { + const { categories, periods } = data; + + // Separate categories by type and calculate totals + const { receitas, despesas, receitasTotal, despesasTotal } = useMemo(() => { + const receitas: CategoryReportItem[] = []; + const despesas: CategoryReportItem[] = []; + let receitasTotal = 0; + let despesasTotal = 0; + + for (const category of categories) { + if (category.type === "receita") { + receitas.push(category); + receitasTotal += category.total; + } else { + despesas.push(category); + despesasTotal += category.total; + } + } + + return { receitas, despesas, receitasTotal, despesasTotal }; + }, [categories]); + + return ( +
+ {/* Despesas Section */} +
+ + {/* Receitas Section */} +
); } diff --git a/components/relatorios/category-report-table.tsx b/components/relatorios/category-report-table.tsx index c5b3803..6fae5d8 100644 --- a/components/relatorios/category-report-table.tsx +++ b/components/relatorios/category-report-table.tsx @@ -1,110 +1,49 @@ "use client"; -import { - Table, - TableBody, - TableCell, - TableFooter, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import type { CategoryReportData } from "@/lib/relatorios/types"; -import { formatCurrency, formatPeriodLabel } from "@/lib/relatorios/utils"; -import { getIconComponent } from "@/lib/utils/icons"; -import DotIcon from "../dot-icon"; -import { Card } from "../ui/card"; -import { CategoryCell } from "./category-cell"; +import { useMemo } from "react"; +import type { CategoryReportData, CategoryReportItem } from "@/lib/relatorios/types"; +import { CategoryTable } from "./category-table"; interface CategoryReportTableProps { data: CategoryReportData; } export function CategoryReportTable({ data }: CategoryReportTableProps) { - const { categories, periods, totals, grandTotal } = data; + const { categories, periods } = data; + + // Separate categories by type + const { receitas, despesas } = useMemo(() => { + const receitas: CategoryReportItem[] = []; + const despesas: CategoryReportItem[] = []; + + for (const category of categories) { + if (category.type === "receita") { + receitas.push(category); + } else { + despesas.push(category); + } + } + + return { receitas, despesas }; + }, [categories]); return ( - - - - - - Categoria - - {periods.map((period) => ( - - {formatPeriodLabel(period)} - - ))} - - Total - - - +
+ {/* Despesas Table */} + - - {categories.map((category) => { - const Icon = category.icon ? getIconComponent(category.icon) : null; - const isReceita = category.type.toLowerCase() === "receita"; - const dotColor = isReceita - ? "bg-green-600 dark:bg-green-400" - : "bg-red-600 dark:bg-red-400"; - - return ( - - -
- - {Icon && } - {category.name} -
-
- {periods.map((period, periodIndex) => { - const monthData = category.monthlyData.get(period); - const isFirstMonth = periodIndex === 0; - - return ( - - - - ); - })} - - {formatCurrency(category.total)} - -
- ); - })} -
- - - - Total Geral - {periods.map((period) => { - const periodTotal = totals.get(period) ?? 0; - return ( - - {formatCurrency(periodTotal)} - - ); - })} - - {formatCurrency(grandTotal)} - - - -
-
+ {/* Receitas Table */} + +
); } diff --git a/components/relatorios/category-table.tsx b/components/relatorios/category-table.tsx new file mode 100644 index 0000000..6a88464 --- /dev/null +++ b/components/relatorios/category-table.tsx @@ -0,0 +1,150 @@ +"use client"; + +import Link from "next/link"; +import { useMemo } from "react"; +import { CategoryIconBadge } from "@/components/categorias/category-icon-badge"; +import { + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import type { CategoryReportItem } from "@/lib/relatorios/types"; +import { formatCurrency, formatPeriodLabel } from "@/lib/relatorios/utils"; +import { formatPeriodForUrl } from "@/lib/utils/period"; +import DotIcon from "../dot-icon"; +import { Card } from "../ui/card"; +import { CategoryCell } from "./category-cell"; + +export interface CategoryTableProps { + title: string; + categories: CategoryReportItem[]; + periods: string[]; + colorIndexOffset: number; +} + +export function CategoryTable({ + title, + categories, + periods, + colorIndexOffset, +}: CategoryTableProps) { + // Calculate section totals + const sectionTotals = useMemo(() => { + const totalsMap = new Map(); + let grandTotal = 0; + + for (const category of categories) { + grandTotal += category.total; + for (const period of periods) { + const monthData = category.monthlyData.get(period); + const current = totalsMap.get(period) ?? 0; + totalsMap.set(period, current + (monthData?.amount ?? 0)); + } + } + + return { totalsMap, grandTotal }; + }, [categories, periods]); + + if (categories.length === 0) { + return null; + } + + return ( + + + + + + Categoria + + {periods.map((period) => ( + + {formatPeriodLabel(period)} + + ))} + + Total + + + + + + {categories.map((category, index) => { + const colorIndex = colorIndexOffset + index; + const periodParam = formatPeriodForUrl(periods[periods.length - 1]); + + return ( + + +
+ + + + + {category.name} + +
+
+ {periods.map((period, periodIndex) => { + const monthData = category.monthlyData.get(period); + const isFirstMonth = periodIndex === 0; + + return ( + + + + ); + })} + + {formatCurrency(category.total)} + +
+ ); + })} +
+ + + + Total + {periods.map((period) => { + const periodTotal = sectionTotals.totalsMap.get(period) ?? 0; + return ( + + {formatCurrency(periodTotal)} + + ); + })} + + {formatCurrency(sectionTotals.grandTotal)} + + + +
+
+ ); +} diff --git a/components/relatorios/index.ts b/components/relatorios/index.ts index 28b3b0f..42f6a4f 100644 --- a/components/relatorios/index.ts +++ b/components/relatorios/index.ts @@ -5,6 +5,7 @@ export { CategoryReportExport } from "./category-report-export"; export { CategoryReportFilters } from "./category-report-filters"; export { CategoryReportPage } from "./category-report-page"; export { CategoryReportTable } from "./category-report-table"; +export { CategoryTable } from "./category-table"; export type { CategoryOption, CategoryReportFiltersProps, diff --git a/components/sidebar/nav-link.tsx b/components/sidebar/nav-link.tsx index 2c9a15d..f1757a7 100644 --- a/components/sidebar/nav-link.tsx +++ b/components/sidebar/nav-link.tsx @@ -184,12 +184,12 @@ export function createSidebarNavData( }, { title: "Tendências", - url: "/relatorios/categorias", + url: "/relatorios/tendencias", icon: RiFileChartLine, }, { title: "Uso de Cartões", - url: "/relatorios/cartoes", + url: "/relatorios/uso-cartoes", icon: RiBankCard2Line, }, ], diff --git a/components/top-estabelecimentos/top-categories.tsx b/components/top-estabelecimentos/top-categories.tsx index 93ace64..aae24dc 100644 --- a/components/top-estabelecimentos/top-categories.tsx +++ b/components/top-estabelecimentos/top-categories.tsx @@ -1,16 +1,11 @@ "use client"; import { RiPriceTag3Line } from "@remixicon/react"; +import { CategoryIconBadge } from "@/components/categorias/category-icon-badge"; import MoneyValues from "@/components/money-values"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { WidgetEmptyState } from "@/components/widget-empty-state"; import type { TopEstabelecimentosData } from "@/lib/top-estabelecimentos/fetch-data"; -import { - buildCategoryInitials, - getCategoryBgColor, - getCategoryColor, -} from "@/lib/utils/category-colors"; -import { getIconComponent } from "@/lib/utils/icons"; import { title_font } from "@/public/fonts/font_index"; import { Progress } from "../ui/progress"; @@ -56,12 +51,6 @@ export function TopCategories({ categories }: TopCategoriesProps) {
{categories.map((category, index) => { - const IconComponent = category.icon - ? getIconComponent(category.icon) - : null; - const color = getCategoryColor(index); - const bgColor = getCategoryBgColor(index); - const initials = buildCategoryInitials(category.name); const percent = totalAmount > 0 ? (category.totalAmount / totalAmount) * 100 : 0; @@ -72,21 +61,11 @@ export function TopCategories({ categories }: TopCategoriesProps) { >
-
- {IconComponent ? ( - - ) : ( - - {initials} - - )} -
+ {/* Name and percentage */}
@@ -110,7 +89,7 @@ export function TopCategories({ categories }: TopCategoriesProps) {
{/* Progress bar */} -
+