user())) { abort(403); } $request->validate([ 'season_id' => ['nullable', 'integer', 'exists:seasons,id'], 'year' => ['nullable', 'integer', 'min:2020', 'max:2099'], 'team_id' => ['nullable', 'integer', 'exists:teams,id'], 'type' => ['nullable', 'string', Rule::in(FinanceType::values())], 'category' => ['nullable', 'string', Rule::in(FinanceCategory::values())], ]); $query = Finance::with(['team', 'creator']); // Saison-Filter (hat Vorrang vor Jahr) $activeSeason = null; if ($request->filled('season_id')) { $activeSeason = Season::find($request->season_id); if ($activeSeason) { $query->whereBetween('date', [$activeSeason->start_date, $activeSeason->end_date]); } } elseif ($request->filled('year')) { $query->whereYear('date', $request->year); } else { // Default: aktuelle Saison falls vorhanden $activeSeason = Season::current(); if ($activeSeason) { $query->whereBetween('date', [$activeSeason->start_date, $activeSeason->end_date]); } } if ($request->filled('team_id')) { $query->where('team_id', $request->team_id); } if ($request->filled('type')) { $query->where('type', $request->type); } if ($request->filled('category')) { $query->where('category', $request->category); } // Summen berechnen (gleiche Filter wie Hauptquery) $statsQuery = clone $query; $totalIncome = (clone $statsQuery)->where('type', FinanceType::Income)->sum('amount'); $totalExpense = (clone $statsQuery)->where('type', FinanceType::Expense)->sum('amount'); $balance = $totalIncome - $totalExpense; // Monatliche Aggregation fuer Chart $monthlyRaw = (clone $statsQuery) ->select( DB::raw("strftime('%Y', date) as y"), DB::raw("strftime('%m', date) as m"), 'type', DB::raw('SUM(amount) as total') ) ->groupBy('y', 'm', 'type') ->orderBy('y') ->orderBy('m') ->get(); $chartMonthly = $this->buildMonthlyChart($monthlyRaw); // Kategorie-Aufschluesselung fuer Chart $categoryRaw = (clone $statsQuery) ->select('type', 'category', DB::raw('SUM(amount) as total')) ->groupBy('type', 'category') ->get(); $chartCategories = $this->buildCategoryChart($categoryRaw); // Paginierte Liste $finances = $query->orderByDesc('date')->orderByDesc('created_at')->paginate(25)->withQueryString(); $teams = Team::orderBy('name')->get(); $seasons = Season::orderByDesc('start_date')->get(); // Verfuegbare Jahre $years = Finance::selectRaw("strftime('%Y', date) as y") ->distinct() ->orderByDesc('y') ->pluck('y') ->toArray(); return view('admin.finances.index', compact( 'finances', 'totalIncome', 'totalExpense', 'balance', 'chartMonthly', 'chartCategories', 'teams', 'seasons', 'years', 'activeSeason' )); } public function create(): View { if (!Setting::isFeatureVisibleFor('finances', auth()->user())) { abort(403); } $teams = Team::orderBy('name')->get(); return view('admin.finances.create', compact('teams')); } public function store(Request $request): RedirectResponse { if (!Setting::isFeatureVisibleFor('finances', auth()->user())) { abort(403); } $validated = $request->validate([ 'team_id' => ['nullable', 'integer', 'exists:teams,id'], 'type' => ['required', 'string', Rule::in(FinanceType::values())], 'category' => ['required', 'string', Rule::in(FinanceCategory::values())], 'title' => ['required', 'string', 'max:150'], 'amount' => ['required', 'numeric', 'min:0.01', 'max:999999.99'], 'date' => ['required', 'date'], 'notes' => ['nullable', 'string', 'max:1000'], ]); $validated['amount'] = (int) round($validated['amount'] * 100); $finance = new Finance($validated); $finance->created_by = auth()->id(); $finance->save(); ActivityLog::logAction('finance_created', $finance, "{$validated['type']}: {$validated['title']}"); return redirect()->route('admin.finances.index') ->with('success', __('admin.finance_created')); } public function edit(Finance $finance): View { if (!Setting::isFeatureVisibleFor('finances', auth()->user())) { abort(403); } $teams = Team::orderBy('name')->get(); return view('admin.finances.edit', compact('finance', 'teams')); } public function update(Request $request, Finance $finance): RedirectResponse { if (!Setting::isFeatureVisibleFor('finances', auth()->user())) { abort(403); } $validated = $request->validate([ 'team_id' => ['nullable', 'integer', 'exists:teams,id'], 'type' => ['required', 'string', Rule::in(FinanceType::values())], 'category' => ['required', 'string', Rule::in(FinanceCategory::values())], 'title' => ['required', 'string', 'max:150'], 'amount' => ['required', 'numeric', 'min:0.01', 'max:999999.99'], 'date' => ['required', 'date'], 'notes' => ['nullable', 'string', 'max:1000'], ]); $validated['amount'] = (int) round($validated['amount'] * 100); $before = $finance->only(['type', 'category', 'title', 'amount', 'date']); $finance->update($validated); ActivityLog::logAction('finance_updated', $finance, "{$validated['type']}: {$validated['title']}", [ 'before' => $before, 'after' => $finance->only(['type', 'category', 'title', 'amount', 'date']), ]); return redirect()->route('admin.finances.index') ->with('success', __('admin.finance_updated')); } public function destroy(Finance $finance): RedirectResponse { if (!Setting::isFeatureVisibleFor('finances', auth()->user())) { abort(403); } ActivityLog::logAction('finance_deleted', $finance, "{$finance->type->value}: {$finance->title}"); $finance->delete(); return redirect()->route('admin.finances.index') ->with('success', __('admin.finance_deleted')); } private function buildMonthlyChart($monthlyRaw): array { $months = []; foreach ($monthlyRaw as $row) { $key = $row->y . '-' . $row->m; if (!isset($months[$key])) { $months[$key] = ['income' => 0, 'expense' => 0]; } $type = $row->type instanceof FinanceType ? $row->type->value : $row->type; $months[$key][$type] = (int) $row->total; } $labels = []; $income = []; $expense = []; foreach ($months as $key => $data) { [$y, $m] = explode('-', $key); $labels[] = str_pad($m, 2, '0', STR_PAD_LEFT) . '/' . $y; $income[] = round($data['income'] / 100, 2); $expense[] = round($data['expense'] / 100, 2); } return [ 'labels' => $labels, 'income' => $income, 'expense' => $expense, ]; } private function buildCategoryChart($categoryRaw): array { $incomeCategories = []; $expenseCategories = []; foreach ($categoryRaw as $row) { $cat = $row->category instanceof FinanceCategory ? $row->category : FinanceCategory::tryFrom($row->category); $label = $cat ? $cat->label() : $row->category; $amount = round((int) $row->total / 100, 2); $type = $row->type instanceof FinanceType ? $row->type->value : $row->type; if ($type === 'income') { $incomeCategories[$label] = $amount; } else { $expenseCategories[$label] = $amount; } } $colors = ['#305f3f', '#3e7750', '#579469', '#7fb38e', '#afd0b8', '#d6e7da', '#806130', '#99783a', '#c2a86e', '#dbc9a2']; $redColors = ['#76403b', '#8f504b', '#a86b67', '#c3918e', '#dbbcba', '#eddddc', '#57486c', '#6b5a84', '#84729e', '#a596ba']; return [ 'income' => [ 'labels' => array_keys($incomeCategories), 'data' => array_values($incomeCategories), 'colors' => array_slice($colors, 0, count($incomeCategories)), ], 'expense' => [ 'labels' => array_keys($expenseCategories), 'data' => array_values($expenseCategories), 'colors' => array_slice($redColors, 0, count($expenseCategories)), ], ]; } }