v1.32.1 - Fix Reports category totals bug, fix overdue endpoint, configure weekly server updates

This commit is contained in:
marcoitaloesp-ai 2025-12-14 18:53:46 +00:00 committed by GitHub
parent 39de07bf96
commit 99be24e309
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 5171 additions and 1042 deletions

View File

@ -5,6 +5,33 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
## [1.32.1] - 2025-12-14
### Fixed
- **Reportes por Categoría** - Corregido bug de agrupación que mostraba totales incorrectos
- El total de "Garagem" mostraba 3062.88€ (todo "Transporte") en lugar de 1201.25€
- La query agrupaba por parent_id pero mostraba nombre de subcategoría
- Ahora cada subcategoría muestra su total individual correcto
- Añadido parámetro opcional `group_by_parent=true` para agrupar por categoría padre
- **Endpoint `/api/reports/overdue`** - Corregido error 500
- Cambiado `li.amount` a `li.installment_amount` (nombre correcto de la columna)
- Añadida condición `deleted_at IS NULL` para excluir registros eliminados
### Changed
- **Servidor actualizado** - Aplicadas 12 actualizaciones de seguridad
- Kernel Linux 6.8.0-90
- Nginx 1.29.4
- PHP-common actualizado
- AppArmor 4.0.1
- **Actualizaciones automáticas configuradas**
- Unattended-upgrades habilitado
- Actualizaciones semanales (cada 7 días)
- Auto-limpieza cada 30 días
- Incluye: Ubuntu security, updates, Nginx, PHP PPA
## [1.32.0] - 2025-12-14
### Added

View File

@ -1 +1 @@
1.32.0
1.32.1

View File

@ -47,7 +47,7 @@ public function index(Request $request)
}
/**
* Crear un presupuesto
* Crear un presupuesto (con propagación automática a meses futuros)
*/
public function store(Request $request)
{
@ -77,10 +77,46 @@ public function store(Request $request)
$validated['user_id'] = Auth::id();
// Crear el presupuesto del mes actual
$budget = Budget::create($validated);
// Propagar automáticamente a los 12 meses siguientes
$currentYear = $validated['year'];
$currentMonth = $validated['month'];
for ($i = 1; $i <= 12; $i++) {
$nextMonth = $currentMonth + $i;
$nextYear = $currentYear;
if ($nextMonth > 12) {
$nextMonth -= 12;
$nextYear++;
}
// Solo crear si no existe
$existsNext = Budget::forUser(Auth::id())
->where('category_id', $validated['category_id'])
->where('year', $nextYear)
->where('month', $nextMonth)
->exists();
if (!$existsNext) {
Budget::create([
'user_id' => Auth::id(),
'category_id' => $validated['category_id'],
'name' => $validated['name'] ?? null,
'amount' => $validated['amount'],
'currency' => $validated['currency'] ?? null,
'year' => $nextYear,
'month' => $nextMonth,
'period_type' => $validated['period_type'] ?? 'monthly',
'notes' => $validated['notes'] ?? null,
]);
}
}
return response()->json([
'message' => 'Presupuesto creado',
'message' => 'Presupuesto creado y propagado',
'data' => $budget->load('category'),
], 201);
}
@ -140,15 +176,30 @@ public function update(Request $request, $id)
}
/**
* Eliminar un presupuesto
* Eliminar un presupuesto (y de meses futuros)
*/
public function destroy($id)
{
$budget = Budget::forUser(Auth::id())->findOrFail($id);
$budget->delete();
$categoryId = $budget->category_id;
$year = $budget->year;
$month = $budget->month;
// Eliminar este y todos los futuros de la misma categoría
Budget::forUser(Auth::id())
->where('category_id', $categoryId)
->where(function($q) use ($year, $month) {
$q->where('year', '>', $year)
->orWhere(function($q2) use ($year, $month) {
$q2->where('year', $year)
->where('month', '>=', $month);
});
})
->delete();
return response()->json([
'message' => 'Presupuesto eliminado',
'message' => 'Presupuesto eliminado (incluyendo meses futuros)',
]);
}
@ -184,16 +235,16 @@ public function availableCategories(Request $request)
$year = $request->get('year', now()->year);
$month = $request->get('month', now()->month);
// Obtener categorías de gasto (debit) del usuario
// Obtener IDs de categorías ya usadas en el período
$usedCategoryIds = Budget::forUser(Auth::id())
->forPeriod($year, $month)
->pluck('category_id')
->toArray();
// Categorías padre con tipo debit
// Categorías padre con tipo expense o both (gastos)
$categories = Category::where('user_id', Auth::id())
->whereNull('parent_id')
->where('type', 'debit')
->whereIn('type', ['expense', 'both'])
->whereNotIn('id', $usedCategoryIds)
->orderBy('name')
->get();
@ -222,20 +273,13 @@ public function yearSummary(Request $request)
$monthlyData[] = [
'month' => $month,
'month_name' => Carbon::createFromDate($year, $month, 1)->format('M'),
'budget' => $totalBudget,
'spent' => $totalSpent,
'remaining' => $totalBudget - $totalSpent,
'usage_percentage' => $totalBudget > 0 ? round(($totalSpent / $totalBudget) * 100, 1) : 0,
'budgeted' => round($totalBudget, 2),
'spent' => round($totalSpent, 2),
'remaining' => round($totalBudget - $totalSpent, 2),
'percentage' => $totalBudget > 0 ? round(($totalSpent / $totalBudget) * 100, 1) : 0,
];
}
return response()->json([
'year' => $year,
'monthly' => $monthlyData,
'totals' => [
'budget' => array_sum(array_column($monthlyData, 'budget')),
'spent' => array_sum(array_column($monthlyData, 'spent')),
],
]);
return response()->json($monthlyData);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

0
backend/deploy.sh Normal file → Executable file
View File

View File

@ -257,7 +257,9 @@
// ============================================
Route::prefix('reports')->group(function () {
Route::get('summary', [ReportController::class, 'summary']);
Route::get('executive-summary', [ReportController::class, 'executiveSummary']);
Route::get('by-category', [ReportController::class, 'byCategory']);
Route::get('by-cost-center', [ReportController::class, 'byCostCenter']);
Route::get('monthly-evolution', [ReportController::class, 'monthlyEvolution']);
Route::get('by-day-of-week', [ReportController::class, 'byDayOfWeek']);
Route::get('top-expenses', [ReportController::class, 'topExpenses']);
@ -265,6 +267,9 @@
Route::get('accounts', [ReportController::class, 'accountsReport']);
Route::get('projection', [ReportController::class, 'projection']);
Route::get('recurring', [ReportController::class, 'recurringReport']);
Route::get('liabilities', [ReportController::class, 'liabilities']);
Route::get('future-transactions', [ReportController::class, 'futureTransactions']);
Route::get('overdue', [ReportController::class, 'overdueTransactions']);
});
// ============================================

View File

