import { useState, useCallback } from 'react'; import { ArrowLeft, Calculator, Download } from 'lucide-react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import api, { type Transaction } from '@/lib/api'; import { MONTH_NAMES_ES as MONTH_NAMES, MONTH_NAMES_ES_SHORT as MONTH_NAMES_SHORT, } from '@/lib/dates'; import { useBudget } from '@/hooks/useBudget'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import ErrorState from '@/components/ErrorState'; import PeriodNavigator from '@/components/PeriodNavigator'; import MonthlyDetail from '@/components/budget/MonthlyDetail'; import RecurringItemsManager from '@/components/budget/RecurringItemsManager'; import TransactionList from '@/components/TransactionList'; import ConvertToInstallmentsDialog from '@/components/transactions/ConvertToInstallmentsDialog'; const MIN_YEAR = 2026; const MAX_YEAR = 2030; export default function Budget() { const currentYear = Math.max(MIN_YEAR, new Date().getFullYear()); const location = useLocation(); const navigate = useNavigate(); // Proyecciones row clicks land here with the clicked period in the state. const navState = location.state as | { from?: string; year?: number; month?: number } | null; const cameFromProyecciones = navState?.from === 'proyecciones'; const initialYear = navState?.year && navState.year >= MIN_YEAR && navState.year <= MAX_YEAR ? navState.year : currentYear; const { year, setYear, selectedMonth, setSelectedMonth, monthDetail, recurringItems, monthLoading, error: budgetError, addItem, updateItem, deleteItem, refresh, } = useBudget(initialYear, navState?.month); const queryClient = useQueryClient(); const [subTab, setSubTab] = useState<'detail' | 'transactions'>('detail'); const [txSearch, setTxSearch] = useState(''); const [txSource, setTxSource] = useState<'CREDIT_CARD' | 'CASH_AND_TRANSFER'>('CREDIT_CARD'); const [convertTx, setConvertTx] = useState(null); const txQuery = useQuery({ queryKey: ['transactions', 'budget', year, selectedMonth, txSource, txSearch], queryFn: async ({ signal }) => { const params: Record = { search: txSearch || undefined, limit: 200, }; if (txSource === 'CREDIT_CARD') { params.source = 'CREDIT_CARD'; // Credit card: billing cycle that ends around the 18th of selectedMonth const prevMonth = selectedMonth === 1 ? 12 : selectedMonth - 1; const prevYear = selectedMonth === 1 ? year - 1 : year; params.cycle_year = prevYear; params.cycle_month = prevMonth; } else { // Cash + Transfer merged: calendar month, exclude credit card params.exclude_source = 'CREDIT_CARD'; const startDate = `${year}-${String(selectedMonth).padStart(2, '0')}-01`; const endMonth = selectedMonth === 12 ? 1 : selectedMonth + 1; const endYear = selectedMonth === 12 ? year + 1 : year; const endDate = `${endYear}-${String(endMonth).padStart(2, '0')}-01`; params.start_date = startDate; params.end_date = endDate; } const { data } = await api.get('/transactions/', { params, signal }); const INCOME_TYPES = ['DEPOSITO', 'SALARY']; return data.filter((tx) => !INCOME_TYPES.includes(tx.transaction_type)); }, placeholderData: (prev) => prev, }); const transactions = txQuery.data ?? []; // Make the period explicit: budget month M on the card tab = the billing // cycle 18/(M-1) → 18/M; cash/transfers use the calendar month. const cyclePrevMonth = selectedMonth === 1 ? 12 : selectedMonth - 1; const cycleLabel = txSource === 'CREDIT_CARD' ? `Ciclo 18 ${MONTH_NAMES_SHORT[cyclePrevMonth].toLowerCase()} – 18 ${MONTH_NAMES_SHORT[selectedMonth].toLowerCase()}` : `Mes de ${MONTH_NAMES[selectedMonth].toLowerCase()}`; const invalidateTransactionsAndBudget = useCallback(() => { queryClient.invalidateQueries({ queryKey: ['transactions'] }); queryClient.invalidateQueries({ queryKey: ['installment-plans'] }); refresh(); }, [queryClient, refresh]); const handleToggleDeferred = useCallback(async (tx: Transaction) => { try { await api.patch(`/transactions/${tx.id}`, { deferred_to_next_cycle: !tx.deferred_to_next_cycle, }); toast.success( tx.deferred_to_next_cycle ? 'Transacción restaurada a su ciclo original' : 'Transacción diferida al siguiente ciclo', ); } catch { toast.error('No se pudo actualizar la transacción'); return; } invalidateTransactionsAndBudget(); }, [invalidateTransactionsAndBudget]); const handleNavigateToTransactions = useCallback(() => { setTxSource('CASH_AND_TRANSFER'); setSubTab('transactions'); }, []); return (
{/* Header */}
{cameFromProyecciones && ( )}

Presupuesto

setYear(year - 1)} onNext={() => setYear(year + 1)} prevDisabled={year <= MIN_YEAR} nextDisabled={year >= MAX_YEAR} />
Resumen Items Recurrentes setSubTab(v as typeof subTab)} >
Detalle Transacciones setSelectedMonth(selectedMonth - 1)} onNext={() => setSelectedMonth(selectedMonth + 1)} prevDisabled={selectedMonth <= 1} nextDisabled={selectedMonth >= 12} />
{budgetError ? ( ) : ( )} {txQuery.isError ? ( txQuery.refetch()} /> ) : ( setTxSource(v as typeof txSource)} > Tarjeta Efectivo y Transferencias
} toolbarRight={ } /> )}
!open && setConvertTx(null)} onSaved={invalidateTransactionsAndBudget} tx={convertTx} />
); }