v1.32.1 - Fix Reports category totals bug, fix overdue endpoint, configure weekly server updates
This commit is contained in:
parent
39de07bf96
commit
99be24e309
27
CHANGELOG.md
27
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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
0
backend/deploy.sh
Normal file → Executable 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']);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 */
|
||||
|
||||
@ -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
@ -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"
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user