@ -60,7 +60,11 @@
"deselectAll": "Deselect All",
"applyToSelected": "Apply to Selected",
"batchNoSelection": "Select at least one transaction",
"noResults": "No results"
"noResults": "No results",
"incomes": "Income",
"expenses": "Expenses",
"balance": "Balance",
"current": "Current"
},
"auth": {
"login": "Login",
@ -90,7 +94,11 @@
"settings": "Settings",
"business": "Business",
"profile": "Profile",
"help": "Help"
"help": "Help",
"planning": "Planning",
"financialHealth": "Financial Health",
"goals": "Goals",
"budgets": "Budgets"
},
"dashboard": {
"title": "Dashboard",
@ -240,7 +248,6 @@
"recalculateError": "Error recalculating balances",
"adjustBalance": "Adjust Balance",
"adjustInfo": "Enter the actual current balance of the account. The system will automatically adjust the initial balance so that the calculations are correct.",
"currentBalance": "Current Balance",
"targetBalance": "Actual Balance",
"targetBalancePlaceholder": "Enter the actual account balance",
"targetBalanceHelp": "The initial balance will be recalculated automatically",
@ -453,7 +460,7 @@
"nominalRate": "Nominal Rate",
"effectiveRate": "Effective Rate",
"financialSummary": "Financial Summary",
"summaryPoint1": "For every \u20ac1 borrowed, you pay \u20ac0.28 in interest (28%)",
"summaryPoint1": "For every €1 borrowed, you pay €0.28 in interest (28%)",
"summaryPointDynamic1": "In this contract, total interest cost represents {{ratio}}% of the principal",
"summaryPoint2": "The PRICE system favors the bank in early installments",
"summaryPoint3": "Early payments significantly reduce interest",
@ -1252,7 +1259,7 @@
},
"services": {
"title": "Service Technical Sheets",
"description": "Manage the COSS (Cost of Service Sold) for each service",
"description": "Description",
"add": "New Service",
"edit": "Edit Service",
"name": "Service Name",
@ -1261,7 +1268,6 @@
"category": "Category",
"categoryPlaceholder": "E.g.: Cuts",
"duration": "Duration",
"description": "Description",
"descriptionPlaceholder": "Describe the service...",
"businessSetting": "Business Setting",
"selectSetting": "Select setting",
@ -1477,5 +1483,426 @@
"status": "Status",
"totalCmv": "Total COGS"
}
},
"financialHealth": {
"title": "Financial Health",
"subtitle": "Complete analysis of your finances",
"lastUpdate": "Last update",
"overallScore": "Your overall score",
"outOf100": "out of 100",
"scoreDescription": "Your financial health score based on various indicators",
"errorLoading": "Error loading financial health data",
"score": "Score",
"savingsRate": "Savings Rate",
"income": "Income",
"expenses": "Expenses",
"vsLastMonth": "vs last month",
"vsAverage": "vs average",
"daysRemaining": "days remaining",
"target": "Target",
"monthlyTarget": "Suggested monthly savings",
"tabs": {
"overview": "Overview",
"metrics": "Metrics",
"categories": "Categories",
"trends": "Trends",
"insights": "Insights"
},
"levels": {
"excellent": "Excellent Financial Health",
"good": "Good Financial Health",
"moderate": "Moderate Health",
"needs_work": "Needs Improvement",
"critical": "Urgent Attention"
},
"summary": {
"netWorth": "Net Worth",
"assets": "Assets",
"liabilities": "Liabilities",
"monthlySavings": "Monthly Savings",
"savingsRate": "Savings rate",
"monthlyIncome": "Monthly Income",
"monthlyExpenses": "Monthly Expenses",
"projectedSavings": "Projected Savings",
"byCurrency": "By currency",
"title": "Financial Summary",
"totalAssets": "Total Assets",
"totalLiabilities": "Total Liabilities"
},
"metrics": {
"savings_capacity": "Savings Capacity",
"debt_control": "Debt Control",
"budget_management": "Budget Management",
"expense_efficiency": "Expense Efficiency",
"emergency_fund": "Emergency Fund",
"financial_stability": "Financial Stability",
"savingsCapacity": "Savings Capacity",
"debtControl": "Debt Control",
"budgetManagement": "Budget Management",
"expenseEfficiency": "Expense Efficiency",
"emergencyFund": "Emergency Fund",
"financialStability": "Financial Stability"
},
"status": {
"excellent": "Excellent",
"good": "Good",
"adequate": "Adequate",
"moderate": "Moderate",
"needs_improvement": "Needs improvement",
"needs_attention": "Needs attention",
"needs_work": "Must improve",
"negative": "Negative",
"critical": "Critical",
"insufficient": "Insufficient",
"debt_free": "Debt free",
"healthy": "Healthy",
"manageable": "Manageable",
"concerning": "Concerning",
"on_track": "On track",
"exceeded": "Exceeded",
"not_configured": "Not configured",
"very_stable": "Very stable",
"stable": "Stable",
"volatile": "Volatile",
"optimized": "Optimized",
"acceptable": "Acceptable",
"high_discretionary": "High discretionary spending",
"needsImprovement": "Needs Improvement"
},
"details": {
"savingsRate": "Savings rate",
"monthlySavings": "Monthly savings",
"totalDebt": "Total debt",
"debtToIncome": "Debt/Income",
"activeDebts": "Active debts",
"budgetsConfigured": "Budgets configured",
"compliance": "Compliance",
"exceeded": "Exceeded",
"noBudgets": "No budgets configured",
"liquidAssets": "Liquid assets",
"monthsCovered": "Months covered",
"gap": "Gap",
"incomeVolatility": "Income volatility",
"expenseVolatility": "Expense volatility",
"savingsTrend": "Savings trend"
},
"distribution": {
"fixed": "Fixed",
"variable": "Variable",
"discretionary": "Discretionary"
},
"categories": {
"distribution": "Distribution",
"topExpenses": "Top Expenses",
"trends": "Trends",
"title": "Category Analysis"
},
"trends": {
"monthlyEvolution": "Monthly Evolution",
"incomeTrend": "Income Trend",
"expenseTrend": "Expense Trend",
"savingsTrend": "Savings Trend",
"monthlyComparison": "Monthly Comparison",
"scoreHistory": "Score History",
"title": "Trends"
},
"trend": {
"increasing": "Increasing",
"decreasing": "Decreasing",
"stable": "Stable"
},
"insightsTitle": "Situation Analysis",
"noInsights": "No insights available at this time",
"recommendationsTitle": "Recommendations",
"noRecommendations": "Excellent! No urgent recommendations",
"priority": {
"high": "High",
"medium": "Medium"
},
"projection": {
"title": "Projection",
"currentExpenses": "Current Expenses",
"projected": "Projected",
"nextMonth": "Next Month",
"projectedSavings": "Projected Savings"
},
"insights": {
"excellentSavings": "Excellent Savings",
"excellentSavingsMsg": "Your savings rate of {{rate}}% is well above average. Keep it up!",
"goodSavings": "Good Savings",
"goodSavingsMsg": "Your savings rate of {{rate}}% is in the healthy range. Consider increasing it gradually.",
"negativeSavings": "Expenses Exceed Income",
"negativeSavingsMsg": "You're spending more than you earn. Review your expenses to avoid debt.",
"spendingMoreThanEarning": "You're spending {{deficit}}€ more than you earn monthly. Review your expenses.",
"debtFree": "Debt Free",
"debtFreeMsg": "You have no active debts. Excellent financial management!",
"highDebt": "High Debt",
"highDebtMsg": "Your debt-to-income ratio is {{ratio}}%. Consider prioritizing debt repayment.",
"budgetsExceeded": "Budgets Exceeded",
"budgetsExceededMsg": "You have {{count}} exceeded budgets this month. Review your expenses.",
"allBudgetsOk": "Budgets Under Control",
"allBudgetsOkMsg": "All your budgets are within limits. Excellent control!",
"goodEmergencyFund": "Solid Emergency Fund",
"goodEmergencyFundMsg": "You have {{months}} months of expenses covered. Your financial security is guaranteed.",
"lowEmergencyFund": "Low Emergency Fund",
"lowEmergencyFundMsg": "You only have {{months}} months of expenses covered. It's recommended to have at least 6 months.",
"emergencyFundMessage": "You're missing {{gap}}€ to cover 6 months of expenses. Consider saving more.",
"stableFinances": "Stable Finances",
"stableFinancesMsg": "Your income and expenses show low volatility, indicating good stability.",
"volatileFinances": "Variable Finances",
"volatileFinancesMsg": "Your finances show high volatility. Consider creating a safety buffer.",
"highConcentration": "Expense Concentration",
"highConcentrationMsg": "{{category}} represents {{percentage}}% of your expenses. Consider diversifying.",
"spendingIncrease": "Spending Increase",
"spendingIncreaseMsg": "{{category}} increased {{change}}% vs last month. Check if necessary.",
"spendingDecrease": "Spending Decrease",
"spendingDecreaseMsg": "{{category}} decreased {{change}}% vs last month. Good job optimizing!",
"spending_spike": "Spending Spike",
"spendingSpike": "{{category}} increased {{increase}}% vs last month. This spike may affect your budget.",
"noBudgets": "No Budgets",
"createBudgetsMessage": "You have no budgets configured. Create budgets to better control your spending.",
"title": "Insights",
"recommendations": "Recommendations"
},
"recommendations": {
"increaseSavings": "Try to increase your savings rate. Small increments make a big difference over time.",
"reduceSavingsDeficit": "Reduce expenses by {{amount}} monthly to balance your budget and avoid debt.",
"prioritizeDebt": "Prioritize debt repayment. Consider the avalanche method (highest interest first) or snowball (smallest amount first).",
"setupBudgets": "Set up monthly budgets for your main expense categories.",
"reviewBudgets": "Review exceeded budgets and adjust amounts or reduce expenses.",
"buildEmergencyFund": "Build an emergency fund. Goal: 6 months of expenses. Save {{monthly_suggestion}}€/month.",
"increaseEmergencyFund": "Increase your emergency fund. You're missing {{gap}} to cover 6 months of expenses.",
"reduceVolatility": "Work on stabilizing your finances by creating a buffer for variable months.",
"reduceDiscretionary": "Reduce discretionary spending from {{current_percentage}}% to {{target_percentage}}% to improve your savings.",
"createBudgets": "Create budgets for your main spending categories for better control."
},
"loading": "Analyzing your finances..."
},
"budgets": {
"title": "Budgets",
"subtitle": "Control your monthly spending",
"addBudget": "Add Budget",
"newBudget": "New Budget",
"editBudget": "Edit Budget",
"deleteBudget": "Delete Budget",
"deleteConfirm": "Are you sure you want to delete this budget?",
"noBudgets": "No budgets",
"noBudgetsDescription": "Start by creating your first monthly budget",
"createFirst": "Create First Budget",
"category": "Category",
"selectCategory": "Select a category",
"amount": "Amount",
"month": "Month",
"budgeted": "Budgeted",
"spent": "Spent",
"remaining": "Remaining",
"exceeded": "Exceeded",
"almostExceeded": "Almost exceeded",
"usage": "Usage",
"copyToNext": "Copy to next month",
"totalBudgeted": "Total Budgeted",
"totalSpent": "Total Spent",
"allCategoriesUsed": "All categories already have budgets this month",
"autoPropagateInfo": "This budget will automatically propagate to future months",
"alert": {
"exceeded": "Budget exceeded!",
"warning": "Warning: near limit",
"onTrack": "On budget"
},
"summary": {
"totalBudget": "Total Budget",
"totalSpent": "Total Spent",
"available": "Available",
"usagePercent": "% Used"
},
"yearSummary": "Year Summary",
"currentMonth": "Current",
"noCategory": "No category",
"exceededBy": "Exceeded by",
"copySuccess": "Budgets copied to next month",
"copyTitle": "Copy to next month"
},
"goals": {
"title": "Financial Goals",
"subtitle": "Track your savings goals",
"newGoal": "New Goal",
"editGoal": "Edit Goal",
"deleteGoal": "Delete Goal",
"deleteConfirm": "Are you sure you want to delete this goal?",
"noGoals": "No goals",
"noGoalsDescription": "Start by creating your first financial goal",
"createFirstGoal": "Create First Goal",
"totalGoals": "Total Goals",
"activeGoals": "Active Goals",
"totalSaved": "Total Saved",
"remaining": "Remaining",
"targetDate": "Target Date",
"targetAmount": "Target Amount",
"currentAmount": "Current Amount",
"monthlyContribution": "Monthly Contribution",
"monthsRemaining": "Months Remaining",
"months": "months",
"progress": "Progress",
"contribute": "Contribute",
"contributeAmount": "Contribution Amount",
"contributeNote": "Note (optional)",
"onTrack": "On track!",
"needsMore": "Need to save {{amount}}/month more",
"statusActive": "Active",
"statusCompleted": "Completed",
"statusPaused": "Paused",
"statusCancelled": "Cancelled",
"addContribution": "Add Contribution",
"addGoal": "Add Goal",
"archive": "Archive",
"color": "Color",
"completed": "Completed",
"congratulations": "Congratulations!",
"contributionDate": "Contribution Date",
"createFirst": "Create First Goal",
"description": "Description",
"goalCompleted": "Goal Completed!",
"icon": "Icon",
"markCompleted": "Mark as Completed",
"name": "Name",
"notes": "Notes",
"notesPlaceholder": "Add a note (optional)",
"pause": "Pause",
"priority": "Priority",
"resume": "Resume",
"viewDetails": "View Details",
"stats": {
"activeGoals": "Active Goals",
"completedGoals": "Completed Goals",
"overallProgress": "Overall Progress",
"totalGoals": "Total Goals",
"totalSaved": "Total Saved",
"totalTarget": "Total Target"
},
"status": {
"active": "Active",
"advancing": "Advancing",
"cancelled": "Cancelled",
"completed": "Completed",
"paused": "Paused",
"starting": "Starting"
}
},
"reports": {
"accounts": "Accounts",
"avgExpense": "Average Expense",
"avgIncome": "Average Income",
"balance": "Balance",
"byCategory": "By Category",
"byCostCenter": "By Cost Center",
"comparison": "Comparison",
"custom": "Custom",
"dayOfWeek": {
"friday": "Friday",
"monday": "Monday",
"saturday": "Saturday",
"sunday": "Sunday",
"thursday": "Thursday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"day": "Day"
},
"daysRemaining": "Days Remaining",
"expenses": "Expenses",
"income": "Income",
"last3Months": "Last 3 Months",
"last6Months": "Last 6 Months",
"lastMonth": "Last Month",
"lastYear": "Last Year",
"monthlyEvolution": "Monthly Evolution",
"period": "Period",
"projectedExpense": "Projected Expense",
"projectedIncome": "Projected Income",
"projection": "Projection",
"projectionTitle": "Month Projection",
"recurring": "Recurring",
"liabilities": "Liabilities",
"futureTransactions": "Future",
"overdue": "Overdue",
"savingsRate": "Savings Rate",
"selectPeriod": "Select Period",
"subtitle": "Detailed analysis of your finances",
"summary": "Summary",
"thisMonth": "This Month",
"thisYear": "This Year",
"title": "Reports",
"topExpenses": "Top Expenses",
"vsAverage": "vs Average",
"vsLastPeriod": "vs Last Period",
"yearComparison": "Year Comparison",
"expenseDistribution": "Expense Distribution",
"categoryDetail": "Category Detail",
"category": "Category",
"amount": "Amount",
"description": "Description",
"date": "Date",
"top20Expenses": "Top 20 Monthly Expenses",
"expensesByDayOfWeek": "Expenses by Day of Week",
"totalSpent": "Total spent",
"totalIncome": "Total Income",
"totalExpense": "Total Expense",
"totalRecurring": "Total Recurring",
"monthlyIncome": "Monthly Income",
"monthlyExpense": "Monthly Expense",
"netRecurring": "Net Recurring",
"recurringList": "Recurring List",
"nextDate": "Next Date",
"totalLiabilities": "Total Liabilities",
"totalDebt": "Total Debt",
"totalPaid": "Total Paid",
"totalPending": "Total Pending",
"overdueInstallments": "overdue installments",
"installments": "installments",
"paid": "Paid",
"pending": "Pending",
"nextInstallment": "Next Installment",
"totalTransactions": "Total Transactions",
"futureIncome": "Future Income",
"futureExpense": "Future Expense",
"netImpact": "Net Impact",
"next30Days": "Next 30 Days",
"account": "Account",
"totalOverdue": "Total Overdue",
"overdueAmount": "Overdue Amount",
"noOverdue": "No Overdue!",
"noOverdueDescription": "You have no overdue payments. Great management!",
"overdueList": "Overdue List",
"dueDate": "Due Date",
"daysOverdue": "Days Overdue",
"historicalAverage": "Historical Average",
"monthProjection": "Month Projection",
"last3Months": "last 3 months",
"currentMonth": "Current Month"
},
"months": {
"january": "January",
"february": "February",
"march": "March",
"april": "April",
"may": "May",
"june": "June",
"july": "July",
"august": "August",
"september": "September",
"october": "October",
"november": "November",
"december": "December",
"jan": "Jan",
"feb": "Feb",
"mar": "Mar",
"apr": "Apr",
"mayShort": "May",
"jun": "Jun",
"jul": "Jul",
"aug": "Aug",
"sep": "Sep",
"oct": "Oct",
"nov": "Nov",
"dec": "Dec"
}
}
}

View File

