Files
WebAPP/app/Http/Controllers/Admin/ListGeneratorController.php
Rhino 8ccadbe89f Feature-Toggles, Administration, wiederkehrende Events und Event-Serien
- Administration & Rollenmanagement: Neuer Admin-Bereich mit Feature-Toggles
  und Sichtbarkeitseinstellungen pro Rolle (11 Toggles, 24 Visibility-Settings)
- AdministrationController mit eigenem Settings-Tab, aus SettingsController extrahiert
- Feature-Toggle-Guards in Controllers (Invitation, File, ListGenerator, Comment)
  und Views (events/show, events/edit, events/create)
- Setting::isFeatureEnabled() und isFeatureVisibleFor() Hilfsmethoden
- Wiederkehrende Trainings: Täglich/Wöchentlich/2-Wöchentlich mit Ende per
  Datum oder Anzahl (max. 52), Vorschau im Formular
- Event-Serien: Verknüpfung über event_series_id (UUID), Modal-Dialog beim
  Speichern und Löschen mit Optionen "nur dieses" / "alle folgenden"
- Löschen-Button direkt in der Event-Bearbeitung mit Serien-Dialog
- DemoDataSeeder: 4 Trainings als Serie mit gemeinsamer event_series_id
- Übersetzungen in allen 6 Sprachen (de, en, pl, ru, ar, tr)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:38:45 +01:00

