"use client"; import { RiArrowDownSFill, RiArrowUpSFill, RiExternalLinkLine, RiPieChartLine, RiWallet3Line, } from "@remixicon/react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useMemo } from "react"; import { Cell, Pie, PieChart, Tooltip } from "recharts"; import { CategoryIconBadge } from "@/components/categorias/category-icon-badge"; import { useIsMobile } from "@/hooks/use-mobile"; 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 { getCategoryColor } from "@/lib/utils/category-colors"; import { formatPeriodForUrl } from "@/lib/utils/period"; import { WidgetEmptyState } from "../widget-empty-state"; type ExpensesByCategoryWidgetWithChartProps = { data: ExpensesByCategoryData; period: string; }; const formatPercentage = (value: number) => { return `${Math.abs(value).toFixed(0)}%`; }; const formatCurrency = (value: number) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL", }).format(value); type ChartDataItem = { category: string; name: string; value: number; percentage: number; fill: string | undefined; href: string | undefined; }; export function ExpensesByCategoryWidgetWithChart({ data, period, }: ExpensesByCategoryWidgetWithChartProps) { const router = useRouter(); const isMobile = useIsMobile(); const periodParam = formatPeriodForUrl(period); // Configuração do chart com as mesmas cores dos ícones das categorias (getCategoryColor) const chartConfig = useMemo(() => { const config: ChartConfig = {}; if (data.categories.length <= 7) { data.categories.forEach((category, index) => { config[category.categoryId] = { label: category.categoryName, color: getCategoryColor(index), }; }); } else { // Top 7 + Outros const top7 = data.categories.slice(0, 7); top7.forEach((category, index) => { config[category.categoryId] = { label: category.categoryName, color: getCategoryColor(index), }; }); config.outros = { label: "Outros", color: getCategoryColor(7), }; } return config; }, [data.categories]); // Preparar dados para o gráfico de pizza - Top 7 + Outros (com href para navegação) const chartData = useMemo((): ChartDataItem[] => { const buildItem = ( categoryId: string, name: string, value: number, percentage: number, fill: string | undefined, ): ChartDataItem => ({ category: categoryId, name, value, percentage, fill, href: categoryId === "outros" ? undefined : `/categorias/${categoryId}?periodo=${periodParam}`, }); if (data.categories.length <= 7) { return data.categories.map((category) => buildItem( category.categoryId, category.categoryName, category.currentAmount, category.percentageOfTotal, chartConfig[category.categoryId]?.color, ), ); } const top7 = data.categories.slice(0, 7); const others = data.categories.slice(7); const othersTotal = others.reduce((sum, cat) => sum + cat.currentAmount, 0); const othersPercentage = others.reduce( (sum, cat) => sum + cat.percentageOfTotal, 0, ); const top7Data = top7.map((category) => buildItem( category.categoryId, category.categoryName, category.currentAmount, category.percentageOfTotal, chartConfig[category.categoryId]?.color, ), ); if (others.length > 0) { top7Data.push( buildItem( "outros", "Outros", othersTotal, othersPercentage, chartConfig.outros?.color, ), ); } return top7Data; }, [data.categories, chartConfig, periodParam]); if (data.categories.length === 0) { return ( } title="Nenhuma despesa encontrada" description="Quando houver despesas registradas, elas aparecerão aqui." /> ); } return (
{/* Gráfico de pizza (donut) — fatias clicáveis */}
{ if (payload?.href) router.push(payload.href); }} label={(props: { cx?: number; cy?: number; midAngle?: number; innerRadius?: number; outerRadius?: number; percent?: number; }) => { const { cx = 0, cy = 0, midAngle = 0, innerRadius = 0, outerRadius = 0, percent = 0 } = props; const percentage = percent * 100; if (percentage < 6) return null; const radius = (Number(innerRadius) + Number(outerRadius)) / 2; const x = cx + radius * Math.cos(-midAngle * (Math.PI / 180)); const y = cy + radius * Math.sin(-midAngle * (Math.PI / 180)); return ( {formatPercentage(percentage)} ); }} labelLine={false} > {chartData.map((entry, index) => ( ))} {!isMobile && ( { if (active && payload?.length) { const d = payload[0].payload as ChartDataItem; return (
{d.name} {formatCurrency(d.value)} {formatPercentage(d.percentage)} do total {d.href && ( Clique para ver detalhes )}
); } return null; }} cursor={false} /> )}
{/* Legenda clicável */}
{chartData.map((entry, index) => { const content = ( <> {entry.name} {formatPercentage(entry.percentage)} ); return entry.href ? ( {content} ) : (
{content}
); })}
{/* Lista de categorias */}
{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; return (
{category.categoryName}
{formatPercentage(category.percentageOfTotal)} da despesa total
{category.percentageChange !== null && ( {hasIncrease && } {hasDecrease && } {formatPercentage(category.percentageChange)} )}
{hasBudget && category.budgetUsedPercentage !== null && (
{budgetExceeded ? ( <> {formatPercentage(category.budgetUsedPercentage)} do limite - excedeu em {formatCurrency(exceededAmount)} ) : ( <> {formatPercentage(category.budgetUsedPercentage)} do limite )}
)}
); })}
); }