@ -60,7 +60,11 @@
"deselectAll": "Desmarcar Todas",
"applyToSelected": "Aplicar a Seleccionadas",
"batchNoSelection": "Seleccione al menos una transacción",
"noResults": "Sin resultados"
"noResults": "Sin resultados",
"incomes": "Ingresos",
"expenses": "Gastos",
"balance": "Balance",
"current": "Actual"
},
"auth": {
"login": "Iniciar Sesión",
@ -244,7 +248,6 @@
"recalculateError": "Error al recalcular saldos",
"adjustBalance": "Ajustar Saldo",
"adjustInfo": "Introduzca el saldo real actual de la cuenta. El sistema ajustará automáticamente el saldo inicial para que los cálculos sean correctos.",
"currentBalance": "Saldo Actual",
"targetBalance": "Saldo Real",
"targetBalancePlaceholder": "Introduzca el saldo real de la cuenta",
"targetBalanceHelp": "El saldo inicial se recalculará automáticamente",
@ -457,7 +460,7 @@
"nominalRate": "Tasa Nominal",
"effectiveRate": "Tasa Efectiva",
"financialSummary": "Resumen Financiero",
"summaryPoint1": "Por cada \u20ac1 prestado, pagas \u20ac0,28 en intereses (28%)",
"summaryPoint1": "Por cada €1 prestado, pagas €0,28 en intereses (28%)",
"summaryPointDynamic1": "En este contrato, el costo total de intereses representa {{ratio}}% del capital",
"summaryPoint2": "El sistema PRICE favorece al banco en las primeras cuotas",
"summaryPoint3": "Los pagos anticipados reducen significativamente los intereses",
@ -465,7 +468,7 @@
"thisContract": "Este contrato",
"interestOverPrincipal": "sobre el capital",
"contractCost": "Costo Total del Contrato",
"contractCostText": "Cu\u00e1nto pagar\u00e1s adem\u00e1s del capital prestado:"
"contractCostText": "Cuánto pagarás además del capital prestado:"
},
"transactions": {
"title": "Transacciones",
@ -1256,7 +1259,7 @@
},
"services": {
"title": "Fichas Técnicas de Servicios",
"description": "Administra el CSV (Costo del Servicio Vendido) de cada servicio",
"description": "Descripción",
"add": "Nuevo Servicio",
"edit": "Editar Servicio",
"name": "Nombre del Servicio",
@ -1265,7 +1268,6 @@
"category": "Categoría",
"categoryPlaceholder": "Ej: Cortes",
"duration": "Duración",
"description": "Descripción",
"descriptionPlaceholder": "Describe el servicio...",
"businessSetting": "Configuración de Negocio",
"selectSetting": "Seleccionar configuración",
@ -1487,27 +1489,176 @@
"subtitle": "Evaluación integral de tus finanzas",
"lastUpdate": "Última actualización",
"overallScore": "Tu puntuación general",
"excellent": "Excelente Salud",
"good": "Buena Salud",
"moderate": "Salud Moderada",
"needsWork": "Necesita Mejorar",
"critical": "Atención Necesaria",
"onTrack": "Estás en buen camino, pero hay espacio para mejorar",
"outOf100": "de 100",
"scoreDescription": "Evaluación basada en 6 métricas clave",
"errorLoading": "Error al cargar la información de salud financiera",
"score": "Puntuación",
"savingsRate": "Tasa de ahorro",
"income": "Ingresos",
"expenses": "Gastos",
"vsLastMonth": "vs mes anterior",
"vsAverage": "vs promedio",
"daysRemaining": "días restantes",
"target": "Objetivo",
"monthlyTarget": "Ahorro mensual sugerido",
"tabs": {
"overview": "Resumen",
"metrics": "Métricas",
"categories": "Categorías",
"trends": "Tendencias",
"insights": "Insights"
},
"levels": {
"excellent": "Excelente Salud Financiera",
"good": "Buena Salud Financiera",
"moderate": "Salud Moderada",
"needs_work": "Necesita Mejorar",
"critical": "Atención Urgente"
},
"summary": {
"netWorth": "Patrimonio Neto",
"assets": "Activos",
"liabilities": "Pasivos",
"monthlySavings": "Ahorro Mensual",
"savingsRate": "Tasa de ahorro",
"monthlyIncome": "Ingresos del mes",
"monthlyExpenses": "Gastos del mes",
"projectedSavings": "Ahorro Proyectado",
"byCurrency": "Por moneda"
},
"metrics": {
"savingsCapacity": "Capacidad de ahorro",
"debtControl": "Control de deudas",
"budgetManagement": "Gestión de presupuesto",
"investments": "Inversiones",
"emergencyFund": "Fondo de emergencia",
"futurePlanning": "Planificación futuro"
"savings_capacity": "Capacidad de Ahorro",
"debt_control": "Control de Deudas",
"budget_management": "Gestión de Presupuesto",
"expense_efficiency": "Eficiencia de Gastos",
"emergency_fund": "Fondo de Emergencia",
"financial_stability": "Estabilidad Financiera"
},
"status": {
"excellent": "Excelente",
"good": "Bueno",
"adequate": "Adecuado",
"moderate": "Moderado",
"needs_improvement": "Necesita mejorar",
"needs_attention": "Requiere atención",
"needs_work": "Debe mejorar",
"negative": "Negativo",
"critical": "Crítico",
"insufficient": "Insuficiente",
"debt_free": "Sin deudas",
"healthy": "Saludable",
"manageable": "Manejable",
"concerning": "Preocupante",
"on_track": "En meta",
"exceeded": "Excedido",
"not_configured": "No configurado",
"very_stable": "Muy estable",
"stable": "Estable",
"volatile": "Volátil",
"optimized": "Optimizado",
"acceptable": "Aceptable",
"high_discretionary": "Alto gasto discrecional"
},
"details": {
"savingsRate": "Tasa de ahorro",
"monthlySavings": "Ahorro mensual",
"totalDebt": "Deuda total",
"debtToIncome": "Deuda/Ingresos",
"activeDebts": "Deudas activas",
"budgetsConfigured": "Presupuestos configurados",
"compliance": "Cumplimiento",
"exceeded": "Excedidos",
"noBudgets": "Sin presupuestos configurados",
"liquidAssets": "Activos líquidos",
"monthsCovered": "Meses cubiertos",
"gap": "Brecha",
"incomeVolatility": "Volatilidad ingresos",
"expenseVolatility": "Volatilidad gastos",
"savingsTrend": "Tendencia de ahorro"
},
"distribution": {
"fixed": "Gastos Fijos",
"variable": "Gastos Variables",
"discretionary": "Gastos Discrecionales"
},
"categories": {
"distribution": "Distribución de Gastos",
"topExpenses": "Mayores Gastos",
"trends": "Tendencias por Categoría"
},
"trends": {
"monthlyEvolution": "Evolución Mensual",
"incomeTrend": "Tendencia de Ingresos",
"expenseTrend": "Tendencia de Gastos",
"savingsTrend": "Tendencia de Ahorro",
"monthlyComparison": "Comparación Mensual",
"scoreHistory": "Historial de Puntuación"
},
"trend": {
"increasing": "En aumento",
"decreasing": "En descenso",
"stable": "Estable"
},
"insightsTitle": "Análisis de tu Situación",
"noInsights": "No hay insights disponibles en este momento",
"recommendationsTitle": "Recomendaciones",
"noRecommendations": "¡Excelente! No hay recomendaciones urgentes",
"priority": {
"high": "Alta",
"medium": "Media"
},
"projection": {
"title": "Proyección del Mes",
"currentExpenses": "Gastos Actuales",
"projected": "Proyectado"
},
"insights": {
"highPriority": "Prioridad Alta",
"mediumPriority": "Prioridad Media",
"achievement": "Logro Destacado",
"opportunity": "Oportunidad",
"upcomingGoal": "Meta Próxima",
"suggestion": "Sugerencia"
"excellentSavings": "Ahorro Excelente",
"excellentSavingsMsg": "Tu tasa de ahorro del {{rate}}% está muy por encima del promedio. ¡Sigue así!",
"goodSavings": "Buen Ahorro",
"goodSavingsMsg": "Tu tasa de ahorro del {{rate}}% está en el rango saludable. Considera aumentarla gradualmente.",
"negativeSavings": "Gastos Superan Ingresos",
"negativeSavingsMsg": "Estás gastando más de lo que ganas. Revisa tus gastos para evitar endeudamiento.",
"spendingMoreThanEarning": "Estás gastando {{deficit}}€ más de lo que ganas mensualmente. Revisa tus gastos.",
"debtFree": "Sin Deudas",
"debtFreeMsg": "No tienes deudas activas. ¡Excelente gestión financiera!",
"highDebt": "Deuda Elevada",
"highDebtMsg": "Tu ratio deuda/ingresos es del {{ratio}}%. Considera priorizar el pago de deudas.",
"budgetsExceeded": "Presupuestos Excedidos",
"budgetsExceededMsg": "Tienes {{count}} presupuestos excedidos este mes. Revisa tus gastos.",
"allBudgetsOk": "Presupuestos Bajo Control",
"allBudgetsOkMsg": "Todos tus presupuestos están dentro del límite. ¡Excelente control!",
"goodEmergencyFund": "Fondo de Emergencia Sólido",
"goodEmergencyFundMsg": "Tienes {{months}} meses de gastos cubiertos. Tu seguridad financiera está garantizada.",
"lowEmergencyFund": "Fondo de Emergencia Bajo",
"lowEmergencyFundMsg": "Solo tienes {{months}} meses de gastos cubiertos. Se recomienda tener al menos 6 meses.",
"emergencyFundMessage": "Te faltan {{gap}}€ para cubrir 6 meses de gastos. Considera ahorrar más.",
"stableFinances": "Finanzas Estables",
"stableFinancesMsg": "Tus ingresos y gastos muestran baja volatilidad, indicando buena estabilidad.",
"volatileFinances": "Finanzas Variables",
"volatileFinancesMsg": "Tus finanzas muestran alta volatilidad. Considera crear un buffer de seguridad.",
"highConcentration": "Concentración de Gastos",
"highConcentrationMsg": "{{category}} representa el {{percentage}}% de tus gastos. Considera diversificar.",
"spendingIncrease": "Aumento de Gastos",
"spendingIncreaseMsg": "{{category}} aumentó {{change}}% vs mes anterior. Revisa si es necesario.",
"spendingDecrease": "Reducción de Gastos",
"spendingDecreaseMsg": "{{category}} disminuyó {{change}}% vs mes anterior. ¡Buen trabajo optimizando!",
"spending_spike": "Pico de Gasto",
"spendingSpike": "{{category}} aumentó {{increase}}% vs mes anterior. Este pico puede afectar tu presupuesto.",
"noBudgets": "Sin Presupuestos",
"createBudgetsMessage": "No tienes presupuestos configurados. Crea presupuestos para controlar mejor tus gastos."
},
"recommendations": {
"increaseSavings": "Intenta aumentar tu tasa de ahorro. Pequeños incrementos hacen gran diferencia a largo plazo.",
"reduceSavingsDeficit": "Reduce gastos en {{amount}} mensuales para equilibrar tu presupuesto y evitar deudas.",
"prioritizeDebt": "Prioriza el pago de deudas. Considera el método avalancha (mayor interés primero) o bola de nieve (menor monto primero).",
"setupBudgets": "Configura presupuestos mensuales para tus principales categorías de gasto.",
"reviewBudgets": "Revisa los presupuestos excedidos y ajusta los montos o reduce gastos.",
"buildEmergencyFund": "Construye un fondo de emergencia. Objetivo: 6 meses de gastos. Ahorra {{monthly_suggestion}}€/mes.",
"increaseEmergencyFund": "Aumenta tu fondo de emergencia. Te faltan {{gap}} para cubrir 6 meses de gastos.",
"reduceVolatility": "Trabaja en estabilizar tus finanzas creando un buffer para meses variables.",
"reduceDiscretionary": "Reduce gastos discrecionales del {{current_percentage}}% al {{target_percentage}}% para mejorar tu ahorro.",
"createBudgets": "Crea presupuestos para tus categorías principales de gasto para mejor control."
}
},
"goals": {
@ -1600,6 +1751,8 @@
"totalBudgeted": "Total Presupuestado",
"totalSpent": "Total Gastado",
"almostExceeded": "Cerca del límite (80%+)",
"allCategoriesUsed": "Ya tienes presupuesto para todas las categorías este mes",
"autoPropagateInfo": "Este presupuesto se propagará automáticamente a los meses siguientes",
"summary": {
"totalBudget": "Presupuesto Total",
"totalSpent": "Gastado",
@ -1610,18 +1763,27 @@
"onTrack": "Bajo control",
"warning": "Cerca del límite",
"exceeded": "¡Excedido!"
}
},
"currentMonth": "Actual",
"noCategory": "Sin categoría",
"exceededBy": "Excedido en",
"copySuccess": "Presupuestos copiados al siguiente mes",
"copyTitle": "Copiar al próximo mes"
},
"reports": {
"title": "Reportes",
"subtitle": "Análisis detallado de tus finanzas",
"summary": "Resumen",
"byCategory": "Por Categoría",
"byCostCenter": "Por Centro de Costo",
"monthlyEvolution": "Evolución Mensual",
"comparison": "Comparativa",
"topExpenses": "Mayores Gastos",
"projection": "Proyección",
"recurring": "Recurrentes",
"liabilities": "Pasivos",
"futureTransactions": "Futuras",
"overdue": "Vencidas",
"accounts": "Por Cuenta",
"period": "Período",
"selectPeriod": "Seleccionar período",
@ -1646,13 +1808,83 @@
"wednesday": "Miércoles",
"thursday": "Jueves",
"friday": "Viernes",
"saturday": "Sábado"
"saturday": "Sábado",
"day": "Día"
},
"projectionTitle": "Proyección del mes",
"projectedExpense": "Gasto proyectado",
"projectedIncome": "Ingreso proyectado",
"daysRemaining": "Días restantes",
"vsAverage": "vs promedio histórico"
"vsAverage": "vs promedio histórico",
"yearComparison": "Comparativa Anual",
"expenseDistribution": "Distribución de Gastos",
"categoryDetail": "Detalle por Categoría",
"category": "Categoría",
"amount": "Monto",
"description": "Descripción",
"date": "Fecha",
"top20Expenses": "Top 20 Gastos del Mes",
"expensesByDayOfWeek": "Gastos por Día de la Semana",
"totalSpent": "Total gastado",
"totalIncome": "Total Ingresos",
"totalExpense": "Total Gastos",
"totalRecurring": "Total Recurrentes",
"monthlyIncome": "Ingreso Mensual",
"monthlyExpense": "Gasto Mensual",
"netRecurring": "Neto Recurrente",
"recurringList": "Lista de Recurrentes",
"nextDate": "Próxima Fecha",
"totalLiabilities": "Total Pasivos",
"totalDebt": "Deuda Total",
"totalPaid": "Total Pagado",
"totalPending": "Total Pendiente",
"overdueInstallments": "cuotas vencidas",
"installments": "cuotas",
"paid": "Pagado",
"pending": "Pendiente",
"nextInstallment": "Próxima Cuota",
"totalTransactions": "Total Transacciones",
"futureIncome": "Ingresos Futuros",
"futureExpense": "Gastos Futuros",
"netImpact": "Impacto Neto",
"next30Days": "Próximos 30 Días",
"account": "Cuenta",
"totalOverdue": "Total Vencidos",
"overdueAmount": "Monto Vencido",
"noOverdue": "¡Sin Vencidos!",
"noOverdueDescription": "No tienes pagos vencidos. ¡Excelente gestión!",
"overdueList": "Lista de Vencidos",
"dueDate": "Fecha Vencimiento",
"daysOverdue": "Días de Atraso",
"historicalAverage": "Promedio histórico",
"monthProjection": "Proyección del mes",
"last3Months": "últimos 3 meses",
"currentMonth": "Mes Actual"
},
"months": {
"january": "Enero",
"february": "Febrero",
"march": "Marzo",
"april": "Abril",
"may": "Mayo",
"june": "Junio",
"july": "Julio",
"august": "Agosto",
"september": "Septiembre",
"october": "Octubre",
"november": "Noviembre",
"december": "Diciembre",
"jan": "Ene",
"feb": "Feb",
"mar": "Mar",
"apr": "Abr",
"mayShort": "May",
"jun": "Jun",
"jul": "Jul",
"aug": "Ago",
"sep": "Sep",
"oct": "Oct",
"nov": "Nov",
"dec": "Dic"
}
}
}

