import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { budgetService, categoryService, costCenterService } from '../services/api'; import useFormatters from '../hooks/useFormatters'; import { getCurrencyByCode } from '../config/currencies'; import ConfirmModal from '../components/ConfirmModal'; import BudgetWizard from '../components/BudgetWizard'; const Budgets = () => { const { t } = useTranslation(); const { currency } = useFormatters(); const [loading, setLoading] = useState(true); const [budgets, setBudgets] = useState([]); const [categories, setCategories] = useState([]); const [availableCategories, setAvailableCategories] = useState([]); const [costCenters, setCostCenters] = useState([]); const [year, setYear] = useState(new Date().getFullYear()); const [month, setMonth] = useState(new Date().getMonth() + 1); const [showModal, setShowModal] = useState(false); const [editingBudget, setEditingBudget] = useState(null); const [deleteBudget, setDeleteBudget] = useState(null); const [yearSummary, setYearSummary] = useState(null); const [primaryCurrency, setPrimaryCurrency] = useState('EUR'); const [showWizard, setShowWizard] = useState(false); // Meses con i18n const getMonths = () => [ { value: 1, label: t('months.january') }, { value: 2, label: t('months.february') }, { value: 3, label: t('months.march') }, { value: 4, label: t('months.april') }, { value: 5, label: t('months.may') }, { value: 6, label: t('months.june') }, { value: 7, label: t('months.july') }, { value: 8, label: t('months.august') }, { value: 9, label: t('months.september') }, { value: 10, label: t('months.october') }, { value: 11, label: t('months.november') }, { value: 12, label: t('months.december') }, ]; const months = getMonths(); useEffect(() => { loadData(); }, [year, month]); const loadData = async () => { setLoading(true); try { const [budgetsData, categoriesData, availableData, summaryData, costCentersData] = await Promise.all([ budgetService.getAll({ year, month }), categoryService.getAll(), budgetService.getAvailableCategories({ year, month }), budgetService.getYearSummary({ year }), costCenterService.getAll(), ]); // Extraer datos del response si viene en formato { data, ... } const budgetsList = budgetsData?.data || budgetsData; setBudgets(Array.isArray(budgetsList) ? budgetsList : []); // Detectar moneda primaria de los presupuestos o usar EUR if (budgetsList?.length > 0 && budgetsList[0].currency) { setPrimaryCurrency(budgetsList[0].currency); } const cats = categoriesData?.data || categoriesData; // Filtrar categorías de gastos: expense o both setCategories(Array.isArray(cats) ? cats.filter(c => c.type === 'expense' || c.type === 'both') : []); // Categorías disponibles (no usadas aún) const available = Array.isArray(availableData) ? availableData : []; setAvailableCategories(available); setYearSummary(Array.isArray(summaryData) ? summaryData : []); // Cost Centers const centers = costCentersData?.data || costCentersData; setCostCenters(Array.isArray(centers) ? centers : []); } catch (error) { console.error('Error loading budgets:', error); } finally { setLoading(false); } }; const handleDelete = async () => { if (!deleteBudget) return; try { await budgetService.delete(deleteBudget.id); setDeleteBudget(null); loadData(); } catch (error) { console.error('Error deleting budget:', error); } }; const handleEdit = (budget) => { setEditingBudget(budget); setShowModal(true); }; const handleCopyToNextMonth = async () => { try { await budgetService.copyToNextMonth(year, month); // Move to next month view if (month === 12) { setYear(year + 1); setMonth(1); } else { setMonth(month + 1); } } catch (error) { console.error('Error copying budgets:', error); } }; const openNewBudget = () => { setEditingBudget(null); setShowModal(true); }; const getProgressColor = (percentage) => { if (percentage >= 100) return '#ef4444'; if (percentage >= 80) return '#f59e0b'; if (percentage >= 60) return '#eab308'; return '#10b981'; }; // Calculate totals agrupados por moneda const safeBudgets = Array.isArray(budgets) ? budgets : []; // Agrupar por moneda const totalsByCurrency = safeBudgets.reduce((acc, b) => { const curr = b.currency || primaryCurrency; if (!acc[curr]) { acc[curr] = { budgeted: 0, spent: 0 }; } acc[curr].budgeted += parseFloat(b.amount || 0); acc[curr].spent += parseFloat(b.spent_amount || 0); return acc; }, {}); // Totales principales (para compatibilidad) const totals = { budgeted: safeBudgets.reduce((sum, b) => sum + parseFloat(b.amount || 0), 0), spent: safeBudgets.reduce((sum, b) => sum + parseFloat(b.spent_amount || 0), 0), }; totals.remaining = totals.budgeted - totals.spent; totals.percentage = totals.budgeted > 0 ? (totals.spent / totals.budgeted) * 100 : 0; // Formatear totales por moneda const formatTotalsByCurrency = (type) => { const entries = Object.entries(totalsByCurrency); if (entries.length === 0) return currency(0, primaryCurrency); if (entries.length === 1) { const [curr, vals] = entries[0]; const value = type === 'budgeted' ? vals.budgeted : type === 'spent' ? vals.spent : vals.budgeted - vals.spent; return currency(value, curr); } return entries.map(([curr, vals]) => { const value = type === 'budgeted' ? vals.budgeted : type === 'spent' ? vals.spent : vals.budgeted - vals.spent; return currency(value, curr); }).join(' + '); }; if (loading) { return (
{t('common.loading')}
); } return (
{/* Header */}

{t('budgets.title')}

{t('budgets.subtitle')}

{/* Month/Year Selector */}
{/* Summary Cards */}
{t('budgets.totalBudgeted')}
{formatTotalsByCurrency('budgeted')}
{t('budgets.totalSpent')}
{formatTotalsByCurrency('spent')}
= 0 ? 'linear-gradient(135deg, #10b981 0%, #059669 100%)' : 'linear-gradient(135deg, #f97316 0%, #ea580c 100%)' }} >
{t('budgets.remaining')}
{formatTotalsByCurrency('remaining')}
{t('budgets.usage')}

