"use client"; import { RiArrowDownSFill, RiArrowUpSFill, RiExternalLinkLine, RiListUnordered, RiPieChart2Line, RiPieChartLine, RiWallet3Line, } from "@remixicon/react"; 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/shared/money-values"; import { WidgetEmptyState } from "@/components/shared/widget-empty-state"; import { type ChartConfig, ChartContainer } from "@/components/ui/chart"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { DashboardCategoryBreakdownData } from "@/lib/dashboard/categories/category-breakdown"; import { formatCurrency } from "@/lib/lancamentos/formatting-helpers"; import { formatPercentage as formatPercentageValue } from "@/lib/utils/percentage"; import { formatPeriodForUrl } from "@/lib/utils/period"; type CategoryBreakdownVariant = "income" | "expense"; type CategoryBreakdownWidgetViewProps = { data: DashboardCategoryBreakdownData; period: string; variant: CategoryBreakdownVariant; }; const CATEGORY_BREAKDOWN_COLORS = [ "var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)", "var(--chart-1)", "var(--chart-2)", ]; const VARIANT_CONFIG = { income: { emptyTitle: "Nenhuma receita encontrada", emptyDescription: "Quando houver receitas registradas, elas aparecerão aqui.", shareLabel: "receita total", percentageDigits: 1, changeClassName: { increase: "text-success", decrease: "text-destructive", }, listItemClassName: "flex flex-col gap-1.5 py-2 border-b border-dashed last:border-0", includeBudgetAmount: true, }, expense: { emptyTitle: "Nenhuma despesa encontrada", emptyDescription: "Quando houver despesas registradas, elas aparecerão aqui.", shareLabel: "despesa total", percentageDigits: 0, changeClassName: { increase: "text-destructive", decrease: "text-success", }, listItemClassName: "flex flex-col py-2 border-b border-dashed last:border-0", includeBudgetAmount: false, }, } as const; const formatPercentage = (value: number, digits: number) => formatPercentageValue(value, { minimumFractionDigits: digits, maximumFractionDigits: digits, absolute: true, }); export function CategoryBreakdownWidgetView({ data, period, variant, }: CategoryBreakdownWidgetViewProps) { const [activeTab, setActiveTab] = useState<"list" | "chart">("list"); const periodParam = formatPeriodForUrl(period); const config = VARIANT_CONFIG[variant]; const chartConfig = useMemo(() => { const nextConfig: ChartConfig = {}; if (data.categories.length <= 7) { data.categories.forEach((category, index) => { nextConfig[category.categoryId] = { label: category.categoryName, color: CATEGORY_BREAKDOWN_COLORS[index % CATEGORY_BREAKDOWN_COLORS.length], }; }); } else { const topCategories = data.categories.slice(0, 7); topCategories.forEach((category, index) => { nextConfig[category.categoryId] = { label: category.categoryName, color: CATEGORY_BREAKDOWN_COLORS[index % CATEGORY_BREAKDOWN_COLORS.length], }; }); nextConfig.outros = { label: "Outros", color: "var(--chart-6)", }; } return nextConfig; }, [data.categories]); const chartData = useMemo(() => { if (data.categories.length <= 7) { return data.categories.map((category) => ({ category: category.categoryId, name: category.categoryName, value: category.currentAmount, percentage: category.percentageOfTotal, fill: chartConfig[category.categoryId]?.color, })); } const topCategories = data.categories.slice(0, 7); const otherCategories = data.categories.slice(7); const otherTotal = otherCategories.reduce( (sum, category) => sum + category.currentAmount, 0, ); const otherPercentage = otherCategories.reduce( (sum, category) => sum + category.percentageOfTotal, 0, ); const groupedData = topCategories.map((category) => ({ category: category.categoryId, name: category.categoryName, value: category.currentAmount, percentage: category.percentageOfTotal, fill: chartConfig[category.categoryId]?.color, })); if (otherCategories.length > 0) { groupedData.push({ category: "outros", name: "Outros", value: otherTotal, percentage: otherPercentage, fill: chartConfig.outros?.color, }); } return groupedData; }, [data.categories, chartConfig]); if (data.categories.length === 0) { return ( } title={config.emptyTitle} description={config.emptyDescription} /> ); } return ( setActiveTab(value as "list" | "chart")} className="w-full" >
Lista Gráfico
{data.categories.map((category, index) => { const hasIncrease = category.percentageChange !== null && category.percentageChange > 0; const hasDecrease = category.percentageChange !== null && category.percentageChange < 0; const hasBudget = category.budgetAmount !== null; const budgetExceeded = hasBudget && category.budgetUsedPercentage !== null && category.budgetUsedPercentage > 100; const exceededAmount = budgetExceeded && category.budgetAmount ? category.currentAmount - category.budgetAmount : 0; const changeClassName = hasIncrease ? config.changeClassName.increase : hasDecrease ? config.changeClassName.decrease : "text-muted-foreground"; return (
{category.categoryName}
{formatPercentage( category.percentageOfTotal, config.percentageDigits, )}{" "} da {config.shareLabel}
{category.percentageChange !== null ? ( {hasIncrease ? ( ) : null} {hasDecrease ? ( ) : null} {formatPercentage( category.percentageChange, config.percentageDigits, )} ) : null}
{hasBudget && category.budgetUsedPercentage !== null ? (
{budgetExceeded ? ( <> {formatPercentage( category.budgetUsedPercentage, config.percentageDigits, )}{" "} do limite {config.includeBudgetAmount && category.budgetAmount !== null ? ` ${formatCurrency(category.budgetAmount)}` : ""}{" "} - excedeu em {formatCurrency(exceededAmount)} ) : ( <> {formatPercentage( category.budgetUsedPercentage, config.percentageDigits, )}{" "} do limite {config.includeBudgetAmount && category.budgetAmount !== null ? ` ${formatCurrency(category.budgetAmount)}` : ""} )}
) : null}
); })}
formatPercentage( (payload as { percentage?: number } | undefined) ?.percentage ?? 0, config.percentageDigits, ) } outerRadius={75} dataKey="value" nameKey="category" /> { if (!active || !payload?.length) { return null; } const entry = payload[0]?.payload; if (!entry) { return null; } return (
{entry.name} {formatCurrency(entry.value)} {formatPercentage( entry.percentage, config.percentageDigits, )}{" "} do total
); }} />
{chartData.map((entry, index) => (
{entry.name}
))}
); }