View File

@ -61,7 +61,11 @@
"deselectAll": "Desmarcar Todas",
"applyToSelected": "Aplicar nas Selecionadas",
"batchNoSelection": "Selecione pelo menos uma transação",
"noResults": "Sem resultados"
"noResults": "Sem resultados",
"incomes": "Receitas",
"expenses": "Despesas",
"balance": "Saldo",
"current": "Atual"
},
"auth": {
"login": "Entrar",
@ -91,7 +95,11 @@
"settings": "Configurações",
"business": "Negócios",
"profile": "Perfil",
"help": "Ajuda"
"help": "Ajuda",
"planning": "Planejamento",
"financialHealth": "Saúde Financeira",
"goals": "Metas",
"budgets": "Orçamentos"
},
"dashboard": {
"title": "Painel de Controle",
@ -242,7 +250,6 @@
"recalculateError": "Erro ao recalcular saldos",
"adjustBalance": "Ajustar Saldo",
"adjustInfo": "Informe o saldo real atual da conta. O sistema ajustará automaticamente o saldo inicial para que os cálculos fiquem corretos.",
"currentBalance": "Saldo Atual",
"targetBalance": "Saldo Real",
"targetBalancePlaceholder": "Digite o saldo real da conta",
"targetBalanceHelp": "O saldo inicial será recalculado automaticamente",
@ -455,15 +462,15 @@
"nominalRate": "Taxa Nominal",
"effectiveRate": "Taxa Efetiva",
"financialSummary": "Resumo Financeiro",
"summaryPoint1": "Para cada \u20ac1 emprestado, voc\u00ea paga \u20ac0,28 em juros (28%)",
"summaryPoint1": "Para cada €1 emprestado, você paga €0,28 em juros (28%)",
"summaryPointDynamic1": "Neste contrato, o custo total de juros representa {{ratio}}% do capital",
"summaryPoint2": "O sistema PRICE favorece o banco nas primeiras parcelas",
"summaryPoint3": "Pagamentos antecipados reduzem significativamente os juros",
"summaryPoint4": "Encargos extras de quaisquer sobrepagamentos s\u00e3o registrados como taxas",
"summaryPoint4": "Encargos extras de quaisquer sobrepagamentos são registrados como taxas",
"thisContract": "Este contrato",
"interestOverPrincipal": "sobre o capital",
"contractCost": "Custo Total do Contrato",
"contractCostText": "Quanto voc\u00ea pagar\u00e1 al\u00e9m do capital emprestado:"
"contractCostText": "Quanto você pagará além do capital emprestado:"
},
"transactions": {
"title": "Transações",
@ -1254,7 +1261,7 @@
},
"services": {
"title": "Fichas Técnicas de Serviços",
"description": "Gerencie o CSV (Custo do Serviço Vendido) de cada serviço",
"description": "Descrição",
"add": "Novo Serviço",
"edit": "Editar Serviço",
"name": "Nome do Serviço",
@ -1263,7 +1270,6 @@
"category": "Categoria",
"categoryPlaceholder": "Ex: Cortes",
"duration": "Duração",
"description": "Descrição",
"descriptionPlaceholder": "Descreva o serviço...",
"businessSetting": "Configuração de Negócio",
"selectSetting": "Selecionar configuração",
@ -1479,5 +1485,426 @@
"status": "Status",
"totalCmv": "CMV Total"
}
},
"financialHealth": {
"title": "Saúde Financeira",
"subtitle": "Análise completa das suas finanças",
"lastUpdate": "Última atualização",
"overallScore": "Sua pontuação geral",
"outOf100": "de 100",
"scoreDescription": "Sua pontuação de saúde financeira baseada em vários indicadores",
"errorLoading": "Erro ao carregar dados de saúde financeira",
"score": "Pontuação",
"savingsRate": "Taxa de Poupança",
"income": "Receitas",
"expenses": "Despesas",
"vsLastMonth": "vs mês anterior",
"vsAverage": "vs média",
"daysRemaining": "dias restantes",
"target": "Objetivo",
"monthlyTarget": "Economia mensal sugerida",
"tabs": {
"overview": "Visão Geral",
"metrics": "Métricas",
"categories": "Categorias",
"trends": "Tendências",
"insights": "Insights"
},
"levels": {
"excellent": "Excelente Saúde Financeira",
"good": "Boa Saúde Financeira",
"moderate": "Saúde Moderada",
"needs_work": "Precisa Melhorar",
"critical": "Atenção Urgente"
},
"summary": {
"netWorth": "Patrimônio Líquido",
"assets": "Ativos",
"liabilities": "Passivos",
"monthlySavings": "Poupança Mensal",
"savingsRate": "Taxa de poupança",
"monthlyIncome": "Renda Mensal",
"monthlyExpenses": "Despesas Mensais",
"projectedSavings": "Poupança Projetada",
"byCurrency": "Por moeda",
"title": "Resumo Financeiro",
"totalAssets": "Ativos Totais",
"totalLiabilities": "Passivos Totais"
},
"metrics": {
"savings_capacity": "Capacidade de Poupança",
"debt_control": "Controle de Dívidas",
"budget_management": "Gestão de Orçamento",
"expense_efficiency": "Eficiência de Gastos",
"emergency_fund": "Fundo de Emergência",
"financial_stability": "Estabilidade Financeira",
"savingsCapacity": "Capacidade de Poupança",
"debtControl": "Controle de Dívidas",
"budgetManagement": "Gestão de Orçamento",
"expenseEfficiency": "Eficiência de Gastos",
"emergencyFund": "Fundo de Emergência",
"financialStability": "Estabilidade Financeira"
},
"status": {
"excellent": "Excelente",
"good": "Bom",
"adequate": "Adequado",
"moderate": "Moderado",
"needs_improvement": "Precisa melhorar",
"needs_attention": "Requer atenção",
"needs_work": "Deve melhorar",
"negative": "Negativo",
"critical": "Crítico",
"insufficient": "Insuficiente",
"debt_free": "Sem dívidas",
"healthy": "Saudável",
"manageable": "Controlável",
"concerning": "Preocupante",
"on_track": "No caminho certo",
"exceeded": "Excedido",
"not_configured": "Não configurado",
"very_stable": "Muito estável",
"stable": "Estável",
"volatile": "Volátil",
"optimized": "Otimizado",
"acceptable": "Aceitável",
"high_discretionary": "Alto gasto discricionário",
"needsImprovement": "Precisa Melhorar"
},
"details": {
"savingsRate": "Taxa de poupança",
"monthlySavings": "Poupança mensal",
"totalDebt": "Dívida total",
"debtToIncome": "Dívida/Receita",
"activeDebts": "Dívidas ativas",
"budgetsConfigured": "Orçamentos configurados",
"compliance": "Cumprimento",
"exceeded": "Excedidos",
"noBudgets": "Sem orçamentos configurados",
"liquidAssets": "Ativos líquidos",
"monthsCovered": "Meses cobertos",
"gap": "Diferença",
"incomeVolatility": "Volatilidade receitas",
"expenseVolatility": "Volatilidade despesas",
"savingsTrend": "Tendência de poupança"
},
"distribution": {
"fixed": "Fixos",
"variable": "Variáveis",
"discretionary": "Discricionários"
},
"categories": {
"distribution": "Distribuição",
"topExpenses": "Maiores Despesas",
"trends": "Tendências",
"title": "Análise de Categorias"
},
"trends": {
"monthlyEvolution": "Evolução Mensal",
"incomeTrend": "Tendência de Receitas",
"expenseTrend": "Tendência de Despesas",
"savingsTrend": "Tendência de Poupança",
"monthlyComparison": "Comparação Mensal",
"scoreHistory": "Histórico de Pontuação",
"title": "Tendências"
},
"trend": {
"increasing": "Aumentando",
"decreasing": "Diminuindo",
"stable": "Estável"
},
"insightsTitle": "Análise da Sua Situação",
"noInsights": "Não há insights disponíveis no momento",
"recommendationsTitle": "Recomendações",
"noRecommendations": "Excelente! Não há recomendações urgentes",
"priority": {
"high": "Alta",
"medium": "Média"
},
"projection": {
"title": "Projeção",
"currentExpenses": "Despesas Atuais",
"projected": "Projetado",
"nextMonth": "Próximo Mês",
"projectedSavings": "Poupança Projetada"
},
"insights": {
"excellentSavings": "Poupança Excelente",
"excellentSavingsMsg": "Sua taxa de poupança de {{rate}}% está muito acima da média. Continue assim!",
"goodSavings": "Boa Poupança",
"goodSavingsMsg": "Sua taxa de poupança de {{rate}}% está na faixa saudável. Considere aumentá-la gradualmente.",
"negativeSavings": "Despesas Superam Receitas",
"negativeSavingsMsg": "Você está gastando mais do que ganha. Revise suas despesas para evitar endividamento.",
"spendingMoreThanEarning": "Você está gastando {{deficit}}€ mais do que ganha mensalmente. Revise suas despesas.",
"debtFree": "Sem Dívidas",
"debtFreeMsg": "Você não tem dívidas ativas. Excelente gestão financeira!",
"highDebt": "Dívida Elevada",
"highDebtMsg": "Sua relação dívida/receita é de {{ratio}}%. Considere priorizar o pagamento de dívidas.",
"budgetsExceeded": "Orçamentos Excedidos",
"budgetsExceededMsg": "Você tem {{count}} orçamentos excedidos este mês. Revise suas despesas.",
"allBudgetsOk": "Orçamentos Sob Controle",
"allBudgetsOkMsg": "Todos os seus orçamentos estão dentro do limite. Excelente controle!",
"goodEmergencyFund": "Fundo de Emergência Sólido",
"goodEmergencyFundMsg": "Você tem {{months}} meses de despesas cobertos. Sua segurança financeira está garantida.",
"lowEmergencyFund": "Fundo de Emergência Baixo",
"lowEmergencyFundMsg": "Você só tem {{months}} meses de despesas cobertos. Recomenda-se ter pelo menos 6 meses.",
"emergencyFundMessage": "Faltam {{gap}}€ para cobrir 6 meses de despesas. Considere poupar mais.",
"stableFinances": "Finanças Estáveis",
"stableFinancesMsg": "Suas receitas e despesas mostram baixa volatilidade, indicando boa estabilidade.",
"volatileFinances": "Finanças Variáveis",
"volatileFinancesMsg": "Suas finanças mostram alta volatilidade. Considere criar um buffer de segurança.",
"highConcentration": "Concentração de Gastos",
"highConcentrationMsg": "{{category}} representa {{percentage}}% das suas despesas. Considere diversificar.",
"spendingIncrease": "Aumento de Gastos",
"spendingIncreaseMsg": "{{category}} aumentou {{change}}% vs mês anterior. Verifique se é necessário.",
"spendingDecrease": "Redução de Gastos",
"spendingDecreaseMsg": "{{category}} diminuiu {{change}}% vs mês anterior. Bom trabalho otimizando!",
"spending_spike": "Pico de Gasto",
"spendingSpike": "{{category}} aumentou {{increase}}% vs mês anterior. Este pico pode afetar seu orçamento.",
"noBudgets": "Sem Orçamentos",
"createBudgetsMessage": "Você não tem orçamentos configurados. Crie orçamentos para controlar melhor seus gastos.",
"title": "Insights",
"recommendations": "Recomendações"
},
"recommendations": {
"increaseSavings": "Tente aumentar sua taxa de poupança. Pequenos incrementos fazem grande diferença a longo prazo.",
"reduceSavingsDeficit": "Reduza despesas em {{amount}} mensais para equilibrar seu orçamento e evitar dívidas.",
"prioritizeDebt": "Priorize o pagamento de dívidas. Considere o método avalanche (maior juro primeiro) ou bola de neve (menor valor primeiro).",
"setupBudgets": "Configure orçamentos mensais para suas principais categorias de gastos.",
"reviewBudgets": "Revise os orçamentos excedidos e ajuste os valores ou reduza gastos.",
"buildEmergencyFund": "Construa um fundo de emergência. Objetivo: 6 meses de despesas. Poupe {{monthly_suggestion}}€/mês.",
"increaseEmergencyFund": "Aumente seu fundo de emergência. Faltam {{gap}} para cobrir 6 meses de despesas.",
"reduceVolatility": "Trabalhe em estabilizar suas finanças criando um buffer para meses variáveis.",
"reduceDiscretionary": "Reduza gastos discricionários de {{current_percentage}}% para {{target_percentage}}% para melhorar sua poupança.",
"createBudgets": "Crie orçamentos para suas principais categorias de gastos para melhor controle."
},
"loading": "Analisando suas finanças..."
},
"budgets": {
"title": "Orçamentos",
"subtitle": "Controle seus gastos mensais",
"addBudget": "Adicionar Orçamento",
"newBudget": "Novo Orçamento",
"editBudget": "Editar Orçamento",
"deleteBudget": "Excluir Orçamento",
"deleteConfirm": "Tem certeza que deseja excluir este orçamento?",
"noBudgets": "Nenhum orçamento",
"noBudgetsDescription": "Comece criando seu primeiro orçamento mensal",
"createFirst": "Criar Primeiro Orçamento",
"category": "Categoria",
"selectCategory": "Selecione uma categoria",
"amount": "Valor",
"month": "Mês",
"budgeted": "Orçado",
"spent": "Gasto",
"remaining": "Restante",
"exceeded": "Excedido",
"almostExceeded": "Quase excedido",
"usage": "Uso",
"copyToNext": "Copiar para próximo mês",
"totalBudgeted": "Total Orçado",
"totalSpent": "Total Gasto",
"allCategoriesUsed": "Todas as categorias já possuem orçamento este mês",
"autoPropagateInfo": "Este orçamento será propagado automaticamente para os meses seguintes",
"alert": {
"exceeded": "Orçamento excedido!",
"warning": "Atenção: próximo do limite",
"onTrack": "Dentro do orçamento"
},
"summary": {
"totalBudget": "Orçamento Total",
"totalSpent": "Total Gasto",
"available": "Disponível",
"usagePercent": "% Utilizado"
},
"yearSummary": "Resumo do Ano",
"currentMonth": "Atual",
"noCategory": "Sem categoria",
"exceededBy": "Excedido em",
"copySuccess": "Orçamentos copiados para o próximo mês",
"copyTitle": "Copiar para próximo mês"
},
"goals": {
"title": "Metas Financeiras",
"subtitle": "Acompanhe suas metas de economia",
"newGoal": "Nova Meta",
"editGoal": "Editar Meta",
"deleteGoal": "Excluir Meta",
"deleteConfirm": "Tem certeza que deseja excluir esta meta?",
"noGoals": "Nenhuma meta",
"noGoalsDescription": "Comece criando sua primeira meta financeira",
"createFirstGoal": "Criar Primeira Meta",
"totalGoals": "Total de Metas",
"activeGoals": "Metas Ativas",
"totalSaved": "Total Economizado",
"remaining": "Restante",
"targetDate": "Data Alvo",
"targetAmount": "Valor Alvo",
"currentAmount": "Valor Atual",
"monthlyContribution": "Contribuição Mensal",
"monthsRemaining": "Meses Restantes",
"months": "meses",
"progress": "Progresso",
"contribute": "Contribuir",
"contributeAmount": "Valor da Contribuição",
"contributeNote": "Nota (opcional)",
"onTrack": "No caminho certo!",
"needsMore": "Precisa economizar mais {{amount}}/mês",
"statusActive": "Ativa",
"statusCompleted": "Concluída",
"statusPaused": "Pausada",
"statusCancelled": "Cancelada",
"addContribution": "Adicionar Contribuição",
"addGoal": "Adicionar Meta",
"archive": "Arquivar",
"color": "Cor",
"completed": "Concluídas",
"congratulations": "Parabéns!",
"contributionDate": "Data da Contribuição",
"createFirst": "Criar Primeira Meta",
"description": "Descrição",
"goalCompleted": "Meta Concluída!",
"icon": "Ícone",
"markCompleted": "Marcar como Concluída",
"name": "Nome",
"notes": "Notas",
"notesPlaceholder": "Adicione uma nota (opcional)",
"pause": "Pausar",
"priority": "Prioridade",
"resume": "Retomar",
"viewDetails": "Ver Detalhes",
"stats": {
"activeGoals": "Metas Ativas",
"completedGoals": "Metas Concluídas",
"overallProgress": "Progresso Geral",
"totalGoals": "Total de Metas",
"totalSaved": "Total Economizado",
"totalTarget": "Objetivo Total"
},
"status": {
"active": "Ativa",
"advancing": "Avançando",
"cancelled": "Cancelada",
"completed": "Concluída",
"paused": "Pausada",
"starting": "Iniciando"
}
},
"reports": {
"accounts": "Contas",
"avgExpense": "Despesa Média",
"avgIncome": "Receita Média",
"balance": "Saldo",
"byCategory": "Por Categoria",
"byCostCenter": "Por Centro de Custo",
"comparison": "Comparação",
"custom": "Personalizado",
"dayOfWeek": {
"friday": "Sexta",
"monday": "Segunda",
"saturday": "Sábado",
"sunday": "Domingo",
"thursday": "Quinta",
"tuesday": "Terça",
"wednesday": "Quarta",
"day": "Dia"
},
"daysRemaining": "Dias Restantes",
"expenses": "Despesas",
"income": "Receitas",
"last3Months": "Últimos 3 Meses",
"last6Months": "Últimos 6 Meses",
"lastMonth": "Mês Passado",
"lastYear": "Ano Passado",
"monthlyEvolution": "Evolução Mensal",
"period": "Período",
"projectedExpense": "Despesa Projetada",
"projectedIncome": "Receita Projetada",
"projection": "Projeção",
"projectionTitle": "Projeção do Mês",
"recurring": "Recorrentes",
"liabilities": "Passivos",
"futureTransactions": "Futuras",
"overdue": "Vencidas",
"savingsRate": "Taxa de Poupança",
"selectPeriod": "Selecionar Período",
"subtitle": "Análise detalhada das suas finanças",
"summary": "Resumo",
"thisMonth": "Este Mês",
"thisYear": "Este Ano",
"title": "Relatórios",
"topExpenses": "Maiores Despesas",
"vsAverage": "vs Média",
"vsLastPeriod": "vs Período Anterior",
"yearComparison": "Comparativo Anual",
"expenseDistribution": "Distribuição de Despesas",
"categoryDetail": "Detalhes por Categoria",
"category": "Categoria",
"amount": "Valor",
"description": "Descrição",
"date": "Data",
"top20Expenses": "Top 20 Despesas do Mês",
"expensesByDayOfWeek": "Despesas por Dia da Semana",
"totalSpent": "Total gasto",
"totalIncome": "Total Receitas",
"totalExpense": "Total Despesas",
"totalRecurring": "Total Recorrentes",
"monthlyIncome": "Receita Mensal",
"monthlyExpense": "Despesa Mensal",
"netRecurring": "Saldo Recorrente",
"recurringList": "Lista de Recorrentes",
"nextDate": "Próxima Data",
"totalLiabilities": "Total Passivos",
"totalDebt": "Dívida Total",
"totalPaid": "Total Pago",
"totalPending": "Total Pendente",
"overdueInstallments": "parcelas vencidas",
"installments": "parcelas",
"paid": "Pago",
"pending": "Pendente",
"nextInstallment": "Próxima Parcela",
"totalTransactions": "Total Transações",
"futureIncome": "Receitas Futuras",
"futureExpense": "Despesas Futuras",
"netImpact": "Impacto Líquido",
"next30Days": "Próximos 30 Dias",
"account": "Conta",
"totalOverdue": "Total Vencidos",
"overdueAmount": "Valor Vencido",
"noOverdue": "Sem Vencidos!",
"noOverdueDescription": "Você não tem pagamentos vencidos. Excelente gestão!",
"overdueList": "Lista de Vencidos",
"dueDate": "Data de Vencimento",
"daysOverdue": "Dias de Atraso",
"historicalAverage": "Média histórica",
"monthProjection": "Projeção do mês",
"last3Months": "últimos 3 meses",
"currentMonth": "Mês Atual"
},
"months": {
"january": "Janeiro",
"february": "Fevereiro",
"march": "Março",
"april": "Abril",
"may": "Maio",
"june": "Junho",
"july": "Julho",
"august": "Agosto",
"september": "Setembro",
"october": "Outubro",
"november": "Novembro",
"december": "Dezembro",
"jan": "Jan",
"feb": "Fev",
"mar": "Mar",
"apr": "Abr",
"mayShort": "Mai",
"jun": "Jun",
"jul": "Jul",
"aug": "Ago",
"sep": "Set",
"oct": "Out",
"nov": "Nov",
"dec": "Dez"
}
}
}