265 lines
8.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Models\File;
use App\Models\FileCategory;
use App\Models\Player;
use App\Models\Setting;
use App\Models\Team;
use App\Models\User;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\View\View;
class ListGeneratorController extends Controller
{
public function create(): View
{
if (!Setting::isFeatureVisibleFor('list_generator', auth()->user())) {
abort(403);
}
$teams = Team::where('is_active', true)->orderBy('name')->get();
return view('admin.list-generator.create', compact('teams'));
}
public function store(Request $request): View
{
if (!Setting::isFeatureVisibleFor('list_generator', auth()->user())) {
abort(403);
}
$validated = $request->validate([
'title' => 'required|string|max:255',
'subtitle' => 'nullable|string|max:255',
'notes' => 'nullable|string|max:2000',
'team_id' => 'nullable|exists:teams,id',
'source' => 'required|in:players,parents,freetext',
'freetext_rows' => 'nullable|required_if:source,freetext|string|max:50000',
'columns' => 'nullable|array',
'custom_columns' => 'nullable|array',
'custom_columns.*' => 'string|max:100',
]);
$columns = $this->buildColumns($validated);
$rows = $this->buildRows($validated, $columns);
// Auto-detect orientation and font size for single-page PDF
$colCount = count($columns);
$rowCount = count($rows);
$orientation = $colCount > 4 ? 'landscape' : 'portrait';
// Font size calculation for single-page fit
$fontSize = 10;
if ($rowCount > 35) {
$fontSize = 7;
} elseif ($rowCount > 25) {
$fontSize = 8;
} elseif ($rowCount > 15) {
$fontSize = 9;
}
$viewData = [
'title' => $validated['title'],
'subtitle' => $validated['subtitle'] ?? null,
'notes' => $validated['notes'] ?? null,
'columns' => $columns,
'rows' => $rows,
'generatedAt' => now(),
'orientation' => $orientation,
'fontSize' => $fontSize,
];
// Generate PDF
$pdf = Pdf::loadView('admin.list-generator.document', $viewData)
->setPaper('a4', $orientation);
$pdfContent = $pdf->output();
// Save to file library
$category = FileCategory::where('slug', 'allgemein')->firstOrFail();
$storedName = Str::uuid() . '.pdf';
Storage::disk('local')->put('files/' . $storedName, $pdfContent);
$file = new File([
'file_category_id' => $category->id,
'original_name' => Str::slug($validated['title']) . '.pdf',
'mime_type' => 'application/pdf',
'size' => strlen($pdfContent),
]);
$file->stored_name = $storedName;
$file->disk = 'private';
$file->uploaded_by = auth()->id();
$file->save();
ActivityLog::log('created', __('admin.log_list_generated', ['title' => $validated['title']]), 'File', $file->id);
return view('admin.list-generator.result', [
'title' => $validated['title'],
'subtitle' => $validated['subtitle'] ?? null,
'notes' => $validated['notes'] ?? null,
'columns' => $columns,
'rows' => $rows,
'file' => $file,
]);
}
private function buildColumns(array $data): array
{
$columns = ['name' => __('ui.name')];
$selected = $data['columns'] ?? [];
$playerColumns = [
'team' => __('admin.nav_teams'),
'jersey_number' => __('admin.jersey_number'),
'birth_year' => __('admin.birth_year'),
'parents' => __('admin.parents'),
'photo_permission' => __('admin.photo_permission'),
];
$parentColumns = [
'team' => __('admin.nav_teams'),
'email' => __('ui.email'),
'phone' => __('admin.phone'),
'children' => __('admin.children'),
];
$available = match ($data['source']) {
'players' => $playerColumns,
'parents' => $parentColumns,
default => [],
};
foreach ($selected as $col) {
if (isset($available[$col])) {
$columns[$col] = $available[$col];
}
}
foreach (($data['custom_columns'] ?? []) as $i => $header) {
$header = trim($header);
if ($header !== '') {
$columns['custom_' . $i] = $header;
}
}
return $columns;
}
private function buildRows(array $data, array $columns): array
{
if ($data['source'] === 'freetext') {
return $this->buildFreetextRows($data);
}
if ($data['source'] === 'players') {
return $this->buildPlayerRows($data, $columns);
}
return $this->buildParentRows($data, $columns);
}
private function buildPlayerRows(array $data, array $columns): array
{
$query = Player::with(['team', 'parents'])->where('is_active', true);
if (!empty($data['team_id'])) {
$query->where('team_id', $data['team_id']);
}
$query->orderBy('last_name')->orderBy('first_name');
$players = $query->get();
$rows = [];
foreach ($players as $player) {
$row = ['name' => $player->full_name];
if (isset($columns['team'])) {
$row['team'] = $player->team->name ?? '';
}
if (isset($columns['jersey_number'])) {
$row['jersey_number'] = $player->jersey_number ?? '';
}
if (isset($columns['birth_year'])) {
$row['birth_year'] = $player->birth_year ?? '';
}
if (isset($columns['parents'])) {
$row['parents'] = $player->parents->map(fn ($p) => $p->name)->implode(', ') ?: '';
}
if (isset($columns['photo_permission'])) {
$row['photo_permission'] = $player->photo_permission ? __('ui.yes') : __('ui.no');
}
foreach ($columns as $key => $header) {
if (str_starts_with($key, 'custom_')) {
$row[$key] = '';
}
}
$rows[] = $row;
}
return $rows;
}
private function buildParentRows(array $data, array $columns): array
{
$query = User::with('children.team')->where('is_active', true);
if (!empty($data['team_id'])) {
$query->whereHas('children', fn ($q) => $q->where('team_id', $data['team_id']));
}
$query->orderBy('name');
$users = $query->get();
$rows = [];
foreach ($users as $user) {
$row = ['name' => $user->name];
if (isset($columns['team'])) {
$teamNames = $user->children->pluck('team.name')->filter()->unique()->implode(', ');
$row['team'] = $teamNames ?: '';
}
if (isset($columns['email'])) {
$row['email'] = $user->email;
}
if (isset($columns['phone'])) {
$row['phone'] = $user->phone ?? '';
}
if (isset($columns['children'])) {
$row['children'] = $user->children->map(fn ($c) => $c->first_name)->implode(', ') ?: '';
}
foreach ($columns as $key => $header) {
if (str_starts_with($key, 'custom_')) {
$row[$key] = '';
}
}
$rows[] = $row;
}
return $rows;
}
private function buildFreetextRows(array $data): array
{
$lines = array_filter(
array_map('trim', explode("\n", $data['freetext_rows'] ?? '')),
fn ($line) => $line !== ''
);
// Maximum 200 Zeilen — DoS-Schutz (V10)
$lines = array_slice($lines, 0, 200);
return array_map(fn ($line) => ['name' => $line], array_values($lines));
}
}