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,6 @@
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { useState, useMemo, useCallback, useRef } from 'react';
import { useQuery } from '@tanstack/react-query';
import ErrorState from '@/components/ErrorState';
import {
LineChart,
Line,
@@ -238,9 +240,9 @@ function ChartTooltipContent({
// ─── Main Component ───────────────────────────────────────────────────────────
const EMPTY_SNAPSHOTS: PensionSnapshot[] = [];
export default function Pensions() {
const [fundSummary, setFundSummary] = useState<PensionSnapshot[]>([]);
const [allSnapshots, setAllSnapshots] = useState<PensionSnapshot[]>([]);
const [visibleFunds, setVisibleFunds] = useState<Set<FundKey>>(new Set(FUND_KEYS));
const [projections, setProjections] = useState<Record<FundKey, ProjectionState>>({
FCL: { contribution: 150_000, rate: 7.5, targetAge: 35 },
@@ -254,22 +256,22 @@ export default function Pensions() {
const [showManualEntry, setShowManualEntry] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const loadData = useCallback(async () => {
try {
const pensionQ = useQuery({
queryKey: ['pensions'],
queryFn: async () => {
const [summaryRes, snapshotsRes] = await Promise.all([
getPensionFundSummary(),
getPensionSnapshots(),
]);
setFundSummary(summaryRes.data);
setAllSnapshots(snapshotsRes.data);
} catch {
// API not available or no data yet — use defaults
}
}, []);
useEffect(() => {
loadData();
}, [loadData]);
return { summary: summaryRes.data, snapshots: snapshotsRes.data };
},
});
const fundSummary = pensionQ.data?.summary ?? EMPTY_SNAPSHOTS;
const allSnapshots = pensionQ.data?.snapshots ?? EMPTY_SNAPSHOTS;
const loadData = useCallback(async () => {
await pensionQ.refetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pensionQ.refetch]);
const FUNDS = useMemo(() => applySnapshots(fundSummary), [fundSummary]);
@@ -392,6 +394,12 @@ export default function Pensions() {
return (
<div className="space-y-8">
{pensionQ.isError && (
<ErrorState
message="No se pudieron cargar los datos de pensiones"
onRetry={() => pensionQ.refetch()}
/>
)}
{/* ── Page Header ─────────────────────────────────────────────────── */}
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
@@ -849,7 +857,7 @@ export default function Pensions() {
))}
{uploadResult.snapshots.length > 0 && (
<div className="space-y-1 pt-1">
{uploadResult.snapshots.map((snap) => (
{uploadResult.snapshots.slice(0, 10).map((snap) => (
<div key={snap.id} className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">
{snap.fund} · {new Date(snap.period_start).toLocaleDateString('es-CR', { month: 'short', year: '2-digit' })}
@@ -859,6 +867,11 @@ export default function Pensions() {
<span data-sensitive className="font-mono font-medium">{formatCRC(snap.saldo_final)}</span>
</div>
))}
{uploadResult.snapshots.length > 10 && (
<p className="text-xs text-muted-foreground">
y {uploadResult.snapshots.length - 10} más
</p>
)}
</div>
)}
</div>