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:
Rhino
2026-03-02 23:48:20 +01:00
parent 480e2284ba
commit 4eaf2368af
18 changed files with 1270 additions and 1 deletions

View 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>