NEW FEATURES: - Financial Health: Score 0-100, 6 metrics, insights, recommendations - Goals: Create/edit savings goals, contributions, progress tracking - Budgets: Monthly category limits, usage alerts, year summary - Reports: 7 tabs with charts (category, evolution, projection, etc.) BACKEND: - New models: FinancialGoal, GoalContribution, Budget - New controllers: FinancialHealthController, FinancialGoalController, BudgetController, ReportController - New migrations: financial_goals, goal_contributions, budgets FRONTEND: - New pages: FinancialHealth.jsx, Goals.jsx, Budgets.jsx, Reports.jsx - New services: financialHealthService, financialGoalService, budgetService, reportService - Navigation: New 'Planning' group in sidebar Chart.js integration for all visualizations
242 lines
7.5 KiB
PHP
242 lines
7.5 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Budget;
|
|
use App\Models\Category;
|
|
use App\Models\Transaction;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Carbon\Carbon;
|
|
|
|
class BudgetController extends Controller
|
|
{
|
|
/**
|
|
* Listar presupuestos de un período
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
$year = $request->get('year', now()->year);
|
|
$month = $request->get('month', now()->month);
|
|
|
|
$budgets = Budget::forUser(Auth::id())
|
|
->forPeriod($year, $month)
|
|
->active()
|
|
->with('category')
|
|
->orderBy('amount', 'desc')
|
|
->get();
|
|
|
|
// Calcular totales
|
|
$totalBudget = $budgets->sum('amount');
|
|
$totalSpent = $budgets->sum('spent_amount');
|
|
$totalRemaining = $totalBudget - $totalSpent;
|
|
|
|
return response()->json([
|
|
'data' => $budgets,
|
|
'summary' => [
|
|
'total_budget' => $totalBudget,
|
|
'total_spent' => $totalSpent,
|
|
'total_remaining' => $totalRemaining,
|
|
'usage_percentage' => $totalBudget > 0 ? round(($totalSpent / $totalBudget) * 100, 1) : 0,
|
|
'year' => $year,
|
|
'month' => $month,
|
|
'period_label' => Carbon::createFromDate($year, $month, 1)->format('F Y'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Crear un presupuesto
|
|
*/
|
|
public function store(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'category_id' => 'nullable|exists:categories,id',
|
|
'name' => 'nullable|string|max:255',
|
|
'amount' => 'required|numeric|min:0.01',
|
|
'currency' => 'nullable|string|size:3',
|
|
'year' => 'required|integer|min:2020|max:2100',
|
|
'month' => 'required|integer|min:1|max:12',
|
|
'period_type' => 'nullable|in:monthly,yearly',
|
|
'notes' => 'nullable|string',
|
|
]);
|
|
|
|
// Verificar que no exista ya
|
|
$exists = Budget::forUser(Auth::id())
|
|
->where('category_id', $validated['category_id'])
|
|
->where('year', $validated['year'])
|
|
->where('month', $validated['month'])
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
return response()->json([
|
|
'message' => 'Ya existe un presupuesto para esta categoría en este período',
|
|
], 422);
|
|
}
|
|
|
|
$validated['user_id'] = Auth::id();
|
|
|
|
$budget = Budget::create($validated);
|
|
|
|
return response()->json([
|
|
'message' => 'Presupuesto creado',
|
|
'data' => $budget->load('category'),
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* Ver un presupuesto
|
|
*/
|
|
public function show($id)
|
|
{
|
|
$budget = Budget::forUser(Auth::id())
|
|
->with('category')
|
|
->findOrFail($id);
|
|
|
|
// Obtener transacciones del período
|
|
$query = Transaction::where('user_id', Auth::id())
|
|
->where('transaction_type', 'debit')
|
|
->whereYear('transaction_date', $budget->year)
|
|
->whereMonth('transaction_date', $budget->month);
|
|
|
|
if ($budget->category_id) {
|
|
$categoryIds = [$budget->category_id];
|
|
$subcategories = Category::where('parent_id', $budget->category_id)->pluck('id')->toArray();
|
|
$categoryIds = array_merge($categoryIds, $subcategories);
|
|
$query->whereIn('category_id', $categoryIds);
|
|
}
|
|
|
|
$transactions = $query->with('category')
|
|
->orderBy('transaction_date', 'desc')
|
|
->limit(50)
|
|
->get();
|
|
|
|
return response()->json([
|
|
'budget' => $budget,
|
|
'transactions' => $transactions,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Actualizar un presupuesto
|
|
*/
|
|
public function update(Request $request, $id)
|
|
{
|
|
$budget = Budget::forUser(Auth::id())->findOrFail($id);
|
|
|
|
$validated = $request->validate([
|
|
'amount' => 'sometimes|numeric|min:0.01',
|
|
'notes' => 'nullable|string',
|
|
'is_active' => 'sometimes|boolean',
|
|
]);
|
|
|
|
$budget->update($validated);
|
|
|
|
return response()->json([
|
|
'message' => 'Presupuesto actualizado',
|
|
'data' => $budget->fresh('category'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Eliminar un presupuesto
|
|
*/
|
|
public function destroy($id)
|
|
{
|
|
$budget = Budget::forUser(Auth::id())->findOrFail($id);
|
|
$budget->delete();
|
|
|
|
return response()->json([
|
|
'message' => 'Presupuesto eliminado',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Copiar presupuestos al siguiente mes
|
|
*/
|
|
public function copyToNextMonth(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'year' => 'required|integer',
|
|
'month' => 'required|integer|min:1|max:12',
|
|
]);
|
|
|
|
Budget::copyToNextMonth(Auth::id(), $validated['year'], $validated['month']);
|
|
|
|
$nextMonth = $validated['month'] === 12 ? 1 : $validated['month'] + 1;
|
|
$nextYear = $validated['month'] === 12 ? $validated['year'] + 1 : $validated['year'];
|
|
|
|
return response()->json([
|
|
'message' => 'Presupuestos copiados',
|
|
'next_period' => [
|
|
'year' => $nextYear,
|
|
'month' => $nextMonth,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Obtener categorías disponibles para presupuesto
|
|
*/
|
|
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
|
|
$usedCategoryIds = Budget::forUser(Auth::id())
|
|
->forPeriod($year, $month)
|
|
->pluck('category_id')
|
|
->toArray();
|
|
|
|
// Categorías padre con tipo debit
|
|
$categories = Category::where('user_id', Auth::id())
|
|
->whereNull('parent_id')
|
|
->where('type', 'debit')
|
|
->whereNotIn('id', $usedCategoryIds)
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
return response()->json($categories);
|
|
}
|
|
|
|
/**
|
|
* Resumen anual de presupuestos
|
|
*/
|
|
public function yearSummary(Request $request)
|
|
{
|
|
$year = $request->get('year', now()->year);
|
|
|
|
$monthlyData = [];
|
|
|
|
for ($month = 1; $month <= 12; $month++) {
|
|
$budgets = Budget::forUser(Auth::id())
|
|
->forPeriod($year, $month)
|
|
->active()
|
|
->get();
|
|
|
|
$totalBudget = $budgets->sum('amount');
|
|
$totalSpent = $budgets->sum('spent_amount');
|
|
|
|
$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,
|
|
];
|
|
}
|
|
|
|
return response()->json([
|
|
'year' => $year,
|
|
'monthly' => $monthlyData,
|
|
'totals' => [
|
|
'budget' => array_sum(array_column($monthlyData, 'budget')),
|
|
'spent' => array_sum(array_column($monthlyData, 'spent')),
|
|
],
|
|
]);
|
|
}
|
|
}
|