import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, LineChart, Line, } from 'recharts'; import { BarChart3 } from 'lucide-react'; import api from '@/lib/api'; import ErrorState from '@/components/ErrorState'; import BillingCycleSelector from '@/components/BillingCycleSelector'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig, } from '@/components/ui/chart'; interface CategorySpending { category_id: number | null; category_name: string; total: number; count: number; percentage: number; } interface MonthlyTrend { year: number; month: number; label: string; total_crc: number; total_usd: number; count: number; } interface DailySpending { date: string; total: number; count: number; } const COLORS = [ '#B45309', '#16A34A', '#2563EB', '#DC2626', '#7C3AED', '#D97706', '#0F766E', '#DB2777', '#EA580C', '#4F46E5', ]; function formatCRC(value: number) { return `₡${Math.round(value).toLocaleString('es-CR')}`; } const trendChartConfig = { total_crc: { label: 'Total CRC', color: 'var(--chart-1)', }, } satisfies ChartConfig; const dailyChartConfig = { total: { label: 'Gasto Diario', color: 'var(--chart-2)', }, } satisfies ChartConfig; export default function Analytics() { const [cycle, setCycle] = useState<{ year: number; month: number } | null>(null); const cycleParams: Record = cycle ? { cycle_year: String(cycle.year), cycle_month: String(cycle.month) } : {}; const byCategoryQ = useQuery({ queryKey: ['analytics', 'by-category', cycle], queryFn: ({ signal }) => api .get('/analytics/by-category', { params: cycleParams, signal }) .then((r) => r.data), placeholderData: (prev) => prev, }); // The trend endpoint ignores the cycle filter — keep it on its own key so // changing cycles doesn't refetch it. const trendQ = useQuery({ queryKey: ['analytics', 'trend'], queryFn: ({ signal }) => api.get('/analytics/monthly-trend', { signal }).then((r) => r.data), }); const dailyQ = useQuery({ queryKey: ['analytics', 'daily', cycle], queryFn: ({ signal }) => api .get('/analytics/daily-spending', { params: cycleParams, signal }) .then((r) => r.data), placeholderData: (prev) => prev, }); const byCategory = byCategoryQ.data ?? []; const trend = trendQ.data ?? []; const daily = dailyQ.data ?? []; const anyError = byCategoryQ.isError || trendQ.isError || dailyQ.isError; const retryAll = () => { byCategoryQ.refetch(); trendQ.refetch(); dailyQ.refetch(); }; // Build dynamic chart config for pie chart const pieChartConfig = byCategory.reduce((acc, cat, i) => { acc[cat.category_name] = { label: cat.category_name, color: COLORS[i % COLORS.length], }; return acc; }, {}); return (

Analytics

Desglose y tendencias de gasto

{anyError && ( )}
{/* Spending by Category - Donut */} Spending by Category {byCategory.length === 0 ? (
Sin datos para este período
) : (
{byCategory.map((_, i) => ( ))} formatCRC(Number(value))} /> } /> {/* Legend */}
{byCategory.slice(0, 10).map((cat, i) => (
{cat.category_name} {cat.percentage}%
))}
)} {/* Monthly Trend - Bar */} Monthly Spending (CRC) {trend.length === 0 ? (
No data
) : ( `₡${(v / 1000).toFixed(0)}k`} /> formatCRC(Number(value))} /> } /> )}
{/* Daily Spending - Line */} Daily Spending {daily.length === 0 ? (
Sin datos para este período
) : ( { const d = new Date(v); return `${d.getMonth() + 1}/${d.getDate()}`; }} /> `₡${(v / 1000).toFixed(0)}k`} /> formatCRC(Number(value))} labelFormatter={(label) => new Date(label).toLocaleDateString('es-CR', { month: 'short', day: 'numeric' }) } /> } /> )}
{/* Top categories summary */} {byCategory.length > 0 && ( Top Categories
{byCategory.slice(0, 8).map((cat, i) => (
{cat.category_name} {cat.count} txns {formatCRC(cat.total)}
))}
)}
); }