Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill

- 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>
This commit is contained in:
Rhino
2026-03-02 07:30:37 +01:00
commit 2e24a40d68
9633 changed files with 1300799 additions and 0 deletions

View File

@@ -0,0 +1,255 @@
<?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));
}
}