Finanzverwaltung und Saison-System
Neues Einnahmen-/Ausgaben-Modul mit Kategorie-Filter, Monats-Charts und Saison-basierter Filterung. Saison-Verwaltung im Admin-Bereich mit Möglichkeit zum Wechsel der aktuellen Saison. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
27
app/Enums/FinanceCategory.php
Normal file
27
app/Enums/FinanceCategory.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum FinanceCategory: string
|
||||||
|
{
|
||||||
|
case Catering = 'catering';
|
||||||
|
case Sponsoring = 'sponsoring';
|
||||||
|
case Membership = 'membership';
|
||||||
|
case TournamentFees = 'tournament_fees';
|
||||||
|
case Equipment = 'equipment';
|
||||||
|
case Transport = 'transport';
|
||||||
|
case VenueRental = 'venue_rental';
|
||||||
|
case TrainingMaterial = 'training_material';
|
||||||
|
case Events = 'events';
|
||||||
|
case Other = 'other';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return __("ui.enums.finance_category.{$this->value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_column(self::cases(), 'value');
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Enums/FinanceType.php
Normal file
19
app/Enums/FinanceType.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum FinanceType: string
|
||||||
|
{
|
||||||
|
case Income = 'income';
|
||||||
|
case Expense = 'expense';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return __("ui.enums.finance_type.{$this->value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_column(self::cases(), 'value');
|
||||||
|
}
|
||||||
|
}
|
||||||
272
app/Http/Controllers/Admin/FinanceController.php
Normal file
272
app/Http/Controllers/Admin/FinanceController.php
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Enums\FinanceCategory;
|
||||||
|
use App\Enums\FinanceType;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\ActivityLog;
|
||||||
|
use App\Models\Finance;
|
||||||
|
use App\Models\Season;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Models\Team;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class FinanceController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
if (!Setting::isFeatureVisibleFor('finances', auth()->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)),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/Http/Controllers/Admin/SeasonController.php
Normal file
69
app/Http/Controllers/Admin/SeasonController.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Finance;
|
||||||
|
use App\Models\Season;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SeasonController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request): 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)->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Models/Finance.php
Normal file
38
app/Models/Finance.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\FinanceCategory;
|
||||||
|
use App\Enums\FinanceType;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Finance extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['team_id', 'type', 'category', 'title', 'amount', 'date', 'notes'];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 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, ',', '.') . ' €';
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/Models/Season.php
Normal file
34
app/Models/Season.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Season extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['name', 'start_date', 'end_date', 'is_current'];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'start_date' => '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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('seasons', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('finances', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -566,6 +566,9 @@ return [
|
|||||||
'stats_total_shots' => 'إجمالي التسديدات',
|
'stats_total_shots' => 'إجمالي التسديدات',
|
||||||
'stats_gk_appearances' => 'مباريات كحارس',
|
'stats_gk_appearances' => 'مباريات كحارس',
|
||||||
'stats_total_saves' => 'إجمالي التصديات',
|
'stats_total_saves' => 'إجمالي التصديات',
|
||||||
|
'stats_penalties' => 'رميات جزاء',
|
||||||
|
'stats_cards' => 'عقوبات',
|
||||||
|
'stats_avg_time' => '⌀ وقت اللعب',
|
||||||
'stats_close' => 'إغلاق',
|
'stats_close' => 'إغلاق',
|
||||||
'player_goals' => 'أهداف',
|
'player_goals' => 'أهداف',
|
||||||
|
|
||||||
@@ -576,4 +579,52 @@ return [
|
|||||||
'performance_good' => 'جيد',
|
'performance_good' => 'جيد',
|
||||||
'performance_average' => 'متوسط',
|
'performance_average' => 'متوسط',
|
||||||
'performance_below' => 'أقل من المتوسط',
|
'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' => 'المالية',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -602,6 +602,9 @@ return [
|
|||||||
'stats_total_shots' => 'Gesamtwürfe',
|
'stats_total_shots' => 'Gesamtwürfe',
|
||||||
'stats_gk_appearances' => 'TW-Einsätze',
|
'stats_gk_appearances' => 'TW-Einsätze',
|
||||||
'stats_total_saves' => 'Gesamtparaden',
|
'stats_total_saves' => 'Gesamtparaden',
|
||||||
|
'stats_penalties' => '7-Meter',
|
||||||
|
'stats_cards' => 'Strafen',
|
||||||
|
'stats_avg_time' => '⌀ Spielzeit',
|
||||||
'stats_close' => 'Schließen',
|
'stats_close' => 'Schließen',
|
||||||
'player_goals' => 'Tore',
|
'player_goals' => 'Tore',
|
||||||
|
|
||||||
@@ -612,4 +615,52 @@ return [
|
|||||||
'performance_good' => 'Gut',
|
'performance_good' => 'Gut',
|
||||||
'performance_average' => 'Mittel',
|
'performance_average' => 'Mittel',
|
||||||
'performance_below' => 'Unterdurchschnittlich',
|
'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',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -565,6 +565,9 @@ return [
|
|||||||
'stats_total_shots' => 'Total shots',
|
'stats_total_shots' => 'Total shots',
|
||||||
'stats_gk_appearances' => 'GK appearances',
|
'stats_gk_appearances' => 'GK appearances',
|
||||||
'stats_total_saves' => 'Total saves',
|
'stats_total_saves' => 'Total saves',
|
||||||
|
'stats_penalties' => 'Penalties',
|
||||||
|
'stats_cards' => 'Cards',
|
||||||
|
'stats_avg_time' => 'Avg. time',
|
||||||
'stats_close' => 'Close',
|
'stats_close' => 'Close',
|
||||||
'player_goals' => 'Goals',
|
'player_goals' => 'Goals',
|
||||||
|
|
||||||
@@ -575,4 +578,52 @@ return [
|
|||||||
'performance_good' => 'Good',
|
'performance_good' => 'Good',
|
||||||
'performance_average' => 'Average',
|
'performance_average' => 'Average',
|
||||||
'performance_below' => 'Below 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',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -566,6 +566,9 @@ return [
|
|||||||
'stats_total_shots' => 'Łączne rzuty',
|
'stats_total_shots' => 'Łączne rzuty',
|
||||||
'stats_gk_appearances' => 'Występy jako bramkarz',
|
'stats_gk_appearances' => 'Występy jako bramkarz',
|
||||||
'stats_total_saves' => 'Łączne obrony',
|
'stats_total_saves' => 'Łączne obrony',
|
||||||
|
'stats_penalties' => 'Rzuty karne',
|
||||||
|
'stats_cards' => 'Kary',
|
||||||
|
'stats_avg_time' => '⌀ Czas gry',
|
||||||
'stats_close' => 'Zamknij',
|
'stats_close' => 'Zamknij',
|
||||||
'player_goals' => 'Bramki',
|
'player_goals' => 'Bramki',
|
||||||
|
|
||||||
@@ -576,4 +579,52 @@ return [
|
|||||||
'performance_good' => 'Dobrze',
|
'performance_good' => 'Dobrze',
|
||||||
'performance_average' => 'Średnio',
|
'performance_average' => 'Średnio',
|
||||||
'performance_below' => 'Poniżej średniej',
|
'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',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -584,6 +584,9 @@ return [
|
|||||||
'stats_total_shots' => 'Всего бросков',
|
'stats_total_shots' => 'Всего бросков',
|
||||||
'stats_gk_appearances' => 'Игры вратарём',
|
'stats_gk_appearances' => 'Игры вратарём',
|
||||||
'stats_total_saves' => 'Всего отражений',
|
'stats_total_saves' => 'Всего отражений',
|
||||||
|
'stats_penalties' => 'Пенальти',
|
||||||
|
'stats_cards' => 'Наказания',
|
||||||
|
'stats_avg_time' => '⌀ Время',
|
||||||
'stats_close' => 'Закрыть',
|
'stats_close' => 'Закрыть',
|
||||||
'player_goals' => 'Голы',
|
'player_goals' => 'Голы',
|
||||||
|
|
||||||
@@ -594,4 +597,52 @@ return [
|
|||||||
'performance_good' => 'Хорошо',
|
'performance_good' => 'Хорошо',
|
||||||
'performance_average' => 'Средне',
|
'performance_average' => 'Средне',
|
||||||
'performance_below' => 'Ниже среднего',
|
'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' => 'Финансы',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -584,6 +584,9 @@ return [
|
|||||||
'stats_total_shots' => 'Toplam atışlar',
|
'stats_total_shots' => 'Toplam atışlar',
|
||||||
'stats_gk_appearances' => 'Kaleci maçları',
|
'stats_gk_appearances' => 'Kaleci maçları',
|
||||||
'stats_total_saves' => 'Toplam kurtarışlar',
|
'stats_total_saves' => 'Toplam kurtarışlar',
|
||||||
|
'stats_penalties' => 'Penaltılar',
|
||||||
|
'stats_cards' => 'Cezalar',
|
||||||
|
'stats_avg_time' => '⌀ Süre',
|
||||||
'stats_close' => 'Kapat',
|
'stats_close' => 'Kapat',
|
||||||
'player_goals' => 'Goller',
|
'player_goals' => 'Goller',
|
||||||
|
|
||||||
@@ -594,4 +597,52 @@ return [
|
|||||||
'performance_good' => 'İyi',
|
'performance_good' => 'İyi',
|
||||||
'performance_average' => 'Orta',
|
'performance_average' => 'Orta',
|
||||||
'performance_below' => 'Ortalamanın altı',
|
'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',
|
||||||
];
|
];
|
||||||
|
|||||||
94
resources/views/admin/finances/create.blade.php
Normal file
94
resources/views/admin/finances/create.blade.php
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<x-layouts.admin :title="__('admin.new_finance')">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">{{ __('admin.new_finance') }}</h1>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 max-w-lg">
|
||||||
|
<form method="POST" action="{{ route('admin.finances.store') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
{{-- Typ --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('admin.finance_type') }} *</label>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input type="radio" name="type" value="income" {{ old('type', 'income') === 'income' ? 'checked' : '' }} class="text-green-600 focus:ring-green-500">
|
||||||
|
<span class="text-sm text-gray-700">{{ __('admin.finance_income') }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input type="radio" name="type" value="expense" {{ old('type') === 'expense' ? 'checked' : '' }} class="text-red-600 focus:ring-red-500">
|
||||||
|
<span class="text-sm text-gray-700">{{ __('admin.finance_expense') }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@error('type')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Kategorie --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="category" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.finance_category') }} *</label>
|
||||||
|
<select name="category" id="category" required class="w-full px-3 py-2 border border-gray-300 rounded-md @error('category') border-red-500 @enderror">
|
||||||
|
<option value="">{{ __('admin.please_select') }}</option>
|
||||||
|
@foreach (\App\Enums\FinanceCategory::cases() as $cat)
|
||||||
|
<option value="{{ $cat->value }}" {{ old('category') === $cat->value ? 'selected' : '' }}>{{ $cat->label() }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('category')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Titel --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.finance_title') }} *</label>
|
||||||
|
<input type="text" name="title" id="title" value="{{ old('title') }}" required maxlength="150"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('title') border-red-500 @enderror">
|
||||||
|
@error('title')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Betrag + Datum --}}
|
||||||
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label for="amount" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.finance_amount') }} *</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="number" name="amount" id="amount" value="{{ old('amount') }}" required step="0.01" min="0.01" max="999999.99"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md pr-8 @error('amount') border-red-500 @enderror">
|
||||||
|
<span class="absolute right-3 top-2.5 text-sm text-gray-400">€</span>
|
||||||
|
</div>
|
||||||
|
@error('amount')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="date" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.finance_date') }} *</label>
|
||||||
|
<input type="date" name="date" id="date" value="{{ old('date', now()->format('Y-m-d')) }}" required
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('date') border-red-500 @enderror">
|
||||||
|
@error('date')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Team --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }}</label>
|
||||||
|
<select name="team_id" id="team_id" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="">{{ __('admin.finance_no_team') }}</option>
|
||||||
|
@foreach ($teams as $team)
|
||||||
|
<option value="{{ $team->id }}" {{ old('team_id') == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('team_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Notizen --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="notes" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.finance_notes') }}</label>
|
||||||
|
<textarea name="notes" id="notes" rows="2" maxlength="1000"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('notes') border-red-500 @enderror">{{ old('notes') }}</textarea>
|
||||||
|
@error('notes')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Buttons --}}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium">
|
||||||
|
{{ __('ui.create') }}
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('admin.finances.index') }}" class="text-sm text-gray-600 hover:underline">
|
||||||
|
{{ __('ui.cancel') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</x-layouts.admin>
|
||||||
95
resources/views/admin/finances/edit.blade.php
Normal file
95
resources/views/admin/finances/edit.blade.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<x-layouts.admin :title="__('admin.finance_edit')">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">{{ __('admin.finance_edit') }}</h1>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 max-w-lg">
|
||||||
|
<form method="POST" action="{{ route('admin.finances.update', $finance) }}">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
{{-- Typ --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('admin.finance_type') }} *</label>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input type="radio" name="type" value="income" {{ old('type', $finance->type->value) === 'income' ? 'checked' : '' }} class="text-green-600 focus:ring-green-500">
|
||||||
|
<span class="text-sm text-gray-700">{{ __('admin.finance_income') }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input type="radio" name="type" value="expense" {{ old('type', $finance->type->value) === 'expense' ? 'checked' : '' }} class="text-red-600 focus:ring-red-500">
|
||||||
|
<span class="text-sm text-gray-700">{{ __('admin.finance_expense') }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@error('type')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Kategorie --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="category" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.finance_category') }} *</label>
|
||||||
|
<select name="category" id="category" required class="w-full px-3 py-2 border border-gray-300 rounded-md @error('category') border-red-500 @enderror">
|
||||||
|
<option value="">{{ __('admin.please_select') }}</option>
|
||||||
|
@foreach (\App\Enums\FinanceCategory::cases() as $cat)
|
||||||
|
<option value="{{ $cat->value }}" {{ old('category', $finance->category->value) === $cat->value ? 'selected' : '' }}>{{ $cat->label() }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('category')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Titel --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.finance_title') }} *</label>
|
||||||
|
<input type="text" name="title" id="title" value="{{ old('title', $finance->title) }}" required maxlength="150"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('title') border-red-500 @enderror">
|
||||||
|
@error('title')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Betrag + Datum --}}
|
||||||
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label for="amount" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.finance_amount') }} *</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="number" name="amount" id="amount" value="{{ old('amount', number_format($finance->amount / 100, 2, '.', '')) }}" required step="0.01" min="0.01" max="999999.99"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md pr-8 @error('amount') border-red-500 @enderror">
|
||||||
|
<span class="absolute right-3 top-2.5 text-sm text-gray-400">€</span>
|
||||||
|
</div>
|
||||||
|
@error('amount')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="date" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.finance_date') }} *</label>
|
||||||
|
<input type="date" name="date" id="date" value="{{ old('date', $finance->date->format('Y-m-d')) }}" required
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('date') border-red-500 @enderror">
|
||||||
|
@error('date')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Team --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }}</label>
|
||||||
|
<select name="team_id" id="team_id" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="">{{ __('admin.finance_no_team') }}</option>
|
||||||
|
@foreach ($teams as $team)
|
||||||
|
<option value="{{ $team->id }}" {{ old('team_id', $finance->team_id) == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('team_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Notizen --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="notes" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.finance_notes') }}</label>
|
||||||
|
<textarea name="notes" id="notes" rows="2" maxlength="1000"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('notes') border-red-500 @enderror">{{ old('notes', $finance->notes) }}</textarea>
|
||||||
|
@error('notes')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Buttons --}}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium">
|
||||||
|
{{ __('ui.save') }}
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('admin.finances.index') }}" class="text-sm text-gray-600 hover:underline">
|
||||||
|
{{ __('ui.cancel') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</x-layouts.admin>
|
||||||
243
resources/views/admin/finances/index.blade.php
Normal file
243
resources/views/admin/finances/index.blade.php
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<x-layouts.admin :title="__('admin.finances_title')">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-bold">{{ __('admin.finances_title') }}</h1>
|
||||||
|
<a href="{{ route('admin.finances.create') }}" class="inline-flex items-center gap-1.5 bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700 transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||||
|
{{ __('admin.new_finance') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Filter --}}
|
||||||
|
<form method="GET" action="{{ route('admin.finances.index') }}" class="bg-white rounded-lg shadow p-4 mb-6">
|
||||||
|
<div class="flex flex-wrap items-end gap-3">
|
||||||
|
{{-- Saison --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.season') }}</label>
|
||||||
|
<select name="season_id" class="px-2 py-1.5 border border-gray-300 rounded-md text-sm" onchange="this.form.submit()">
|
||||||
|
<option value="">{{ __('admin.all_seasons') }}</option>
|
||||||
|
@foreach ($seasons as $season)
|
||||||
|
<option value="{{ $season->id }}" {{ request('season_id', $activeSeason?->id) == $season->id ? 'selected' : '' }}>
|
||||||
|
{{ $season->name }}{{ $season->is_current ? ' ●' : '' }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Jahr --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.filter_year') }}</label>
|
||||||
|
<select name="year" class="px-2 py-1.5 border border-gray-300 rounded-md text-sm" onchange="this.form.submit()">
|
||||||
|
<option value="">–</option>
|
||||||
|
@foreach ($years as $y)
|
||||||
|
<option value="{{ $y }}" {{ request('year') == $y ? 'selected' : '' }}>{{ $y }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Team --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">{{ __('ui.team') }}</label>
|
||||||
|
<select name="team_id" class="px-2 py-1.5 border border-gray-300 rounded-md text-sm" onchange="this.form.submit()">
|
||||||
|
<option value="">{{ __('ui.all_teams') }}</option>
|
||||||
|
@foreach ($teams as $team)
|
||||||
|
<option value="{{ $team->id }}" {{ request('team_id') == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Typ --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.finance_type') }}</label>
|
||||||
|
<select name="type" class="px-2 py-1.5 border border-gray-300 rounded-md text-sm" onchange="this.form.submit()">
|
||||||
|
<option value="">{{ __('admin.finance_all_types') }}</option>
|
||||||
|
<option value="income" {{ request('type') === 'income' ? 'selected' : '' }}>{{ __('admin.finance_income') }}</option>
|
||||||
|
<option value="expense" {{ request('type') === 'expense' ? 'selected' : '' }}>{{ __('admin.finance_expense') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Kategorie --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.finance_category') }}</label>
|
||||||
|
<select name="category" class="px-2 py-1.5 border border-gray-300 rounded-md text-sm" onchange="this.form.submit()">
|
||||||
|
<option value="">{{ __('admin.finance_all_categories') }}</option>
|
||||||
|
@foreach (\App\Enums\FinanceCategory::cases() as $cat)
|
||||||
|
<option value="{{ $cat->value }}" {{ request('category') === $cat->value ? 'selected' : '' }}>{{ $cat->label() }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{-- Bilanz-Cards --}}
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||||
|
<div class="bg-green-50 rounded-lg shadow p-4 text-center">
|
||||||
|
<div class="text-xs font-medium text-green-600 uppercase tracking-wider mb-1">{{ __('admin.finance_total_income') }}</div>
|
||||||
|
<div class="text-2xl font-bold text-green-700">{{ number_format($totalIncome / 100, 2, ',', '.') }} €</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-red-50 rounded-lg shadow p-4 text-center">
|
||||||
|
<div class="text-xs font-medium text-red-600 uppercase tracking-wider mb-1">{{ __('admin.finance_total_expense') }}</div>
|
||||||
|
<div class="text-2xl font-bold text-red-700">{{ number_format($totalExpense / 100, 2, ',', '.') }} €</div>
|
||||||
|
</div>
|
||||||
|
<div class="{{ $balance >= 0 ? 'bg-green-50' : 'bg-red-50' }} rounded-lg shadow p-4 text-center">
|
||||||
|
<div class="text-xs font-medium {{ $balance >= 0 ? 'text-green-600' : 'text-red-600' }} uppercase tracking-wider mb-1">{{ __('admin.finance_balance') }}</div>
|
||||||
|
<div class="text-2xl font-bold {{ $balance >= 0 ? 'text-green-700' : 'text-red-700' }}">
|
||||||
|
{{ $balance >= 0 ? '+' : '' }}{{ number_format($balance / 100, 2, ',', '.') }} €
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Charts --}}
|
||||||
|
@if ($totalIncome > 0 || $totalExpense > 0)
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
|
||||||
|
{{-- Monatliche Uebersicht --}}
|
||||||
|
@if (!empty($chartMonthly['labels']))
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.finance_chart_monthly') }}</h3>
|
||||||
|
<div class="relative" style="height: 250px;">
|
||||||
|
<canvas id="chartMonthly"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Kategorie-Aufschluesselung --}}
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.finance_chart_categories') }}</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
@if (!empty($chartCategories['income']['data']))
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-center text-green-600 font-medium mb-2">{{ __('admin.finance_income') }}</p>
|
||||||
|
<div class="relative" style="height: 200px;">
|
||||||
|
<canvas id="chartCatIncome"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if (!empty($chartCategories['expense']['data']))
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-center text-red-600 font-medium mb-2">{{ __('admin.finance_expense') }}</p>
|
||||||
|
<div class="relative" style="height: 200px;">
|
||||||
|
<canvas id="chartCatExpense"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Tabelle --}}
|
||||||
|
<div class="bg-white rounded-lg shadow overflow-hidden overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-gray-50 border-b">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-gray-600">{{ __('admin.finance_date') }}</th>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-gray-600">{{ __('admin.finance_type') }}</th>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-gray-600">{{ __('admin.finance_category') }}</th>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-gray-600">{{ __('ui.team') }}</th>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-gray-600">{{ __('admin.finance_title') }}</th>
|
||||||
|
<th class="text-right px-4 py-3 font-medium text-gray-600">{{ __('admin.finance_amount') }}</th>
|
||||||
|
<th class="text-right px-4 py-3 font-medium text-gray-600">{{ __('admin.actions') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100">
|
||||||
|
@forelse ($finances as $entry)
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap">{{ $entry->date->format('d.m.Y') }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
@if ($entry->type === \App\Enums\FinanceType::Income)
|
||||||
|
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">{{ __('admin.finance_income') }}</span>
|
||||||
|
@else
|
||||||
|
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">{{ __('admin.finance_expense') }}</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">{{ $entry->category->label() }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-500">{{ $entry->team?->name ?? '–' }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
{{ $entry->title }}
|
||||||
|
@if ($entry->notes)
|
||||||
|
<span class="text-gray-400 text-xs ml-1" title="{{ $entry->notes }}">💬</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right font-medium whitespace-nowrap {{ $entry->type === \App\Enums\FinanceType::Income ? 'text-green-700' : 'text-red-700' }}">
|
||||||
|
{{ $entry->type === \App\Enums\FinanceType::Income ? '+' : '-' }}{{ $entry->formatted_amount }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right whitespace-nowrap space-x-2">
|
||||||
|
<a href="{{ route('admin.finances.edit', $entry) }}" class="text-blue-600 hover:underline text-sm">{{ __('ui.edit') }}</a>
|
||||||
|
<form method="POST" action="{{ route('admin.finances.destroy', $entry) }}" class="inline" onsubmit="return confirm(@js(__('admin.finance_confirm_delete')))">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="text-red-600 hover:underline text-sm">{{ __('ui.delete') }}</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="px-4 py-8 text-center text-gray-500">{{ __('admin.finance_no_entries') }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Pagination --}}
|
||||||
|
@if ($finances->hasPages())
|
||||||
|
<div class="mt-4">{{ $finances->links() }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Chart.js --}}
|
||||||
|
@if ($totalIncome > 0 || $totalExpense > 0)
|
||||||
|
@push('scripts')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"
|
||||||
|
integrity="sha384-vsrfeLOOY6KuIYKDlmVH5UiBmgIdB1oEf7p01YgWHuqmOHfZr374+odEv96n9tNC"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const monthlyData = @js($chartMonthly);
|
||||||
|
const catData = @js($chartCategories);
|
||||||
|
|
||||||
|
// Monatliche Uebersicht
|
||||||
|
if (monthlyData.labels.length && document.getElementById('chartMonthly')) {
|
||||||
|
new Chart(document.getElementById('chartMonthly'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: monthlyData.labels,
|
||||||
|
datasets: [
|
||||||
|
{ label: @js(__('admin.finance_income')), data: monthlyData.income, backgroundColor: '#3e7750', borderRadius: 3 },
|
||||||
|
{ label: @js(__('admin.finance_expense')), data: monthlyData.expense, backgroundColor: '#8f504b', borderRadius: 3 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: { y: { beginAtZero: true, ticks: { callback: v => v + ' €' } } },
|
||||||
|
plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 11 } } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kategorie-Charts
|
||||||
|
const doughnutOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, font: { size: 10 } } } }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (catData.income.data.length && document.getElementById('chartCatIncome')) {
|
||||||
|
new Chart(document.getElementById('chartCatIncome'), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: { labels: catData.income.labels, datasets: [{ data: catData.income.data, backgroundColor: catData.income.colors }] },
|
||||||
|
options: doughnutOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catData.expense.data.length && document.getElementById('chartCatExpense')) {
|
||||||
|
new Chart(document.getElementById('chartCatExpense'), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: { labels: catData.expense.labels, datasets: [{ data: catData.expense.data, backgroundColor: catData.expense.colors }] },
|
||||||
|
options: doughnutOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endif
|
||||||
|
</x-layouts.admin>
|
||||||
@@ -30,6 +30,8 @@ use App\Http\Controllers\Admin\SettingsController;
|
|||||||
use App\Http\Controllers\Admin\ListGeneratorController;
|
use App\Http\Controllers\Admin\ListGeneratorController;
|
||||||
use App\Http\Controllers\Admin\StatisticsController;
|
use App\Http\Controllers\Admin\StatisticsController;
|
||||||
use App\Http\Controllers\Admin\SupportController;
|
use App\Http\Controllers\Admin\SupportController;
|
||||||
|
use App\Http\Controllers\Admin\FinanceController;
|
||||||
|
use App\Http\Controllers\Admin\SeasonController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
@@ -77,7 +79,7 @@ Route::get('/club-logo', function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback: statisches Logo
|
// 2. Fallback: statisches Logo
|
||||||
$fallback = public_path('images/logo_woelfe.png');
|
$fallback = public_path('images/vereinos_logo.png');
|
||||||
if (file_exists($fallback)) {
|
if (file_exists($fallback)) {
|
||||||
return response()->file($fallback, [
|
return response()->file($fallback, [
|
||||||
'Cache-Control' => 'public, max-age=86400',
|
'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', [StatisticsController::class, 'index'])->name('statistics.index');
|
||||||
Route::get('statistics/player/{player}', [StatisticsController::class, 'playerDetail'])->name('statistics.player-detail');
|
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)
|
// Events (Leseansicht fuer alle Admin-Panel-Nutzer)
|
||||||
Route::get('events', [AdminEventController::class, 'index'])->name('events.index');
|
Route::get('events', [AdminEventController::class, 'index'])->name('events.index');
|
||||||
Route::get('events/{event}/edit', [AdminEventController::class, 'edit'])->name('events.edit');
|
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/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');
|
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
|
// Bekannte Orte
|
||||||
Route::get('locations', [LocationController::class, 'index'])->name('locations.index');
|
Route::get('locations', [LocationController::class, 'index'])->name('locations.index');
|
||||||
Route::post('locations', [LocationController::class, 'store'])->name('locations.store');
|
Route::post('locations', [LocationController::class, 'store'])->name('locations.store');
|
||||||
|
|||||||
Reference in New Issue
Block a user