View File

@ -390,9 +390,9 @@ a {
}
.sidebar-link-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
line-height: 1.3;
word-wrap: break-word;
}
/* Sidebar Groups */

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { budgetService, categoryService } from '../services/api';
import useFormatters from '../hooks/useFormatters';
import { getCurrencyByCode } from '../config/currencies';
import ConfirmModal from '../components/ConfirmModal';
const Budgets = () => {
@ -18,26 +19,30 @@ const Budgets = () => {
const [editingBudget, setEditingBudget] = useState(null);
const [deleteBudget, setDeleteBudget] = useState(null);
const [yearSummary, setYearSummary] = useState(null);
const [primaryCurrency, setPrimaryCurrency] = useState('EUR');
const [formData, setFormData] = useState({
category_id: '',
amount: '',
});
const months = [
{ value: 1, label: 'Enero' },
{ value: 2, label: 'Febrero' },
{ value: 3, label: 'Marzo' },
{ value: 4, label: 'Abril' },
{ value: 5, label: 'Mayo' },
{ value: 6, label: 'Junio' },
{ value: 7, label: 'Julio' },
{ value: 8, label: 'Agosto' },
{ value: 9, label: 'Septiembre' },
{ value: 10, label: 'Octubre' },
{ value: 11, label: 'Noviembre' },
{ value: 12, label: 'Diciembre' },
// 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]);
@ -48,13 +53,28 @@ const Budgets = () => {
const [budgetsData, categoriesData, availableData, summaryData] = await Promise.all([
budgetService.getAll({ year, month }),
categoryService.getAll(),
budgetService.getAvailableCategories(year, month),
budgetService.getYearSummary(year),
budgetService.getAvailableCategories({ year, month }),
budgetService.getYearSummary({ year }),
]);
setBudgets(budgetsData);
setCategories(categoriesData.filter(c => c.type === 'debit'));
setAvailableCategories(availableData);
setYearSummary(summaryData);
// 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 : []);
} catch (error) {
console.error('Error loading budgets:', error);
} finally {
@ -140,14 +160,47 @@ const Budgets = () => {
return '#10b981';
};
// Calculate totals
// 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: budgets.reduce((sum, b) => sum + parseFloat(b.amount), 0),
spent: budgets.reduce((sum, b) => sum + parseFloat(b.spent_amount || 0), 0),
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 (
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: '400px' }}>
@ -249,7 +302,7 @@ const Budgets = () => {
<div className="card border-0" style={{ background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' }}>
<div className="card-body text-white py-3">
<small className="opacity-75">{t('budgets.totalBudgeted')}</small>
<h4 className="mb-0">{currency(totals.budgeted)}</h4>
<h5 className="mb-0">{formatTotalsByCurrency('budgeted')}</h5>
</div>
</div>
</div>
@ -257,7 +310,7 @@ const Budgets = () => {
<div className="card border-0" style={{ background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)' }}>
<div className="card-body text-white py-3">
<small className="opacity-75">{t('budgets.totalSpent')}</small>
<h4 className="mb-0">{currency(totals.spent)}</h4>
<h5 className="mb-0">{formatTotalsByCurrency('spent')}</h5>
</div>
</div>
</div>
@ -272,7 +325,7 @@ const Budgets = () => {
>
<div className="card-body text-white py-3">
<small className="opacity-75">{t('budgets.remaining')}</small>
<h4 className="mb-0">{currency(totals.remaining)}</h4>
<h5 className="mb-0">{formatTotalsByCurrency('remaining')}</h5>
</div>
</div>
</div>
@ -336,7 +389,7 @@ const Budgets = () => {
></i>
</div>
<div>
<h6 className="text-white mb-0">{budget.category?.name || 'Sin categoría'}</h6>
<h6 className="text-white mb-0">{budget.category?.name || t('budgets.noCategory')}</h6>
</div>
</div>
<div className="dropdown">
@ -377,9 +430,9 @@ const Budgets = () => {
</div>
<div className="d-flex justify-content-between mb-2">
<span className={`fw-bold ${isExceeded ? 'text-danger' : 'text-white'}`}>
{currency(spent)}
{currency(spent, budget.currency || primaryCurrency)}
</span>
<span className="text-white">{currency(amount)}</span>
<span className="text-white">{currency(amount, budget.currency || primaryCurrency)}</span>
</div>
<div className="progress bg-slate-700" style={{ height: '8px' }}>
<div
@ -397,7 +450,7 @@ const Budgets = () => {
<div>
<small className="text-slate-400 d-block">{t('budgets.remaining')}</small>
<span className={`fw-bold ${remaining >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(remaining)}
{currency(remaining, budget.currency || primaryCurrency)}
</span>
</div>
<div className="text-end">
@ -416,7 +469,7 @@ const Budgets = () => {
<div className="alert alert-danger py-2 mt-3 mb-0">
<small>
<i className="bi bi-exclamation-triangle me-1"></i>
{t('budgets.exceeded')} {currency(Math.abs(remaining))}
{t('budgets.exceeded')} {currency(Math.abs(remaining), budget.currency || primaryCurrency)}
</small>
</div>
)}
@ -471,13 +524,13 @@ const Budgets = () => {
<td>
{monthName}
{isCurrentMonth && (
<span className="badge bg-primary ms-2">Actual</span>
<span className="badge bg-primary ms-2">{t('budgets.currentMonth')}</span>
)}
</td>
<td className="text-end">{currency(item.budgeted)}</td>
<td className="text-end text-danger">{currency(item.spent)}</td>
<td className="text-end">{currency(item.budgeted, primaryCurrency)}</td>
<td className="text-end text-danger">{currency(item.spent, primaryCurrency)}</td>
<td className={`text-end ${item.remaining >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(item.remaining)}
{currency(item.remaining, primaryCurrency)}
</td>
<td className="text-end">
<span
@ -504,7 +557,7 @@ const Budgets = () => {
{/* Budget Form Modal */}
{showModal && (
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.7)' }}>
<div className="modal-dialog modal-dialog-centered modal-sm">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content border-0" style={{ background: '#1e293b' }}>
<div className="modal-header border-0">
<h5 className="modal-title text-white">
@ -520,43 +573,98 @@ const Budgets = () => {
<form onSubmit={handleSubmit}>
<div className="modal-body">
<p className="text-slate-400 small mb-3">
<i className="bi bi-calendar3 me-1"></i>
{months.find(m => m.value === month)?.label} {year}
</p>
{/* Category */}
{/* Category Selection - Grid style like transactions */}
<div className="mb-3">
<label className="form-label text-slate-400">{t('budgets.category')} *</label>
<select
className="form-select bg-dark border-secondary text-white"
value={formData.category_id}
onChange={(e) => setFormData({...formData, category_id: e.target.value})}
required
disabled={editingBudget}
>
<option value="">{t('budgets.selectCategory')}</option>
{(editingBudget ? categories : availableCategories).map(cat => (
<option key={cat.id} value={cat.id}>
{cat.name}
</option>
))}
</select>
{editingBudget ? (
<input
type="text"
className="form-control bg-dark border-secondary text-white"
value={editingBudget.category?.name || ''}
disabled
/>
) : (
<>
{availableCategories.length === 0 ? (
<div className="alert alert-warning py-2 mb-0">
<i className="bi bi-info-circle me-2"></i>
{t('budgets.allCategoriesUsed')}
</div>
) : (
<div className="category-grid" style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
gap: '8px',
maxHeight: '300px',
overflowY: 'auto',
padding: '4px'
}}>
{availableCategories.map(cat => (
<div
key={cat.id}
onClick={() => setFormData({...formData, category_id: cat.id})}
className={`p-2 rounded text-center cursor-pointer ${
formData.category_id == cat.id
? 'border border-primary'
: 'border border-secondary'
}`}
style={{
background: formData.category_id == cat.id ? 'rgba(59, 130, 246, 0.2)' : '#0f172a',
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
<i
className={`bi ${cat.icon || 'bi-tag'} d-block mb-1`}
style={{
fontSize: '1.5rem',
color: cat.color || '#6b7280'
}}
></i>
<small className="text-white d-block text-truncate" title={cat.name}>
{cat.name}
</small>
</div>
))}
</div>
)}
</>
)}
</div>
{/* Amount */}
<div className="mb-3">
<label className="form-label text-slate-400">{t('budgets.amount')} *</label>
<div className="input-group">
<span className="input-group-text bg-dark border-secondary text-white"></span>
<span className="input-group-text bg-dark border-secondary text-white">
{getCurrencyByCode(primaryCurrency)?.symbol || '€'}
</span>
<input
type="number"
step="0.01"
min="0.01"
className="form-control bg-dark border-secondary text-white"
value={formData.amount}
onChange={(e) => setFormData({...formData, amount: e.target.value})}
placeholder="0.00"
required
/>
</div>
</div>
{/* Info about auto-propagation */}
{!editingBudget && (
<div className="alert alert-info py-2 mb-0">
<small>
<i className="bi bi-info-circle me-1"></i>
{t('budgets.autoPropagateInfo')}
</small>
</div>
)}
</div>
<div className="modal-footer border-0">
<button
@ -566,7 +674,11 @@ const Budgets = () => {
>
{t('common.cancel')}
</button>
<button type="submit" className="btn btn-primary">
<button
type="submit"
className="btn btn-primary"
disabled={!editingBudget && (!formData.category_id || !formData.amount)}
>
<i className="bi bi-check-lg me-1"></i>
{t('common.save')}
</button>

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ import ConfirmModal from '../components/ConfirmModal';
const Goals = () => {
const { t } = useTranslation();
const { currency, formatDate } = useFormatters();
const { currency, date } = useFormatters();
const [loading, setLoading] = useState(true);
const [goals, setGoals] = useState([]);
@ -46,8 +46,10 @@ const Goals = () => {
const loadGoals = async () => {
setLoading(true);
try {
const data = await financialGoalService.getAll();
setGoals(data);
const response = await financialGoalService.getAll();
// El API devuelve { data: [], stats: {} }
const goalsData = response?.data || response;
setGoals(Array.isArray(goalsData) ? goalsData : []);
} catch (error) {
console.error('Error loading goals:', error);
} finally {
@ -156,13 +158,34 @@ const Goals = () => {
return <span className={`badge ${config.bg}`}>{config.label}</span>;
};
// Stats calculation
// Stats calculation - usar array seguro
const safeGoals = Array.isArray(goals) ? goals : [];
// Agrupar totales por moneda
const totalsByCurrency = safeGoals.reduce((acc, g) => {
const curr = g.currency || 'EUR';
if (!acc[curr]) {
acc[curr] = { target: 0, current: 0 };
}
acc[curr].target += parseFloat(g.target_amount || 0);
acc[curr].current += parseFloat(g.current_amount || 0);
return acc;
}, {});
const stats = {
total: goals.length,
active: goals.filter(g => g.status === 'active').length,
completed: goals.filter(g => g.status === 'completed').length,
totalTarget: goals.reduce((sum, g) => sum + parseFloat(g.target_amount), 0),
totalCurrent: goals.reduce((sum, g) => sum + parseFloat(g.current_amount), 0),
total: safeGoals.length,
active: safeGoals.filter(g => g.status === 'active').length,
completed: safeGoals.filter(g => g.status === 'completed').length,
byCurrency: totalsByCurrency,
};
// Formatear totales por moneda para mostrar
const formatTotalsByCurrency = (type) => {
const entries = Object.entries(totalsByCurrency);
if (entries.length === 0) return currency(0, 'EUR');
return entries.map(([curr, vals]) =>
currency(type === 'current' ? vals.current : type === 'target' ? vals.target : vals.target - vals.current, curr)
).join(' + ');
};
if (loading) {
@ -214,7 +237,7 @@ const Goals = () => {
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body text-center py-3">
<small className="text-slate-400">{t('goals.totalSaved')}</small>
<h4 className="text-success mb-0">{currency(stats.totalCurrent)}</h4>
<h5 className="text-success mb-0">{formatTotalsByCurrency('current')}</h5>
</div>
</div>
</div>
@ -222,14 +245,14 @@ const Goals = () => {
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body text-center py-3">
<small className="text-slate-400">{t('goals.remaining')}</small>
<h4 className="text-warning mb-0">{currency(stats.totalTarget - stats.totalCurrent)}</h4>
<h5 className="text-warning mb-0">{formatTotalsByCurrency('remaining')}</h5>
</div>
</div>
</div>
</div>
{/* Goals Grid */}
{goals.length === 0 ? (
{safeGoals.length === 0 ? (
<div className="card border-0 text-center py-5" style={{ background: '#0f172a' }}>
<div className="card-body">
<i className="bi bi-flag text-slate-500" style={{ fontSize: '4rem' }}></i>
@ -243,7 +266,7 @@ const Goals = () => {
</div>
) : (
<div className="row g-4">
{goals.map(goal => {
{safeGoals.map(goal => {
const progress = goal.progress_percentage ||
((goal.current_amount / goal.target_amount) * 100);
const remaining = goal.remaining_amount ||
@ -284,8 +307,8 @@ const Goals = () => {
{/* Progress */}
<div className="mb-3">
<div className="d-flex justify-content-between mb-1">
<span className="text-success fw-bold">{currency(goal.current_amount)}</span>
<span className="text-slate-400">{currency(goal.target_amount)}</span>
<span className="text-success fw-bold">{currency(goal.current_amount, goal.currency)}</span>
<span className="text-slate-400">{currency(goal.target_amount, goal.currency)}</span>
</div>
<div className="progress bg-slate-700" style={{ height: '8px' }}>
<div
@ -299,7 +322,7 @@ const Goals = () => {
<div className="d-flex justify-content-between mt-1">
<small className="text-slate-400">{progress.toFixed(1)}%</small>
<small className="text-slate-400">
{t('goals.remaining')}: {currency(remaining)}
{t('goals.remaining')}: {currency(remaining, goal.currency)}
</small>
</div>
</div>
@ -309,13 +332,13 @@ const Goals = () => {
{goal.target_date && (
<div className="d-flex justify-content-between small mb-1">
<span className="text-slate-400">{t('goals.targetDate')}</span>
<span className="text-white">{formatDate(goal.target_date)}</span>
<span className="text-white">{date(goal.target_date)}</span>
</div>
)}
{goal.monthly_contribution > 0 && (
<div className="d-flex justify-content-between small mb-1">
<span className="text-slate-400">{t('goals.monthlyContribution')}</span>
<span className="text-white">{currency(goal.monthly_contribution)}</span>
<span className="text-white">{currency(goal.monthly_contribution, goal.currency)}</span>
</div>
)}
{goal.months_remaining > 0 && (
@ -333,7 +356,7 @@ const Goals = () => {
<i className={`bi ${goal.is_on_track ? 'bi-check-circle' : 'bi-exclamation-triangle'} me-1`}></i>
{goal.is_on_track
? t('goals.onTrack')
: t('goals.needsMore', { amount: currency(goal.required_monthly_saving || 0) })
: t('goals.needsMore', { amount: currency(goal.required_monthly_saving || 0, goal.currency) })
}
</small>
</div>
@ -593,12 +616,12 @@ const Goals = () => {
></i>
<h6 className="text-white mt-2">{contributingGoal.name}</h6>
<small className="text-slate-400">
{currency(contributingGoal.current_amount)} de {currency(contributingGoal.target_amount)}
{currency(contributingGoal.current_amount, contributingGoal.currency)} de {currency(contributingGoal.target_amount, contributingGoal.currency)}
</small>
</div>
<div className="mb-3">
<label className="form-label text-slate-400">{t('goals.contributeAmount')} *</label>
<label className="form-label text-slate-400">{t('goals.contributeAmount')} ({contributingGoal.currency || 'EUR'}) *</label>
<input
type="number"
step="0.01"

View File

@ -47,6 +47,11 @@ const Reports = () => {
const [topExpenses, setTopExpenses] = useState(null);
const [projection, setProjection] = useState(null);
const [comparison, setComparison] = useState(null);
const [costCenterData, setCostCenterData] = useState(null);
const [recurringData, setRecurringData] = useState(null);
const [liabilitiesData, setLiabilitiesData] = useState(null);
const [futureData, setFutureData] = useState(null);
const [overdueData, setOverdueData] = useState(null);
// Load data based on active tab
const loadData = useCallback(async () => {
@ -81,6 +86,26 @@ const Reports = () => {
const compRes = await reportService.comparePeriods();
setComparison(compRes);
break;
case 'costCenter':
const ccRes = await reportService.getByCostCenter();
setCostCenterData(ccRes);
break;
case 'recurring':
const recRes = await reportService.getRecurringReport();
setRecurringData(recRes);
break;
case 'liabilities':
const liabRes = await reportService.getLiabilities();
setLiabilitiesData(liabRes);
break;
case 'future':
const futRes = await reportService.getFutureTransactions({ days: 30 });
setFutureData(futRes);
break;
case 'overdue':
const overdueRes = await reportService.getOverdue();
setOverdueData(overdueRes);
break;
}
} catch (error) {
console.error('Error loading report data:', error);
@ -96,11 +121,15 @@ const Reports = () => {
const tabs = [
{ id: 'summary', label: t('reports.summary'), icon: 'bi-clipboard-data' },
{ id: 'category', label: t('reports.byCategory'), icon: 'bi-pie-chart' },
{ id: 'costCenter', label: t('reports.byCostCenter'), icon: 'bi-diagram-3' },
{ id: 'evolution', label: t('reports.monthlyEvolution'), icon: 'bi-graph-up' },
{ id: 'comparison', label: t('reports.comparison'), icon: 'bi-arrow-left-right' },
{ id: 'topExpenses', label: t('reports.topExpenses'), icon: 'bi-sort-down' },
{ id: 'projection', label: t('reports.projection'), icon: 'bi-lightning' },
{ id: 'dayOfWeek', label: 'Por día', icon: 'bi-calendar-week' },
{ id: 'recurring', label: t('reports.recurring'), icon: 'bi-arrow-repeat' },
{ id: 'liabilities', label: t('reports.liabilities'), icon: 'bi-credit-card' },
{ id: 'future', label: t('reports.futureTransactions'), icon: 'bi-calendar-plus' },
{ id: 'overdue', label: t('reports.overdue'), icon: 'bi-exclamation-triangle' },
];
// Chart options
@ -161,7 +190,7 @@ const Reports = () => {
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #059669 0%, #047857 100%)' }}>
<div className="card-body text-white">
<h6 className="opacity-75">{t('reports.income')} {year}</h6>
<h3 className="mb-2">{currency(summary.current.income)}</h3>
<h3 className="mb-2">{currency(summary.current.income, summary.currency)}</h3>
{summary.variation.income !== 0 && (
<span className={`badge ${summary.variation.income >= 0 ? 'bg-success' : 'bg-danger'}`}>
<i className={`bi bi-arrow-${summary.variation.income >= 0 ? 'up' : 'down'} me-1`}></i>
@ -176,7 +205,7 @@ const Reports = () => {
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)' }}>
<div className="card-body text-white">
<h6 className="opacity-75">{t('reports.expenses')} {year}</h6>
<h3 className="mb-2">{currency(summary.current.expense)}</h3>
<h3 className="mb-2">{currency(summary.current.expense, summary.currency)}</h3>
{summary.variation.expense !== 0 && (
<span className={`badge ${summary.variation.expense <= 0 ? 'bg-success' : 'bg-danger'}`}>
<i className={`bi bi-arrow-${summary.variation.expense >= 0 ? 'up' : 'down'} me-1`}></i>
@ -191,9 +220,9 @@ const Reports = () => {
<div className="card border-0 h-100" style={{ background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' }}>
<div className="card-body text-white">
<h6 className="opacity-75">{t('reports.balance')} {year}</h6>
<h3 className="mb-2">{currency(summary.current.balance)}</h3>
<h3 className="mb-2">{currency(summary.current.balance, summary.currency)}</h3>
<span className="small opacity-75">
Tasa de ahorro: {summary.current.income > 0
{t('reports.savingsRate')}: {summary.current.income > 0
? ((summary.current.balance / summary.current.income) * 100).toFixed(1)
: 0}%
</span>
@ -207,13 +236,13 @@ const Reports = () => {
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-bar-chart me-2"></i>
Comparativa Anual
{t('reports.yearComparison')}
</h6>
</div>
<div className="card-body" style={{ height: '300px' }}>
<Bar
data={{
labels: ['Ingresos', 'Gastos', 'Balance'],
labels: [t('common.incomes'), t('common.expenses'), t('common.balance')],
datasets: [
{
label: String(year - 1),
@ -255,7 +284,7 @@ const Reports = () => {
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-pie-chart me-2"></i>
Distribución de Gastos
{t('reports.expenseDistribution')}
</h6>
</div>
<div className="card-body" style={{ height: '400px' }}>
@ -279,16 +308,16 @@ const Reports = () => {
<div className="card-header border-0 bg-transparent d-flex justify-content-between">
<h6 className="text-white mb-0">
<i className="bi bi-list-ol me-2"></i>
Detalle por Categoría
{t('reports.categoryDetail')}
</h6>
<span className="text-success fw-bold">{currency(categoryData.total)}</span>
<span className="text-success fw-bold">{currency(categoryData.total, categoryData.currency)}</span>
</div>
<div className="card-body p-0" style={{ maxHeight: '400px', overflowY: 'auto' }}>
<table className="table table-dark table-hover mb-0">
<thead className="sticky-top" style={{ background: '#1e293b' }}>
<tr>
<th>Categoría</th>
<th className="text-end">Total</th>
<th>{t('reports.category')}</th>
<th className="text-end">{t('common.total')}</th>
<th className="text-end">%</th>
</tr>
</thead>
@ -299,7 +328,7 @@ const Reports = () => {
<i className={`bi ${cat.icon} me-2`} style={{ color: colors[i] }}></i>
{cat.category_name}
</td>
<td className="text-end">{currency(cat.total)}</td>
<td className="text-end">{currency(cat.total, categoryData.currency)}</td>
<td className="text-end">
<span className="badge bg-secondary">{cat.percentage}%</span>
</td>
@ -328,7 +357,7 @@ const Reports = () => {
className={`btn btn-sm ${months === m ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => setMonths(m)}
>
{m} meses
{m} {t('common.months')}
</button>
))}
</div>
@ -339,7 +368,7 @@ const Reports = () => {
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.avgIncome')}</small>
<h5 className="text-success mb-0">{currency(evolutionData.averages.income)}</h5>
<h5 className="text-success mb-0">{currency(evolutionData.averages.income, evolutionData.currency)}</h5>
</div>
</div>
</div>
@ -347,7 +376,7 @@ const Reports = () => {
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.avgExpense')}</small>
<h5 className="text-danger mb-0">{currency(evolutionData.averages.expense)}</h5>
<h5 className="text-danger mb-0">{currency(evolutionData.averages.expense, evolutionData.currency)}</h5>
</div>
</div>
</div>
@ -356,7 +385,7 @@ const Reports = () => {
<div className="card-body">
<small className="text-slate-400">{t('reports.balance')}</small>
<h5 className={`mb-0 ${evolutionData.averages.balance >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(evolutionData.averages.balance)}
{currency(evolutionData.averages.balance, evolutionData.currency)}
</h5>
</div>
</div>
@ -470,16 +499,16 @@ const Reports = () => {
<div className="card-body">
<div className="d-flex justify-content-between mb-3">
<span className="text-slate-400">{t('reports.income')}</span>
<span className="text-success">{currency(comparison.period2.income)}</span>
<span className="text-success">{currency(comparison.period2.income, comparison.currency)}</span>
</div>
<div className="d-flex justify-content-between mb-3">
<span className="text-slate-400">{t('reports.expenses')}</span>
<span className="text-danger">{currency(comparison.period2.expense)}</span>
<span className="text-danger">{currency(comparison.period2.expense, comparison.currency)}</span>
</div>
<div className="d-flex justify-content-between">
<span className="text-slate-400">{t('reports.balance')}</span>
<span className={comparison.period2.balance >= 0 ? 'text-success' : 'text-danger'}>
{currency(comparison.period2.balance)}
{currency(comparison.period2.balance, comparison.currency)}
</span>
</div>
</div>
@ -491,14 +520,14 @@ const Reports = () => {
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
{comparison.period1.label}
<span className="badge bg-primary ms-2">Actual</span>
<span className="badge bg-primary ms-2">{t('common.current')}</span>
</h6>
</div>
<div className="card-body">
<div className="d-flex justify-content-between mb-3">
<span className="text-slate-400">{t('reports.income')}</span>
<div>
<span className="text-success">{currency(comparison.period1.income)}</span>
<span className="text-success">{currency(comparison.period1.income, comparison.currency)}</span>
{comparison.variation.income !== 0 && (
<span className={`badge ms-2 ${comparison.variation.income >= 0 ? 'bg-success' : 'bg-danger'}`}>
{comparison.variation.income > 0 ? '+' : ''}{comparison.variation.income}%
@ -509,7 +538,7 @@ const Reports = () => {
<div className="d-flex justify-content-between mb-3">
<span className="text-slate-400">{t('reports.expenses')}</span>
<div>
<span className="text-danger">{currency(comparison.period1.expense)}</span>
<span className="text-danger">{currency(comparison.period1.expense, comparison.currency)}</span>
{comparison.variation.expense !== 0 && (
<span className={`badge ms-2 ${comparison.variation.expense <= 0 ? 'bg-success' : 'bg-danger'}`}>
{comparison.variation.expense > 0 ? '+' : ''}{comparison.variation.expense}%
@ -520,7 +549,7 @@ const Reports = () => {
<div className="d-flex justify-content-between">
<span className="text-slate-400">{t('reports.balance')}</span>
<span className={comparison.period1.balance >= 0 ? 'text-success' : 'text-danger'}>
{currency(comparison.period1.balance)}
{currency(comparison.period1.balance, comparison.currency)}
</span>
</div>
</div>
@ -567,9 +596,9 @@ const Reports = () => {
<div className="card-header border-0 bg-transparent d-flex justify-content-between">
<h6 className="text-white mb-0">
<i className="bi bi-sort-down me-2"></i>
Top 20 Gastos del Mes
{t('reports.top20Expenses')}
</h6>
<span className="text-danger fw-bold">{currency(topExpenses.total)}</span>
<span className="text-danger fw-bold">{currency(topExpenses.total, topExpenses.currency)}</span>
</div>
<div className="card-body p-0">
<div className="table-responsive">
@ -577,20 +606,20 @@ const Reports = () => {
<thead>
<tr>
<th>#</th>
<th>Descripción</th>
<th>Categoría</th>
<th>Fecha</th>
<th className="text-end">Monto</th>
<th>{t('reports.description')}</th>
<th>{t('reports.category')}</th>
<th>{t('reports.date')}</th>
<th className="text-end">{t('reports.amount')}</th>
</tr>
</thead>
<tbody>
{topExpenses.data.map((t, i) => (
<tr key={t.id}>
{topExpenses.data.map((item, i) => (
<tr key={item.id}>
<td><span className="badge bg-secondary">{i + 1}</span></td>
<td className="text-truncate" style={{ maxWidth: '200px' }}>{t.description}</td>
<td><span className="badge bg-primary">{t.category || '-'}</span></td>
<td className="text-slate-400">{t.date}</td>
<td className="text-end text-danger fw-bold">{currency(t.amount)}</td>
<td className="text-truncate" style={{ maxWidth: '200px' }}>{item.description}</td>
<td><span className="badge bg-primary">{item.category || '-'}</span></td>
<td className="text-slate-400">{item.date}</td>
<td className="text-end text-danger fw-bold">{currency(item.amount, item.currency || topExpenses.currency)}</td>
</tr>
))}
</tbody>
@ -612,22 +641,22 @@ const Reports = () => {
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-calendar3 me-2"></i>
Mes Actual
{t('reports.currentMonth')}
</h6>
</div>
<div className="card-body">
<div className="d-flex justify-content-between mb-3">
<span className="text-slate-400">{t('reports.income')}</span>
<span className="text-success">{currency(projection.current_month.income)}</span>
<span className="text-success">{currency(projection.current_month.income, projection.currency)}</span>
</div>
<div className="d-flex justify-content-between mb-3">
<span className="text-slate-400">{t('reports.expenses')}</span>
<span className="text-danger">{currency(projection.current_month.expense)}</span>
<span className="text-danger">{currency(projection.current_month.expense, projection.currency)}</span>
</div>
<hr className="border-secondary" />
<div className="d-flex justify-content-between">
<span className="text-slate-400">{t('reports.daysRemaining')}</span>
<span className="text-white">{projection.current_month.days_remaining} días</span>
<span className="text-white">{projection.current_month.days_remaining} {t('common.days')}</span>
</div>
</div>
</div>
@ -644,17 +673,17 @@ const Reports = () => {
<div className="card-body text-white">
<div className="d-flex justify-content-between mb-3">
<span className="opacity-75">{t('reports.projectedIncome')}</span>
<span className="fw-bold">{currency(projection.projection.income)}</span>
<span className="fw-bold">{currency(projection.projection.income, projection.currency)}</span>
</div>
<div className="d-flex justify-content-between mb-3">
<span className="opacity-75">{t('reports.projectedExpense')}</span>
<span className="fw-bold">{currency(projection.projection.expense)}</span>
<span className="fw-bold">{currency(projection.projection.expense, projection.currency)}</span>
</div>
<hr className="border-white opacity-25" />
<div className="d-flex justify-content-between">
<span className="opacity-75">{t('reports.balance')}</span>
<span className={`fw-bold ${projection.projection.balance >= 0 ? '' : 'text-warning'}`}>
{currency(projection.projection.balance)}
{currency(projection.projection.balance, projection.currency)}
</span>
</div>
</div>
@ -667,7 +696,7 @@ const Reports = () => {
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-bar-chart me-2"></i>
{t('reports.vsAverage')} (últimos 3 meses)
{t('reports.vsAverage')} ({t('reports.last3Months')})
</h6>
</div>
<div className="card-body" style={{ height: '250px' }}>
@ -676,13 +705,13 @@ const Reports = () => {
labels: [t('reports.income'), t('reports.expenses')],
datasets: [
{
label: 'Promedio histórico',
label: t('reports.historicalAverage'),
data: [projection.historical_average.income, projection.historical_average.expense],
backgroundColor: 'rgba(148, 163, 184, 0.5)',
borderRadius: 4,
},
{
label: 'Proyección mes',
label: t('reports.monthProjection'),
data: [projection.projection.income, projection.projection.expense],
backgroundColor: ['rgba(16, 185, 129, 0.7)', 'rgba(239, 68, 68, 0.7)'],
borderRadius: 4,
@ -700,9 +729,8 @@ const Reports = () => {
// Render Day of Week Tab
const renderDayOfWeek = () => {
if (!dayOfWeekData) return null;
const days = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'];
if (!dayOfWeekData || !dayOfWeekData.data) return null;
const data = dayOfWeekData.data;
return (
<div className="row g-4">
@ -711,17 +739,17 @@ const Reports = () => {
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-calendar-week me-2"></i>
Gastos por Día de la Semana
{t('reports.expensesByDayOfWeek')}
</h6>
</div>
<div className="card-body" style={{ height: '300px' }}>
<Bar
data={{
labels: dayOfWeekData.map(d => days[d.day_num - 1]),
labels: data.map(d => t(`reports.dayOfWeek.${d.day_key}`)),
datasets: [{
label: 'Total gastado',
data: dayOfWeekData.map(d => d.total),
backgroundColor: dayOfWeekData.map(d =>
label: t('reports.totalSpent'),
data: data.map(d => d.total),
backgroundColor: data.map(d =>
d.day_num === 1 || d.day_num === 7
? 'rgba(245, 158, 11, 0.7)'
: 'rgba(59, 130, 246, 0.7)'
@ -744,22 +772,22 @@ const Reports = () => {
<table className="table table-dark mb-0">
<thead>
<tr>
<th>Día</th>
<th className="text-center">Transacciones</th>
<th className="text-end">Total</th>
<th className="text-end">Promedio</th>
<th>{t('reports.dayOfWeek.day')}</th>
<th className="text-center">{t('transactions.title')}</th>
<th className="text-end">{t('common.total')}</th>
<th className="text-end">{t('reports.avgExpense')}</th>
</tr>
</thead>
<tbody>
{dayOfWeekData.map(d => (
{dayOfWeekData.data.map(d => (
<tr key={d.day_num}>
<td>
<i className={`bi bi-calendar3 me-2 ${d.day_num === 1 || d.day_num === 7 ? 'text-warning' : 'text-primary'}`}></i>
{d.day}
{t(`reports.dayOfWeek.${d.day_key}`)}
</td>
<td className="text-center"><span className="badge bg-secondary">{d.count}</span></td>
<td className="text-end text-danger">{currency(d.total)}</td>
<td className="text-end text-slate-400">{currency(d.average)}</td>
<td className="text-end text-danger">{currency(d.total, dayOfWeekData.currency)}</td>
<td className="text-end text-slate-400">{currency(d.average, dayOfWeekData.currency)}</td>
</tr>
))}
</tbody>
@ -771,6 +799,456 @@ const Reports = () => {
);
};
// Render Cost Center Tab
const renderCostCenter = () => {
if (!costCenterData) return null;
const data = costCenterData.data || [];
return (
<div className="row g-4">
<div className="col-md-4">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.totalIncome')}</small>
<h5 className="text-success mb-0">{currency(costCenterData.total_income || 0, costCenterData.currency)}</h5>
</div>
</div>
</div>
<div className="col-md-4">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.totalExpense')}</small>
<h5 className="text-danger mb-0">{currency(costCenterData.total_expense || 0, costCenterData.currency)}</h5>
</div>
</div>
</div>
<div className="col-md-4">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.balance')}</small>
<h5 className={`mb-0 ${(costCenterData.total_income - costCenterData.total_expense) >= 0 ? 'text-success' : 'text-danger'}`}>
{currency((costCenterData.total_income || 0) - (costCenterData.total_expense || 0), costCenterData.currency)}
</h5>
</div>
</div>
</div>
<div className="col-12">
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-diagram-3 me-2"></i>
{t('reports.byCostCenter')}
</h6>
</div>
<div className="card-body p-0">
<div className="table-responsive">
<table className="table table-dark table-hover mb-0">
<thead>
<tr>
<th>{t('costCenters.name')}</th>
<th className="text-end">{t('reports.income')}</th>
<th className="text-end">{t('reports.expenses')}</th>
<th className="text-end">{t('reports.balance')}</th>
</tr>
</thead>
<tbody>
{data.map(cc => (
<tr key={cc.id}>
<td>
<span className="me-2" style={{ color: cc.color || '#6b7280' }}></span>
{cc.name}
</td>
<td className="text-end text-success">{currency(cc.income, costCenterData.currency)}</td>
<td className="text-end text-danger">{currency(cc.expense, costCenterData.currency)}</td>
<td className={`text-end ${cc.balance >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(cc.balance, costCenterData.currency)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
};
// Render Recurring Tab
const renderRecurring = () => {
if (!recurringData) return null;
const templates = recurringData.templates || [];
const summary = recurringData.summary || {};
return (
<div className="row g-4">
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.totalRecurring')}</small>
<h4 className="text-white mb-0">{summary.total_recurring || 0}</h4>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.monthlyIncome')}</small>
<h5 className="text-success mb-0">{currency(summary.monthly_income || 0, recurringData.currency)}</h5>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.monthlyExpense')}</small>
<h5 className="text-danger mb-0">{currency(summary.monthly_expense || 0, recurringData.currency)}</h5>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.netRecurring')}</small>
<h5 className={`mb-0 ${(summary.net_recurring || 0) >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(summary.net_recurring || 0, recurringData.currency)}
</h5>
</div>
</div>
</div>
<div className="col-12">
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-arrow-repeat me-2"></i>
{t('reports.recurringList')}
</h6>
</div>
<div className="card-body p-0">
<div className="table-responsive">
<table className="table table-dark table-hover mb-0">
<thead>
<tr>
<th>{t('common.description')}</th>
<th>{t('reports.category')}</th>
<th>{t('recurring.frequency')}</th>
<th>{t('reports.nextDate')}</th>
<th className="text-end">{t('reports.amount')}</th>
</tr>
</thead>
<tbody>
{templates.map(t => (
<tr key={t.id}>
<td>{t.description}</td>
<td>
{t.category && (
<span className="badge" style={{ background: t.category_color || '#6b7280' }}>
<i className={`bi ${t.category_icon || 'bi-tag'} me-1`}></i>
{t.category}
</span>
)}
</td>
<td><span className="badge bg-secondary">{t.frequency}</span></td>
<td className="text-slate-400">{t.next_date}</td>
<td className={`text-end fw-bold ${t.type === 'credit' ? 'text-success' : 'text-danger'}`}>
{t.type === 'credit' ? '+' : '-'}{currency(t.amount, t.currency)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
};
// Render Liabilities Tab
const renderLiabilities = () => {
if (!liabilitiesData) return null;
const data = liabilitiesData.data || [];
const summary = liabilitiesData.summary || {};
return (
<div className="row g-4">
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.totalLiabilities')}</small>
<h4 className="text-white mb-0">{summary.total_liabilities || 0}</h4>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.totalDebt')}</small>
<h5 className="text-danger mb-0">{currency(summary.total_debt || 0, liabilitiesData.currency)}</h5>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.totalPaid')}</small>
<h5 className="text-success mb-0">{currency(summary.total_paid || 0, liabilitiesData.currency)}</h5>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.totalPending')}</small>
<h5 className="text-warning mb-0">{currency(summary.total_pending || 0, liabilitiesData.currency)}</h5>
</div>
</div>
</div>
{data.map(liability => (
<div key={liability.id} className="col-md-6">
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-body">
<div className="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 className="text-white mb-1">{liability.name}</h6>
<span className="badge bg-secondary">{liability.type}</span>
</div>
{liability.overdue_installments > 0 && (
<span className="badge bg-danger">
{liability.overdue_installments} {t('reports.overdueInstallments')}
</span>
)}
</div>
<div className="progress mb-3" style={{ height: '8px' }}>
<div
className="progress-bar bg-success"
style={{ width: `${liability.progress || 0}%` }}
></div>
</div>
<div className="d-flex justify-content-between text-sm">
<span className="text-slate-400">
{liability.paid_installments}/{liability.total_installments} {t('reports.installments')}
</span>
<span className="text-success">{liability.progress?.toFixed(1)}%</span>
</div>
<hr className="border-secondary my-3" />
<div className="row text-center">
<div className="col-4">
<small className="text-slate-400 d-block">{t('common.total')}</small>
<span className="text-white">{currency(liability.total_amount, liability.currency)}</span>
</div>
<div className="col-4">
<small className="text-slate-400 d-block">{t('reports.paid')}</small>
<span className="text-success">{currency(liability.paid_amount, liability.currency)}</span>
</div>
<div className="col-4">
<small className="text-slate-400 d-block">{t('reports.pending')}</small>
<span className="text-warning">{currency(liability.pending_amount, liability.currency)}</span>
</div>
</div>
{liability.next_installment && (
<div className={`mt-3 p-2 rounded ${liability.next_installment.is_overdue ? 'bg-danger bg-opacity-25' : 'bg-primary bg-opacity-25'}`}>
<small className="text-slate-400">{t('reports.nextInstallment')}:</small>
<div className="d-flex justify-content-between">
<span className="text-white">{liability.next_installment.due_date}</span>
<span className={liability.next_installment.is_overdue ? 'text-danger' : 'text-primary'}>
{currency(liability.next_installment.amount, liability.currency)}
</span>
</div>
</div>
)}
</div>
</div>
</div>
))}
</div>
);
};
// Render Future Transactions Tab
const renderFuture = () => {
if (!futureData) return null;
const data = futureData.data || [];
const summary = futureData.summary || {};
return (
<div className="row g-4">
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.totalTransactions')}</small>
<h4 className="text-white mb-0">{summary.total_transactions || 0}</h4>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.futureIncome')}</small>
<h5 className="text-success mb-0">{currency(summary.total_income || 0, futureData.currency)}</h5>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.futureExpense')}</small>
<h5 className="text-danger mb-0">{currency(summary.total_expense || 0, futureData.currency)}</h5>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.netImpact')}</small>
<h5 className={`mb-0 ${(summary.net_impact || 0) >= 0 ? 'text-success' : 'text-danger'}`}>
{currency(summary.net_impact || 0, futureData.currency)}
</h5>
</div>
</div>
</div>
<div className="col-12">
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-calendar-plus me-2"></i>
{t('reports.next30Days')}
</h6>
</div>
<div className="card-body p-0">
<div className="table-responsive">
<table className="table table-dark table-hover mb-0">
<thead>
<tr>
<th>{t('reports.date')}</th>
<th>{t('common.description')}</th>
<th>{t('reports.category')}</th>
<th>{t('reports.account')}</th>
<th className="text-end">{t('reports.amount')}</th>
</tr>
</thead>
<tbody>
{data.map(tx => (
<tr key={tx.id}>
<td>
<span className="badge bg-primary">{tx.days_until}d</span>
<span className="text-slate-400 ms-2">{tx.date}</span>
</td>
<td>{tx.description}</td>
<td>
{tx.category && (
<span className="badge bg-secondary">
<i className={`bi ${tx.category_icon || 'bi-tag'} me-1`}></i>
{tx.category}
</span>
)}
</td>
<td className="text-slate-400">{tx.account}</td>
<td className={`text-end fw-bold ${tx.type === 'credit' ? 'text-success' : 'text-danger'}`}>
{tx.type === 'credit' ? '+' : '-'}{currency(tx.amount, tx.currency)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
};
// Render Overdue Tab
const renderOverdue = () => {
if (!overdueData) return null;
const data = overdueData.data || [];
const summary = overdueData.summary || {};
return (
<div className="row g-4">
<div className="col-md-6">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.totalOverdue')}</small>
<h4 className="text-danger mb-0">{summary.total_overdue || 0}</h4>
</div>
</div>
</div>
<div className="col-md-6">
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
<div className="card-body">
<small className="text-slate-400">{t('reports.overdueAmount')}</small>
<h5 className="text-danger mb-0">{currency(summary.total_amount || 0, overdueData.currency)}</h5>
</div>
</div>
</div>
{data.length === 0 ? (
<div className="col-12">
<div className="card border-0 text-center py-5" style={{ background: '#0f172a' }}>
<div className="card-body">
<i className="bi bi-check-circle text-success" style={{ fontSize: '4rem' }}></i>
<h5 className="text-white mt-3">{t('reports.noOverdue')}</h5>
<p className="text-slate-400">{t('reports.noOverdueDescription')}</p>
</div>
</div>
</div>
) : (
<div className="col-12">
<div className="card border-0" style={{ background: '#0f172a' }}>
<div className="card-header border-0 bg-transparent">
<h6 className="text-white mb-0">
<i className="bi bi-exclamation-triangle text-danger me-2"></i>
{t('reports.overdueList')}
</h6>
</div>
<div className="card-body p-0">
<div className="table-responsive">
<table className="table table-dark table-hover mb-0">
<thead>
<tr>
<th>{t('common.description')}</th>
<th>{t('reports.dueDate')}</th>
<th>{t('reports.daysOverdue')}</th>
<th className="text-end">{t('reports.amount')}</th>
</tr>
</thead>
<tbody>
{data.map(item => (
<tr key={item.id}>
<td>{item.description}</td>
<td className="text-slate-400">{item.due_date}</td>
<td>
<span className="badge bg-danger">{item.days_overdue} {t('common.days')}</span>
</td>
<td className="text-end text-danger fw-bold">
{currency(item.amount, item.currency)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
)}
</div>
);
};
const renderContent = () => {
if (loading) {
return (
@ -785,11 +1263,15 @@ const Reports = () => {
switch (activeTab) {
case 'summary': return renderSummary();
case 'category': return renderCategory();
case 'costCenter': return renderCostCenter();
case 'evolution': return renderEvolution();
case 'comparison': return renderComparison();
case 'topExpenses': return renderTopExpenses();
case 'projection': return renderProjection();
case 'dayOfWeek': return renderDayOfWeek();
case 'recurring': return renderRecurring();
case 'liabilities': return renderLiabilities();
case 'future': return renderFuture();
case 'overdue': return renderOverdue();
default: return null;
}
};

View File

@ -1392,12 +1392,24 @@ export const reportService = {
return response.data;
},
// Resumen ejecutivo
getExecutiveSummary: async (params = {}) => {
const response = await api.get('/reports/executive-summary', { params });
return response.data;
},
// Por categoría
getByCategory: async (params = {}) => {
const response = await api.get('/reports/by-category', { params });
return response.data;
},
// Por centro de costos
getByCostCenter: async (params = {}) => {
const response = await api.get('/reports/by-cost-center', { params });
return response.data;
},
// Evolución mensual
getMonthlyEvolution: async (params = {}) => {
const response = await api.get('/reports/monthly-evolution', { params });
@ -1439,6 +1451,24 @@ export const reportService = {
const response = await api.get('/reports/recurring');
return response.data;
},
// Reporte de pasivos/deudas
getLiabilities: async () => {
const response = await api.get('/reports/liabilities');
return response.data;
},
// Transacciones futuras
getFutureTransactions: async (params = {}) => {
const response = await api.get('/reports/future-transactions', { params });
return response.data;
},
// Transacciones vencidas
getOverdue: async () => {
const response = await api.get('/reports/overdue');
return response.data;
},
};
// ============================================