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:
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>
|
||||
Reference in New Issue
Block a user