webmoney/backend/app/Http/Controllers/Api/BudgetController.php

286 lines
9.3 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 (con propagación automática a meses futuros)
*/
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();
// 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 y propagado',
'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('effective_date', $budget->year)
->whereMonth('effective_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('effective_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 (y de meses futuros)
*/
public function destroy($id)
{
$budget = Budget::forUser(Auth::id())->findOrFail($id);
$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 (incluyendo meses futuros)',
]);
}
/**
* 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 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 expense o both (gastos)
$categories = Category::where('user_id', Auth::id())
->whereNull('parent_id')
->whereIn('type', ['expense', 'both'])
->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'),
'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($monthlyData);
}
}