mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 15:28:47 +02:00
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>
120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
import { ChevronLeft, ChevronRight, Loader2, TrendingUp } from 'lucide-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
import { useBudget } from '@/hooks/useBudget';
|
|
import { formatAmount } from '@/lib/format';
|
|
import { cn } from '@/lib/utils';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import ErrorState from '@/components/ErrorState';
|
|
import YearlyOverview from '@/components/budget/YearlyOverview';
|
|
|
|
const MIN_YEAR = 2026;
|
|
const MAX_YEAR = 2030;
|
|
|
|
export default function Proyecciones() {
|
|
const currentYear = Math.max(MIN_YEAR, new Date().getFullYear());
|
|
const {
|
|
year,
|
|
setYear,
|
|
setSelectedMonth,
|
|
projection,
|
|
loading,
|
|
error,
|
|
refresh,
|
|
saveBalanceOverride,
|
|
clearBalanceOverride,
|
|
} = useBudget(currentYear);
|
|
|
|
const navigate = useNavigate();
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<TrendingUp className="w-6 h-6 text-primary" />
|
|
<h1 className="text-2xl font-bold tracking-tight">Proyecciones</h1>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Button variant="outline" size="icon" disabled={year <= MIN_YEAR} onClick={() => setYear(year - 1)}>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
</Button>
|
|
<span className="w-16 text-center font-semibold tabular-nums">{year}</span>
|
|
<Button variant="outline" size="icon" disabled={year >= MAX_YEAR} onClick={() => setYear(year + 1)}>
|
|
<ChevronRight className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Annual summary cards */}
|
|
{projection && (
|
|
<div className="grid gap-3 grid-cols-1 md:grid-cols-3">
|
|
<Card>
|
|
<CardContent className="pt-4 pb-3 px-4">
|
|
<p className="text-xs text-muted-foreground">Ingresos Anuales</p>
|
|
<p data-sensitive className="text-lg font-bold font-mono text-primary">
|
|
{formatAmount(projection.annual_income, 'CRC')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="pt-4 pb-3 px-4">
|
|
<p className="text-xs text-muted-foreground">Egresos Anuales</p>
|
|
<p data-sensitive className="text-lg font-bold font-mono">
|
|
{formatAmount(projection.annual_expenses, 'CRC')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="pt-4 pb-3 px-4">
|
|
<p className="text-xs text-muted-foreground">Balance Neto Anual</p>
|
|
<p
|
|
data-sensitive
|
|
className={cn(
|
|
'text-lg font-bold font-mono',
|
|
projection.annual_net >= 0 ? 'text-primary' : 'text-destructive',
|
|
)}
|
|
>
|
|
{projection.annual_net >= 0 ? '+' : ''}
|
|
{formatAmount(projection.annual_net, 'CRC')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Yearly table */}
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : projection ? (
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
{error ? (
|
|
<ErrorState onRetry={refresh} />
|
|
) : (
|
|
<YearlyOverview
|
|
months={projection.months}
|
|
selectedMonth={0}
|
|
year={year}
|
|
onSelectMonth={(m) => {
|
|
setSelectedMonth(m);
|
|
navigate('/budget');
|
|
}}
|
|
onSaveOverride={async (month, value) => {
|
|
await saveBalanceOverride(year, month, value);
|
|
}}
|
|
onClearOverride={async (month) => {
|
|
await clearBalanceOverride(year, month);
|
|
}}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|