Files
WealthySmart/frontend/src/components/budget/YearlyOverview.tsx
Carlos Escalante 8d76059ae8
All checks were successful
Deploy to VPS / deploy (push) Successful in 34s
Add budget module and push notifications for transactions
Budget: recurring items CRUD, yearly/monthly projections with no-double-count
logic, and full UI (overview, monthly detail, recurring items manager).

Push notifications: Web Push via VAPID keys, triggered on transaction creation
from n8n. Includes service worker handlers, frontend subscription flow, and
a test button on the Dashboard (temporary).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:28:14 -06:00

98 lines
3.4 KiB
TypeScript

import { type MonthlyProjection } from '@/api';
import { formatAmount } from '@/lib/format';
import { cn } from '@/lib/utils';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
const MONTH_NAMES = [
'', 'Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun',
'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic',
];
interface YearlyOverviewProps {
months: MonthlyProjection[];
selectedMonth: number;
onSelectMonth: (month: number) => void;
}
export default function YearlyOverview({
months,
selectedMonth,
onSelectMonth,
}: YearlyOverviewProps) {
const currentMonth = new Date().getMonth() + 1;
const currentYear = new Date().getFullYear();
return (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Mes</TableHead>
<TableHead className="text-right">Ingresos</TableHead>
<TableHead className="text-right">Egresos Fijos</TableHead>
<TableHead className="text-right">Otros Gastos</TableHead>
<TableHead className="text-right">Gran Total</TableHead>
<TableHead className="text-right">Ahorro</TableHead>
<TableHead className="text-right">Balance</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{months.map((m) => {
const isSelected = m.month === selectedMonth;
const isCurrent = m.month === currentMonth && m.year === currentYear;
return (
<TableRow
key={m.month}
className={cn(
'cursor-pointer transition-colors',
isSelected && 'bg-accent',
isCurrent && !isSelected && 'bg-accent/40',
)}
onClick={() => onSelectMonth(m.month)}
>
<TableCell className="font-medium">
{MONTH_NAMES[m.month]}
{isCurrent && (
<span className="ml-1.5 inline-block w-1.5 h-1.5 rounded-full bg-primary" />
)}
</TableCell>
<TableCell className="text-right font-mono text-sm text-primary">
{formatAmount(m.projected_income, 'CRC')}
</TableCell>
<TableCell className="text-right font-mono text-sm">
{formatAmount(m.projected_fixed_expenses, 'CRC')}
</TableCell>
<TableCell className="text-right font-mono text-sm text-muted-foreground">
{formatAmount(m.uncovered_actual, 'CRC')}
</TableCell>
<TableCell className="text-right font-mono text-sm font-medium">
{formatAmount(m.gran_total_egresos, 'CRC')}
</TableCell>
<TableCell className="text-right font-mono text-sm text-muted-foreground">
{formatAmount(m.projected_savings, 'CRC')}
</TableCell>
<TableCell
className={cn(
'text-right font-mono text-sm font-semibold',
m.net_balance >= 0 ? 'text-primary' : 'text-destructive',
)}
>
{m.net_balance >= 0 ? '+' : ''}
{formatAmount(m.net_balance, 'CRC')}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}