Migrate Analytics, Salarios, Pensions, ServiciosMunicipales, Proyecciones to queries

Every page now goes through TanStack Query with cancellation,
placeholder data on filter changes, and a visible ErrorState with retry
instead of console.error/silent-empty rendering (FE-02, UX-02).
Analytics keeps the trend query on its own key so cycle changes don't
refetch it. Pensions/Servicios upload-triggered refreshes are bounded
by the api-level 30s timeout (FE-05); pension upload results cap at 10
rows with an overflow note (FE-19). Note: the Pensions ROI fallback
already guarded short chart data — FE-04 was already handled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-06-10 17:23:52 -06:00
parent 65dffdac06
commit 7a1733bf7e
5 changed files with 139 additions and 88 deletions

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import {
PieChart,
Pie,
@@ -13,6 +14,7 @@ import {
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 {
@@ -70,29 +72,43 @@ const dailyChartConfig = {
export default function Analytics() {
const [cycle, setCycle] = useState<{ year: number; month: number } | null>(null);
const [byCategory, setByCategory] = useState<CategorySpending[]>([]);
const [trend, setTrend] = useState<MonthlyTrend[]>([]);
const [daily, setDaily] = useState<DailySpending[]>([]);
const cycleParams: Record<string, string> = cycle
? { cycle_year: String(cycle.year), cycle_month: String(cycle.month) }
: {};
useEffect(() => {
const params: Record<string, string> = {};
if (cycle) {
params.cycle_year = String(cycle.year);
params.cycle_month = String(cycle.month);
}
const byCategoryQ = useQuery({
queryKey: ['analytics', 'by-category', cycle],
queryFn: ({ signal }) =>
api
.get<CategorySpending[]>('/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<MonthlyTrend[]>('/analytics/monthly-trend', { signal }).then((r) => r.data),
});
const dailyQ = useQuery({
queryKey: ['analytics', 'daily', cycle],
queryFn: ({ signal }) =>
api
.get<DailySpending[]>('/analytics/daily-spending', { params: cycleParams, signal })
.then((r) => r.data),
placeholderData: (prev) => prev,
});
Promise.all([
api.get<CategorySpending[]>('/analytics/by-category', { params }),
api.get<MonthlyTrend[]>('/analytics/monthly-trend'),
api.get<DailySpending[]>('/analytics/daily-spending', { params }),
])
.then(([catRes, trendRes, dailyRes]) => {
setByCategory(catRes.data);
setTrend(trendRes.data);
setDaily(dailyRes.data);
})
.catch(console.error);
}, [cycle]);
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<ChartConfig>((acc, cat, i) => {
@@ -116,6 +132,13 @@ export default function Analytics() {
<BillingCycleSelector value={cycle} onChange={setCycle} />
</div>
{anyError && (
<ErrorState
message="No se pudieron cargar los datos de analytics"
onRetry={retryAll}
/>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Spending by Category - Donut */}
<Card>