mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 08:28:48 +02:00
Repoints imports at the relocated lib/api and src/contexts modules, and refreshes Layout + Login alongside the rest of the migration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
208 lines
7.3 KiB
TypeScript
208 lines
7.3 KiB
TypeScript
import { useState, useRef, useEffect } from 'react';
|
|
import { Pencil } from 'lucide-react';
|
|
|
|
import { type MonthlyProjection } from '@/lib/api';
|
|
import { formatAmount } from '@/lib/format';
|
|
import { cn } from '@/lib/utils';
|
|
import { Input } from '@/components/ui/input';
|
|
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',
|
|
];
|
|
|
|
const FRESH_START_YEAR = 2026;
|
|
const FRESH_START_MONTH = 3;
|
|
|
|
interface YearlyOverviewProps {
|
|
months: MonthlyProjection[];
|
|
selectedMonth: number;
|
|
year: number;
|
|
onSelectMonth: (month: number) => void;
|
|
onSaveOverride: (month: number, value: number) => Promise<void>;
|
|
onClearOverride: (month: number) => Promise<void>;
|
|
}
|
|
|
|
export default function YearlyOverview({
|
|
months,
|
|
selectedMonth,
|
|
year,
|
|
onSelectMonth,
|
|
onSaveOverride,
|
|
onClearOverride,
|
|
}: YearlyOverviewProps) {
|
|
const currentMonth = new Date().getMonth() + 1;
|
|
const currentYear = new Date().getFullYear();
|
|
const [editingMonth, setEditingMonth] = useState<number | null>(null);
|
|
const [editValue, setEditValue] = useState('');
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (editingMonth !== null && inputRef.current) {
|
|
inputRef.current.focus();
|
|
inputRef.current.select();
|
|
}
|
|
}, [editingMonth]);
|
|
|
|
const handleStartEdit = (m: MonthlyProjection) => {
|
|
setEditingMonth(m.month);
|
|
setEditValue(String(Math.round(m.cumulative_balance)));
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (editingMonth === null) return;
|
|
const trimmed = editValue.trim();
|
|
if (trimmed === '') {
|
|
await onClearOverride(editingMonth);
|
|
} else {
|
|
const num = parseFloat(trimmed);
|
|
if (!isNaN(num)) {
|
|
await onSaveOverride(editingMonth, num);
|
|
}
|
|
}
|
|
setEditingMonth(null);
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
handleSave();
|
|
} else if (e.key === 'Escape') {
|
|
setEditingMonth(null);
|
|
}
|
|
};
|
|
|
|
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">Acum. Anterior</TableHead>
|
|
<TableHead className="text-right">Neto Mes</TableHead>
|
|
<TableHead className="text-right">Balance Acum.</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{months.map((m) => {
|
|
const isSelected = m.month === selectedMonth;
|
|
const isCurrent = m.month === currentMonth && m.year === currentYear;
|
|
const isBeforeFreshStart =
|
|
year === FRESH_START_YEAR && m.month < FRESH_START_MONTH;
|
|
const isEditing = editingMonth === m.month;
|
|
|
|
return (
|
|
<TableRow
|
|
key={m.month}
|
|
className={cn(
|
|
'cursor-pointer transition-colors',
|
|
isSelected && 'bg-accent',
|
|
isCurrent && !isSelected && 'bg-accent/40',
|
|
isBeforeFreshStart && 'opacity-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 data-sensitive className="text-right font-mono text-sm text-primary">
|
|
{formatAmount(m.projected_income, 'CRC')}
|
|
</TableCell>
|
|
<TableCell data-sensitive className="text-right font-mono text-sm">
|
|
{formatAmount(m.projected_fixed_expenses, 'CRC')}
|
|
</TableCell>
|
|
<TableCell data-sensitive className="text-right font-mono text-sm text-muted-foreground">
|
|
{formatAmount(m.uncovered_actual, 'CRC')}
|
|
</TableCell>
|
|
<TableCell data-sensitive className="text-right font-mono text-sm font-medium">
|
|
{formatAmount(m.gran_total_egresos, 'CRC')}
|
|
</TableCell>
|
|
<TableCell
|
|
className={cn(
|
|
'text-right font-mono text-sm',
|
|
m.carryover_balance >= 0
|
|
? 'text-muted-foreground'
|
|
: 'text-destructive',
|
|
)}
|
|
>
|
|
{isBeforeFreshStart
|
|
? '—'
|
|
: <span data-sensitive>
|
|
{m.carryover_balance >= 0 ? '+' : ''}
|
|
{formatAmount(m.carryover_balance, 'CRC')}
|
|
</span>
|
|
}
|
|
</TableCell>
|
|
<TableCell
|
|
data-sensitive
|
|
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>
|
|
<TableCell
|
|
className="text-right font-mono text-sm font-semibold p-0 pr-2"
|
|
onClick={(e) => {
|
|
if (isBeforeFreshStart) return;
|
|
e.stopPropagation();
|
|
if (!isEditing) handleStartEdit(m);
|
|
}}
|
|
>
|
|
{isBeforeFreshStart ? (
|
|
<span className="px-2">—</span>
|
|
) : isEditing ? (
|
|
<Input
|
|
ref={inputRef}
|
|
type="number"
|
|
value={editValue}
|
|
onChange={(e) => setEditValue(e.target.value)}
|
|
onBlur={handleSave}
|
|
onKeyDown={handleKeyDown}
|
|
className="h-7 w-36 text-right font-mono text-sm ml-auto"
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
) : (
|
|
<span
|
|
className={cn(
|
|
'inline-flex items-center gap-1 px-2 py-0.5 rounded cursor-pointer hover:bg-muted/50',
|
|
m.cumulative_balance >= 0
|
|
? 'text-primary'
|
|
: 'text-destructive',
|
|
)}
|
|
>
|
|
{m.balance_overridden && (
|
|
<Pencil className="w-3 h-3 text-amber-500 shrink-0" />
|
|
)}
|
|
<span data-sensitive>
|
|
{m.cumulative_balance >= 0 ? '+' : ''}
|
|
{formatAmount(m.cumulative_balance, 'CRC')}
|
|
</span>
|
|
</span>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
}
|