get('year', now()->year); $month = $request->get('month', now()->month); $budgets = Budget::forUser(Auth::id()) ->forPeriod($year, $month) ->active() ->with(['category', 'subcategory', 'costCenter']) ->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', 'subcategory_id' => 'nullable|exists:categories,id', 'cost_center_id' => 'nullable|exists:cost_centers,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,bimestral,trimestral,semestral,yearly', 'is_cumulative' => 'nullable|boolean', 'notes' => 'nullable|string', ]); // Validar que ao menos uma (categoria, subcategoria ou centro de custos) esteja preenchida if (empty($validated['category_id']) && empty($validated['subcategory_id']) && empty($validated['cost_center_id'])) { return response()->json([ 'message' => 'Debe especificar al menos una categoría, subcategoría o centro de custos', ], 422); } // Validar que subcategoria pertence à categoria (se ambas fornecidas) if (!empty($validated['subcategory_id']) && !empty($validated['category_id'])) { $subcat = Category::find($validated['subcategory_id']); if ($subcat && $subcat->parent_id != $validated['category_id']) { return response()->json([ 'message' => 'A subcategoria não pertence à categoria selecionada', ], 422); } } // Se tem subcategoria mas não tem categoria, buscar a categoria pai if (!empty($validated['subcategory_id']) && empty($validated['category_id'])) { $subcat = Category::find($validated['subcategory_id']); if ($subcat && $subcat->parent_id) { $validated['category_id'] = $subcat->parent_id; } } // Verificar que no exista ya $exists = Budget::forUser(Auth::id()) ->where('category_id', $validated['category_id']) ->where('subcategory_id', $validated['subcategory_id'] ?? null) ->where('cost_center_id', $validated['cost_center_id'] ?? null) ->where('year', $validated['year']) ->where('month', $validated['month']) ->exists(); if ($exists) { return response()->json([ 'message' => 'Ya existe un presupuesto para esta categoría/subcategoría en este período', ], 422); } $validated['user_id'] = Auth::id(); // Adicionar moeda primária do usuário se não fornecida if (empty($validated['currency'])) { $validated['currency'] = Auth::user()->primary_currency ?? 'EUR'; } // Crear el presupuesto del mes actual $budget = Budget::create($validated); // Determinar el salto de meses según period_type $periodType = $validated['period_type'] ?? 'monthly'; $periodStepMap = [ 'monthly' => 1, // A cada 1 mes (Jan, Feb, Mar, ..., Dec) = 12 presupuestos 'bimestral' => 2, // A cada 2 meses (Jan, Mar, May, Jul, Sep, Nov) = 6 presupuestos 'trimestral' => 3, // A cada 3 meses (Jan, Apr, Jul, Oct) = 4 presupuestos 'semestral' => 6, // A cada 6 meses (Jan, Jul) = 2 presupuestos 'yearly' => 12, // A cada 12 meses (solo Jan) = 1 presupuesto ]; $step = $periodStepMap[$periodType] ?? 1; $currentYear = $validated['year']; $currentMonth = $validated['month']; // Propagar hasta completar 12 meses (1 año) for ($monthsAhead = $step; $monthsAhead < 12; $monthsAhead += $step) { $nextMonth = $currentMonth + $monthsAhead; $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('subcategory_id', $validated['subcategory_id'] ?? null) ->where('cost_center_id', $validated['cost_center_id'] ?? null) ->where('year', $nextYear) ->where('month', $nextMonth) ->exists(); if (!$existsNext) { Budget::create([ 'user_id' => Auth::id(), 'category_id' => $validated['category_id'], 'subcategory_id' => $validated['subcategory_id'] ?? null, 'cost_center_id' => $validated['cost_center_id'] ?? null, 'name' => $validated['name'] ?? null, 'amount' => $validated['amount'], 'currency' => $validated['currency'] ?? null, 'year' => $nextYear, 'month' => $nextMonth, 'period_type' => $validated['period_type'] ?? 'monthly', 'is_cumulative' => $validated['is_cumulative'] ?? false, 'notes' => $validated['notes'] ?? null, ]); } } return response()->json([ 'message' => 'Presupuesto creado y propagado', 'data' => $budget->load(['category', 'subcategory', 'costCenter']), ], 201); } /** * Ver un presupuesto */ public function show($id) { $budget = Budget::forUser(Auth::id()) ->with(['category', 'subcategory', 'costCenter']) ->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); // Se tem subcategoria específica, usa apenas ela if ($budget->subcategory_id) { $query->where('category_id', $budget->subcategory_id); } // Se tem apenas categoria, inclui subcategorias elseif ($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; $subcategoryId = $budget->subcategory_id; $year = $budget->year; $month = $budget->month; $costCenterId = $budget->cost_center_id; // Eliminar este e todos os futuros da mesma categoria/subcategoria/centro de custos Budget::forUser(Auth::id()) ->where('category_id', $categoryId) ->where('subcategory_id', $subcategoryId) ->where('cost_center_id', $costCenterId) ->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 pares (category_id, subcategory_id) já usados no período $usedPairs = Budget::forUser(Auth::id()) ->forPeriod($year, $month) ->get() ->map(fn($b) => $b->category_id . '_' . ($b->subcategory_id ?? 'null')) ->toArray(); // Categorías padre con tipo expense o both (gastos) $categories = Category::where('user_id', Auth::id()) ->whereNull('parent_id') ->whereIn('type', ['expense', 'both']) ->with(['subcategories' => function($q) { $q->orderBy('name'); }]) ->orderBy('name') ->get(); // Filtrar categorias/subcategorias já usadas $available = $categories->map(function($category) use ($usedPairs) { $categoryKey = $category->id . '_null'; // Filtrar subcategorias não usadas $availableSubcategories = $category->subcategories->filter(function($sub) use ($usedPairs, $category) { $subKey = $category->id . '_' . $sub->id; return !in_array($subKey, $usedPairs); })->values(); return [ 'id' => $category->id, 'name' => $category->name, 'type' => $category->type, 'color' => $category->color, 'icon' => $category->icon, 'is_available' => !in_array($categoryKey, $usedPairs), 'subcategories' => $availableSubcategories, ]; })->filter(function($cat) { // Manter categoria se ela mesma está disponível OU se tem subcategorias disponíveis return $cat['is_available'] || $cat['subcategories']->count() > 0; })->values(); return response()->json($available); } /** * 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); } }