- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er) - Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link - Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest - Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen) - Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
256 lines
8.0 KiB
PHP
256 lines
8.0 KiB
PHP
<?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\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
|
||
{
|
||
$teams = Team::where('is_active', true)->orderBy('name')->get();
|
||
|
||
return view('admin.list-generator.create', compact('teams'));
|
||
}
|
||
|
||
public function store(Request $request): View
|
||
{
|
||
$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));
|
||
}
|
||
}
|