{totals.percentage.toFixed(1)}%

{/* Budgets List */} {budgets.length === 0 ? (
{t('budgets.noBudgets')}

{t('budgets.noBudgetsDescription')}

) : (
{budgets.map(budget => { const spent = parseFloat(budget.spent_amount || 0); const amount = parseFloat(budget.amount); const percentage = budget.usage_percentage || ((spent / amount) * 100); const remaining = budget.remaining_amount || (amount - spent); const isExceeded = spent > amount; return (
{/* Header */}
{budget.subcategory ? budget.subcategory.name : budget.category?.name || t('budgets.general')}
{budget.subcategory && budget.category && ( {budget.category.name} )} {budget.cost_center && ( {budget.cost_center.name} )}
{/* Progress */}
{t('budgets.spent')} {t('budgets.budgeted')}
{currency(spent, budget.currency || primaryCurrency)} {currency(amount, budget.currency || primaryCurrency)}
{/* Stats */}
{t('budgets.remaining')} = 0 ? 'text-success' : 'text-danger'}`}> {currency(remaining, budget.currency || primaryCurrency)}
{t('budgets.usage')} {percentage.toFixed(1)}%
{/* Warning */} {isExceeded && (
{t('budgets.exceeded')} {currency(Math.abs(remaining), budget.currency || primaryCurrency)}
)} {!isExceeded && percentage >= 80 && (
{t('budgets.almostExceeded')}
)}
); })}
)} {/* Year Summary */} {yearSummary && yearSummary.length > 0 && (
{t('budgets.yearSummary')} {year}
{yearSummary.map(item => { const monthName = months.find(m => m.value === item.month)?.label || item.month; const isCurrentMonth = item.month === new Date().getMonth() + 1 && year === new Date().getFullYear(); return ( setMonth(item.month)} > ); })}
{t('budgets.month')} {t('budgets.budgeted')} {t('budgets.spent')} {t('budgets.remaining')} {t('budgets.usage')}
{monthName} {isCurrentMonth && ( {t('budgets.currentMonth')} )} {currency(item.budgeted, primaryCurrency)} {currency(item.spent, primaryCurrency)} = 0 ? 'text-success' : 'text-danger'}`}> {currency(item.remaining, primaryCurrency)} {item.percentage.toFixed(1)}%
)} {/* Budget Form Modal - Using BudgetWizard with mode='single' */} { setShowModal(false); setEditingBudget(null); }} onSuccess={loadData} year={year} month={month} mode="single" editBudget={editingBudget} /> {/* Delete Confirmation */} setDeleteBudget(null)} onConfirm={handleDelete} title={t('budgets.deleteBudget')} message={t('budgets.deleteConfirm', { category: deleteBudget?.category?.name })} confirmText={t('common.delete')} variant="danger" /> {/* Budget Wizard */} setShowWizard(false)} onSuccess={loadData} year={year} month={month} />
); }; export default Budgets;