mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 09:28:47 +02:00
All checks were successful
Deploy to VPS / deploy (push) Successful in 34s
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>
98 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|