diff --git a/app/Enums/FinanceCategory.php b/app/Enums/FinanceCategory.php new file mode 100644 index 0000000..46e06ac --- /dev/null +++ b/app/Enums/FinanceCategory.php @@ -0,0 +1,27 @@ +value}"); + } + + public static function values(): array + { + return array_column(self::cases(), 'value'); + } +} diff --git a/app/Enums/FinanceType.php b/app/Enums/FinanceType.php new file mode 100644 index 0000000..8975c19 --- /dev/null +++ b/app/Enums/FinanceType.php @@ -0,0 +1,19 @@ +value}"); + } + + public static function values(): array + { + return array_column(self::cases(), 'value'); + } +} diff --git a/app/Http/Controllers/Admin/FinanceController.php b/app/Http/Controllers/Admin/FinanceController.php new file mode 100644 index 0000000..b044437 --- /dev/null +++ b/app/Http/Controllers/Admin/FinanceController.php @@ -0,0 +1,272 @@ +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)), + ], + ]; + } +} diff --git a/app/Http/Controllers/Admin/SeasonController.php b/app/Http/Controllers/Admin/SeasonController.php new file mode 100644 index 0000000..ba7e192 --- /dev/null +++ b/app/Http/Controllers/Admin/SeasonController.php @@ -0,0 +1,69 @@ +validate([ + 'name' => ['required', 'string', 'max:50'], + 'start_date' => ['required', 'date'], + 'end_date' => ['required', 'date', 'after:start_date'], + 'is_current' => ['nullable', 'boolean'], + ]); + + $validated['is_current'] = !empty($validated['is_current']); + + if ($validated['is_current']) { + Season::where('is_current', true)->update(['is_current' => false]); + } + + Season::create($validated); + + return redirect()->route('admin.settings.edit', ['tab' => 'seasons']) + ->with('success', __('admin.season_created')); + } + + public function update(Request $request, Season $season): RedirectResponse + { + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:50'], + 'start_date' => ['required', 'date'], + 'end_date' => ['required', 'date', 'after:start_date'], + 'is_current' => ['nullable', 'boolean'], + ]); + + $validated['is_current'] = !empty($validated['is_current']); + + if ($validated['is_current']) { + Season::where('is_current', true)->where('id', '!=', $season->id)->update(['is_current' => false]); + } + + $season->update($validated); + + return redirect()->route('admin.settings.edit', ['tab' => 'seasons']) + ->with('success', __('admin.season_updated')); + } + + public function destroy(Season $season): RedirectResponse + { + $hasData = Finance::whereBetween('date', [$season->start_date, $season->end_date])->exists(); + + if ($hasData) { + return redirect()->route('admin.settings.edit', ['tab' => 'seasons']) + ->with('error', __('admin.season_has_data')); + } + + $season->delete(); + + return redirect()->route('admin.settings.edit', ['tab' => 'seasons']) + ->with('success', __('admin.season_deleted')); + } +} diff --git a/app/Models/Finance.php b/app/Models/Finance.php new file mode 100644 index 0000000..ef9ffb5 --- /dev/null +++ b/app/Models/Finance.php @@ -0,0 +1,38 @@ + FinanceType::class, + 'category' => FinanceCategory::class, + 'date' => 'date', + 'amount' => 'integer', + ]; + } + + public function team(): BelongsTo + { + return $this->belongsTo(Team::class); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function getFormattedAmountAttribute(): string + { + return number_format($this->amount / 100, 2, ',', '.') . ' €'; + } +} diff --git a/app/Models/Season.php b/app/Models/Season.php new file mode 100644 index 0000000..fe413a5 --- /dev/null +++ b/app/Models/Season.php @@ -0,0 +1,34 @@ + 'date', + 'end_date' => 'date', + 'is_current' => 'boolean', + ]; + } + + public function scopeCurrent($query) + { + return $query->where('is_current', true); + } + + public static function current(): ?self + { + return static::where('is_current', true)->first(); + } + + public static function options(): array + { + return static::orderByDesc('start_date')->pluck('name', 'id')->toArray(); + } +} diff --git a/database/migrations/0039_01_01_000000_create_seasons_table.php b/database/migrations/0039_01_01_000000_create_seasons_table.php new file mode 100644 index 0000000..dfe8713 --- /dev/null +++ b/database/migrations/0039_01_01_000000_create_seasons_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('name', 50); + $table->date('start_date'); + $table->date('end_date'); + $table->boolean('is_current')->default(false); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('seasons'); + } +}; diff --git a/database/migrations/0039_01_01_000001_create_finances_table.php b/database/migrations/0039_01_01_000001_create_finances_table.php new file mode 100644 index 0000000..9a3dd66 --- /dev/null +++ b/database/migrations/0039_01_01_000001_create_finances_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('team_id')->nullable()->constrained()->nullOnDelete(); + $table->string('type', 10); + $table->string('category', 30); + $table->string('title', 150); + $table->integer('amount'); + $table->date('date'); + $table->text('notes')->nullable(); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + + $table->index(['team_id', 'date']); + $table->index(['type', 'date']); + }); + } + + public function down(): void + { + Schema::dropIfExists('finances'); + } +}; diff --git a/lang/ar/admin.php b/lang/ar/admin.php index 14afbfe..47eb871 100755 --- a/lang/ar/admin.php +++ b/lang/ar/admin.php @@ -566,6 +566,9 @@ return [ 'stats_total_shots' => 'إجمالي التسديدات', 'stats_gk_appearances' => 'مباريات كحارس', 'stats_total_saves' => 'إجمالي التصديات', + 'stats_penalties' => 'رميات جزاء', + 'stats_cards' => 'عقوبات', + 'stats_avg_time' => '⌀ وقت اللعب', 'stats_close' => 'إغلاق', 'player_goals' => 'أهداف', @@ -576,4 +579,52 @@ return [ 'performance_good' => 'جيد', 'performance_average' => 'متوسط', 'performance_below' => 'أقل من المتوسط', + + // المالية + 'nav_finances' => 'المالية', + 'finances_title' => 'المالية', + 'new_finance' => 'إدخال جديد', + 'finance_edit' => 'تعديل الإدخال', + 'finance_created' => 'تم إنشاء الإدخال بنجاح.', + 'finance_updated' => 'تم تحديث الإدخال بنجاح.', + 'finance_deleted' => 'تم حذف الإدخال بنجاح.', + 'finance_income' => 'إيراد', + 'finance_expense' => 'مصروف', + 'finance_type' => 'النوع', + 'finance_category' => 'الفئة', + 'finance_title' => 'العنوان', + 'finance_amount' => 'المبلغ', + 'finance_date' => 'التاريخ', + 'finance_notes' => 'ملاحظات', + 'finance_no_team' => 'بدون فريق (عام)', + 'finance_total_income' => 'إجمالي الإيرادات', + 'finance_total_expense' => 'إجمالي المصروفات', + 'finance_balance' => 'الرصيد', + 'finance_no_entries' => 'لا توجد إدخالات بعد.', + 'finance_chart_monthly' => 'نظرة شهرية', + 'finance_chart_categories' => 'التوزيع حسب الفئة', + 'finance_confirm_delete' => 'هل تريد حذف هذا الإدخال حقاً؟', + 'finance_all_types' => 'جميع الأنواع', + 'finance_all_categories' => 'جميع الفئات', + + // المواسم + 'season' => 'الموسم', + 'seasons_title' => 'المواسم', + 'season_name' => 'الاسم', + 'season_start' => 'تاريخ البداية', + 'season_end' => 'تاريخ النهاية', + 'season_current' => 'الموسم الحالي', + 'season_created' => 'تم إنشاء الموسم.', + 'season_updated' => 'تم تحديث الموسم.', + 'season_deleted' => 'تم حذف الموسم.', + 'season_has_data' => 'لا يمكن حذف الموسم (توجد بيانات مالية).', + 'season_confirm_delete' => 'هل تريد حذف هذا الموسم حقاً؟', + 'all_seasons' => 'جميع المواسم', + 'no_seasons_yet' => 'لا توجد مواسم بعد.', + 'new_season' => 'إنشاء موسم جديد', + 'settings_tab_seasons' => 'المواسم', + 'filter_year' => 'السنة', + + // الرؤية + 'visibility_feature_finances' => 'المالية', ]; diff --git a/lang/de/admin.php b/lang/de/admin.php index 90b2b8d..57801df 100755 --- a/lang/de/admin.php +++ b/lang/de/admin.php @@ -602,6 +602,9 @@ return [ 'stats_total_shots' => 'Gesamtwürfe', 'stats_gk_appearances' => 'TW-Einsätze', 'stats_total_saves' => 'Gesamtparaden', + 'stats_penalties' => '7-Meter', + 'stats_cards' => 'Strafen', + 'stats_avg_time' => '⌀ Spielzeit', 'stats_close' => 'Schließen', 'player_goals' => 'Tore', @@ -612,4 +615,52 @@ return [ 'performance_good' => 'Gut', 'performance_average' => 'Mittel', 'performance_below' => 'Unterdurchschnittlich', + + // Finanzen + 'nav_finances' => 'Finanzen', + 'finances_title' => 'Finanzen', + 'new_finance' => 'Neuer Eintrag', + 'finance_edit' => 'Eintrag bearbeiten', + 'finance_created' => 'Eintrag erfolgreich erstellt.', + 'finance_updated' => 'Eintrag erfolgreich aktualisiert.', + 'finance_deleted' => 'Eintrag erfolgreich gelöscht.', + 'finance_income' => 'Einnahme', + 'finance_expense' => 'Ausgabe', + 'finance_type' => 'Typ', + 'finance_category' => 'Kategorie', + 'finance_title' => 'Bezeichnung', + 'finance_amount' => 'Betrag', + 'finance_date' => 'Datum', + 'finance_notes' => 'Notizen', + 'finance_no_team' => 'Kein Team (vereinsübergreifend)', + 'finance_total_income' => 'Einnahmen gesamt', + 'finance_total_expense' => 'Ausgaben gesamt', + 'finance_balance' => 'Bilanz', + 'finance_no_entries' => 'Noch keine Einträge vorhanden.', + 'finance_chart_monthly' => 'Monatliche Übersicht', + 'finance_chart_categories' => 'Aufschlüsselung nach Kategorie', + 'finance_confirm_delete' => 'Diesen Eintrag wirklich löschen?', + 'finance_all_types' => 'Alle Typen', + 'finance_all_categories' => 'Alle Kategorien', + + // Saisons + 'season' => 'Saison', + 'seasons_title' => 'Saisons', + 'season_name' => 'Name', + 'season_start' => 'Startdatum', + 'season_end' => 'Enddatum', + 'season_current' => 'Aktuelle Saison', + 'season_created' => 'Saison erstellt.', + 'season_updated' => 'Saison aktualisiert.', + 'season_deleted' => 'Saison gelöscht.', + 'season_has_data' => 'Saison kann nicht gelöscht werden (Finanzdaten vorhanden).', + 'season_confirm_delete' => 'Diese Saison wirklich löschen?', + 'all_seasons' => 'Alle Saisons', + 'no_seasons_yet' => 'Noch keine Saisons angelegt.', + 'new_season' => 'Neue Saison anlegen', + 'settings_tab_seasons' => 'Saisons', + 'filter_year' => 'Jahr', + + // Sichtbarkeit + 'visibility_feature_finances' => 'Finanzen', ]; diff --git a/lang/en/admin.php b/lang/en/admin.php index be44493..cf7afac 100755 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -565,6 +565,9 @@ return [ 'stats_total_shots' => 'Total shots', 'stats_gk_appearances' => 'GK appearances', 'stats_total_saves' => 'Total saves', + 'stats_penalties' => 'Penalties', + 'stats_cards' => 'Cards', + 'stats_avg_time' => 'Avg. time', 'stats_close' => 'Close', 'player_goals' => 'Goals', @@ -575,4 +578,52 @@ return [ 'performance_good' => 'Good', 'performance_average' => 'Average', 'performance_below' => 'Below average', + + // Finances + 'nav_finances' => 'Finances', + 'finances_title' => 'Finances', + 'new_finance' => 'New Entry', + 'finance_edit' => 'Edit Entry', + 'finance_created' => 'Entry created successfully.', + 'finance_updated' => 'Entry updated successfully.', + 'finance_deleted' => 'Entry deleted successfully.', + 'finance_income' => 'Income', + 'finance_expense' => 'Expense', + 'finance_type' => 'Type', + 'finance_category' => 'Category', + 'finance_title' => 'Title', + 'finance_amount' => 'Amount', + 'finance_date' => 'Date', + 'finance_notes' => 'Notes', + 'finance_no_team' => 'No team (club-wide)', + 'finance_total_income' => 'Total Income', + 'finance_total_expense' => 'Total Expenses', + 'finance_balance' => 'Balance', + 'finance_no_entries' => 'No entries yet.', + 'finance_chart_monthly' => 'Monthly Overview', + 'finance_chart_categories' => 'Breakdown by Category', + 'finance_confirm_delete' => 'Really delete this entry?', + 'finance_all_types' => 'All Types', + 'finance_all_categories' => 'All Categories', + + // Seasons + 'season' => 'Season', + 'seasons_title' => 'Seasons', + 'season_name' => 'Name', + 'season_start' => 'Start Date', + 'season_end' => 'End Date', + 'season_current' => 'Current Season', + 'season_created' => 'Season created.', + 'season_updated' => 'Season updated.', + 'season_deleted' => 'Season deleted.', + 'season_has_data' => 'Season cannot be deleted (financial data exists).', + 'season_confirm_delete' => 'Really delete this season?', + 'all_seasons' => 'All Seasons', + 'no_seasons_yet' => 'No seasons created yet.', + 'new_season' => 'Create New Season', + 'settings_tab_seasons' => 'Seasons', + 'filter_year' => 'Year', + + // Visibility + 'visibility_feature_finances' => 'Finances', ]; diff --git a/lang/pl/admin.php b/lang/pl/admin.php index 5a329e4..d046b3e 100755 --- a/lang/pl/admin.php +++ b/lang/pl/admin.php @@ -566,6 +566,9 @@ return [ 'stats_total_shots' => 'Łączne rzuty', 'stats_gk_appearances' => 'Występy jako bramkarz', 'stats_total_saves' => 'Łączne obrony', + 'stats_penalties' => 'Rzuty karne', + 'stats_cards' => 'Kary', + 'stats_avg_time' => '⌀ Czas gry', 'stats_close' => 'Zamknij', 'player_goals' => 'Bramki', @@ -576,4 +579,52 @@ return [ 'performance_good' => 'Dobrze', 'performance_average' => 'Średnio', 'performance_below' => 'Poniżej średniej', + + // Finanse + 'nav_finances' => 'Finanse', + 'finances_title' => 'Finanse', + 'new_finance' => 'Nowy wpis', + 'finance_edit' => 'Edytuj wpis', + 'finance_created' => 'Wpis został utworzony.', + 'finance_updated' => 'Wpis został zaktualizowany.', + 'finance_deleted' => 'Wpis został usunięty.', + 'finance_income' => 'Przychód', + 'finance_expense' => 'Wydatek', + 'finance_type' => 'Typ', + 'finance_category' => 'Kategoria', + 'finance_title' => 'Nazwa', + 'finance_amount' => 'Kwota', + 'finance_date' => 'Data', + 'finance_notes' => 'Notatki', + 'finance_no_team' => 'Bez zespołu (ogólne)', + 'finance_total_income' => 'Suma przychodów', + 'finance_total_expense' => 'Suma wydatków', + 'finance_balance' => 'Bilans', + 'finance_no_entries' => 'Brak wpisów.', + 'finance_chart_monthly' => 'Przegląd miesięczny', + 'finance_chart_categories' => 'Podział na kategorie', + 'finance_confirm_delete' => 'Czy na pewno usunąć ten wpis?', + 'finance_all_types' => 'Wszystkie typy', + 'finance_all_categories' => 'Wszystkie kategorie', + + // Sezony + 'season' => 'Sezon', + 'seasons_title' => 'Sezony', + 'season_name' => 'Nazwa', + 'season_start' => 'Data rozpoczęcia', + 'season_end' => 'Data zakończenia', + 'season_current' => 'Bieżący sezon', + 'season_created' => 'Sezon utworzony.', + 'season_updated' => 'Sezon zaktualizowany.', + 'season_deleted' => 'Sezon usunięty.', + 'season_has_data' => 'Nie można usunąć sezonu (istnieją dane finansowe).', + 'season_confirm_delete' => 'Czy na pewno usunąć ten sezon?', + 'all_seasons' => 'Wszystkie sezony', + 'no_seasons_yet' => 'Brak sezonów.', + 'new_season' => 'Utwórz nowy sezon', + 'settings_tab_seasons' => 'Sezony', + 'filter_year' => 'Rok', + + // Widoczność + 'visibility_feature_finances' => 'Finanse', ]; diff --git a/lang/ru/admin.php b/lang/ru/admin.php index 4d7f8fd..35a151a 100755 --- a/lang/ru/admin.php +++ b/lang/ru/admin.php @@ -584,6 +584,9 @@ return [ 'stats_total_shots' => 'Всего бросков', 'stats_gk_appearances' => 'Игры вратарём', 'stats_total_saves' => 'Всего отражений', + 'stats_penalties' => 'Пенальти', + 'stats_cards' => 'Наказания', + 'stats_avg_time' => '⌀ Время', 'stats_close' => 'Закрыть', 'player_goals' => 'Голы', @@ -594,4 +597,52 @@ return [ 'performance_good' => 'Хорошо', 'performance_average' => 'Средне', 'performance_below' => 'Ниже среднего', + + // Финансы + 'nav_finances' => 'Финансы', + 'finances_title' => 'Финансы', + 'new_finance' => 'Новая запись', + 'finance_edit' => 'Редактировать запись', + 'finance_created' => 'Запись успешно создана.', + 'finance_updated' => 'Запись успешно обновлена.', + 'finance_deleted' => 'Запись успешно удалена.', + 'finance_income' => 'Доход', + 'finance_expense' => 'Расход', + 'finance_type' => 'Тип', + 'finance_category' => 'Категория', + 'finance_title' => 'Название', + 'finance_amount' => 'Сумма', + 'finance_date' => 'Дата', + 'finance_notes' => 'Заметки', + 'finance_no_team' => 'Без команды (общее)', + 'finance_total_income' => 'Всего доходов', + 'finance_total_expense' => 'Всего расходов', + 'finance_balance' => 'Баланс', + 'finance_no_entries' => 'Записей пока нет.', + 'finance_chart_monthly' => 'Месячный обзор', + 'finance_chart_categories' => 'По категориям', + 'finance_confirm_delete' => 'Действительно удалить эту запись?', + 'finance_all_types' => 'Все типы', + 'finance_all_categories' => 'Все категории', + + // Сезоны + 'season' => 'Сезон', + 'seasons_title' => 'Сезоны', + 'season_name' => 'Название', + 'season_start' => 'Дата начала', + 'season_end' => 'Дата окончания', + 'season_current' => 'Текущий сезон', + 'season_created' => 'Сезон создан.', + 'season_updated' => 'Сезон обновлён.', + 'season_deleted' => 'Сезон удалён.', + 'season_has_data' => 'Сезон нельзя удалить (есть финансовые данные).', + 'season_confirm_delete' => 'Действительно удалить этот сезон?', + 'all_seasons' => 'Все сезоны', + 'no_seasons_yet' => 'Сезоны ещё не созданы.', + 'new_season' => 'Создать новый сезон', + 'settings_tab_seasons' => 'Сезоны', + 'filter_year' => 'Год', + + // Видимость + 'visibility_feature_finances' => 'Финансы', ]; diff --git a/lang/tr/admin.php b/lang/tr/admin.php index 0df3fbd..4fb704a 100755 --- a/lang/tr/admin.php +++ b/lang/tr/admin.php @@ -584,6 +584,9 @@ return [ 'stats_total_shots' => 'Toplam atışlar', 'stats_gk_appearances' => 'Kaleci maçları', 'stats_total_saves' => 'Toplam kurtarışlar', + 'stats_penalties' => 'Penaltılar', + 'stats_cards' => 'Cezalar', + 'stats_avg_time' => '⌀ Süre', 'stats_close' => 'Kapat', 'player_goals' => 'Goller', @@ -594,4 +597,52 @@ return [ 'performance_good' => 'İyi', 'performance_average' => 'Orta', 'performance_below' => 'Ortalamanın altı', + + // Finans + 'nav_finances' => 'Finans', + 'finances_title' => 'Finans', + 'new_finance' => 'Yeni Kayıt', + 'finance_edit' => 'Kaydı Düzenle', + 'finance_created' => 'Kayıt başarıyla oluşturuldu.', + 'finance_updated' => 'Kayıt başarıyla güncellendi.', + 'finance_deleted' => 'Kayıt başarıyla silindi.', + 'finance_income' => 'Gelir', + 'finance_expense' => 'Gider', + 'finance_type' => 'Tür', + 'finance_category' => 'Kategori', + 'finance_title' => 'Başlık', + 'finance_amount' => 'Tutar', + 'finance_date' => 'Tarih', + 'finance_notes' => 'Notlar', + 'finance_no_team' => 'Takım yok (genel)', + 'finance_total_income' => 'Toplam Gelir', + 'finance_total_expense' => 'Toplam Gider', + 'finance_balance' => 'Bilanço', + 'finance_no_entries' => 'Henüz kayıt yok.', + 'finance_chart_monthly' => 'Aylık Genel Bakış', + 'finance_chart_categories' => 'Kategoriye Göre Dağılım', + 'finance_confirm_delete' => 'Bu kaydı gerçekten silmek istiyor musunuz?', + 'finance_all_types' => 'Tüm Türler', + 'finance_all_categories' => 'Tüm Kategoriler', + + // Sezonlar + 'season' => 'Sezon', + 'seasons_title' => 'Sezonlar', + 'season_name' => 'Ad', + 'season_start' => 'Başlangıç Tarihi', + 'season_end' => 'Bitiş Tarihi', + 'season_current' => 'Mevcut Sezon', + 'season_created' => 'Sezon oluşturuldu.', + 'season_updated' => 'Sezon güncellendi.', + 'season_deleted' => 'Sezon silindi.', + 'season_has_data' => 'Sezon silinemez (finansal veriler mevcut).', + 'season_confirm_delete' => 'Bu sezonu gerçekten silmek istiyor musunuz?', + 'all_seasons' => 'Tüm Sezonlar', + 'no_seasons_yet' => 'Henüz sezon oluşturulmamış.', + 'new_season' => 'Yeni Sezon Oluştur', + 'settings_tab_seasons' => 'Sezonlar', + 'filter_year' => 'Yıl', + + // Görünürlük + 'visibility_feature_finances' => 'Finans', ]; diff --git a/resources/views/admin/finances/create.blade.php b/resources/views/admin/finances/create.blade.php new file mode 100644 index 0000000..7b2e3ea --- /dev/null +++ b/resources/views/admin/finances/create.blade.php @@ -0,0 +1,94 @@ + +

{{ __('admin.new_finance') }}

+ +
+
+ @csrf + + {{-- Typ --}} +
+ +
+ + +
+ @error('type')

{{ $message }}

@enderror +
+ + {{-- Kategorie --}} +
+ + + @error('category')

{{ $message }}

@enderror +
+ + {{-- Titel --}} +
+ + + @error('title')

{{ $message }}

@enderror +
+ + {{-- Betrag + Datum --}} +
+
+ +
+ + +
+ @error('amount')

{{ $message }}

@enderror +
+
+ + + @error('date')

{{ $message }}

@enderror +
+
+ + {{-- Team --}} +
+ + + @error('team_id')

{{ $message }}

@enderror +
+ + {{-- Notizen --}} +
+ + + @error('notes')

{{ $message }}

@enderror +
+ + {{-- Buttons --}} +
+ + + {{ __('ui.cancel') }} + +
+
+
+
diff --git a/resources/views/admin/finances/edit.blade.php b/resources/views/admin/finances/edit.blade.php new file mode 100644 index 0000000..7bd2ec4 --- /dev/null +++ b/resources/views/admin/finances/edit.blade.php @@ -0,0 +1,95 @@ + +

{{ __('admin.finance_edit') }}

+ +
+
+ @csrf + @method('PUT') + + {{-- Typ --}} +
+ +
+ + +
+ @error('type')

{{ $message }}

@enderror +
+ + {{-- Kategorie --}} +
+ + + @error('category')

{{ $message }}

@enderror +
+ + {{-- Titel --}} +
+ + + @error('title')

{{ $message }}

@enderror +
+ + {{-- Betrag + Datum --}} +
+
+ +
+ + +
+ @error('amount')

{{ $message }}

@enderror +
+
+ + + @error('date')

{{ $message }}

@enderror +
+
+ + {{-- Team --}} +
+ + + @error('team_id')

{{ $message }}

@enderror +
+ + {{-- Notizen --}} +
+ + + @error('notes')

{{ $message }}

@enderror +
+ + {{-- Buttons --}} +
+ + + {{ __('ui.cancel') }} + +
+
+
+
diff --git a/resources/views/admin/finances/index.blade.php b/resources/views/admin/finances/index.blade.php new file mode 100644 index 0000000..435e2aa --- /dev/null +++ b/resources/views/admin/finances/index.blade.php @@ -0,0 +1,243 @@ + +
+

{{ __('admin.finances_title') }}

+ + + {{ __('admin.new_finance') }} + +
+ + {{-- Filter --}} +
+
+ {{-- Saison --}} +
+ + +
+ + {{-- Jahr --}} +
+ + +
+ + {{-- Team --}} +
+ + +
+ + {{-- Typ --}} +
+ + +
+ + {{-- Kategorie --}} +
+ + +
+
+
+ + {{-- Bilanz-Cards --}} +
+
+
{{ __('admin.finance_total_income') }}
+
{{ number_format($totalIncome / 100, 2, ',', '.') }} €
+
+
+
{{ __('admin.finance_total_expense') }}
+
{{ number_format($totalExpense / 100, 2, ',', '.') }} €
+
+
+
{{ __('admin.finance_balance') }}
+
+ {{ $balance >= 0 ? '+' : '' }}{{ number_format($balance / 100, 2, ',', '.') }} € +
+
+
+ + {{-- Charts --}} + @if ($totalIncome > 0 || $totalExpense > 0) +
+ {{-- Monatliche Uebersicht --}} + @if (!empty($chartMonthly['labels'])) +
+

{{ __('admin.finance_chart_monthly') }}

+
+ +
+
+ @endif + + {{-- Kategorie-Aufschluesselung --}} +
+

{{ __('admin.finance_chart_categories') }}

+
+ @if (!empty($chartCategories['income']['data'])) +
+

{{ __('admin.finance_income') }}

+
+ +
+
+ @endif + @if (!empty($chartCategories['expense']['data'])) +
+

{{ __('admin.finance_expense') }}

+
+ +
+
+ @endif +
+
+
+ @endif + + {{-- Tabelle --}} +
+ + + + + + + + + + + + + + @forelse ($finances as $entry) + + + + + + + + + + @empty + + + + @endforelse + +
{{ __('admin.finance_date') }}{{ __('admin.finance_type') }}{{ __('admin.finance_category') }}{{ __('ui.team') }}{{ __('admin.finance_title') }}{{ __('admin.finance_amount') }}{{ __('admin.actions') }}
{{ $entry->date->format('d.m.Y') }} + @if ($entry->type === \App\Enums\FinanceType::Income) + {{ __('admin.finance_income') }} + @else + {{ __('admin.finance_expense') }} + @endif + {{ $entry->category->label() }}{{ $entry->team?->name ?? '–' }} + {{ $entry->title }} + @if ($entry->notes) + 💬 + @endif + + {{ $entry->type === \App\Enums\FinanceType::Income ? '+' : '-' }}{{ $entry->formatted_amount }} + + {{ __('ui.edit') }} +
+ @csrf + @method('DELETE') + +
+
{{ __('admin.finance_no_entries') }}
+
+ + {{-- Pagination --}} + @if ($finances->hasPages()) +
{{ $finances->links() }}
+ @endif + + {{-- Chart.js --}} + @if ($totalIncome > 0 || $totalExpense > 0) + @push('scripts') + + + @endpush + @endif +
diff --git a/routes/web.php b/routes/web.php index 96e4b11..283f957 100755 --- a/routes/web.php +++ b/routes/web.php @@ -30,6 +30,8 @@ use App\Http\Controllers\Admin\SettingsController; use App\Http\Controllers\Admin\ListGeneratorController; use App\Http\Controllers\Admin\StatisticsController; use App\Http\Controllers\Admin\SupportController; +use App\Http\Controllers\Admin\FinanceController; +use App\Http\Controllers\Admin\SeasonController; use Illuminate\Support\Facades\Route; // ------------------------------------------------------- @@ -77,7 +79,7 @@ Route::get('/club-logo', function () { } // 2. Fallback: statisches Logo - $fallback = public_path('images/logo_woelfe.png'); + $fallback = public_path('images/vereinos_logo.png'); if (file_exists($fallback)) { return response()->file($fallback, [ 'Cache-Control' => 'public, max-age=86400', @@ -159,6 +161,14 @@ Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(fun Route::get('statistics', [StatisticsController::class, 'index'])->name('statistics.index'); Route::get('statistics/player/{player}', [StatisticsController::class, 'playerDetail'])->name('statistics.player-detail'); + // Finanzen (fuer alle Admin-Panel-Nutzer mit Feature-Sichtbarkeit) + Route::get('finances', [FinanceController::class, 'index'])->name('finances.index'); + Route::get('finances/create', [FinanceController::class, 'create'])->name('finances.create'); + Route::post('finances', [FinanceController::class, 'store'])->name('finances.store'); + Route::get('finances/{finance}/edit', [FinanceController::class, 'edit'])->name('finances.edit'); + Route::put('finances/{finance}', [FinanceController::class, 'update'])->name('finances.update'); + Route::delete('finances/{finance}', [FinanceController::class, 'destroy'])->name('finances.destroy'); + // Events (Leseansicht fuer alle Admin-Panel-Nutzer) Route::get('events', [AdminEventController::class, 'index'])->name('events.index'); Route::get('events/{event}/edit', [AdminEventController::class, 'edit'])->name('events.edit'); @@ -231,6 +241,11 @@ Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(fun Route::delete('settings/demo-data', [SettingsController::class, 'destroyDemoData'])->name('settings.destroy-demo-data')->middleware('throttle:5,1'); Route::delete('settings/factory-reset', [SettingsController::class, 'factoryReset'])->name('settings.factory-reset')->middleware('throttle:3,1'); + // Saisons + Route::post('seasons', [SeasonController::class, 'store'])->name('seasons.store'); + Route::put('seasons/{season}', [SeasonController::class, 'update'])->name('seasons.update'); + Route::delete('seasons/{season}', [SeasonController::class, 'destroy'])->name('seasons.destroy'); + // Bekannte Orte Route::get('locations', [LocationController::class, 'index'])->name('locations.index'); Route::post('locations', [LocationController::class, 'store'])->name('locations.store');