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>
This commit is contained in:
@@ -22,6 +22,7 @@ use App\Models\Team;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\HtmlSanitizerService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -105,8 +106,14 @@ class EventController extends Controller
|
||||
|
||||
ActivityLog::logWithChanges('created', __('admin.log_event_created', ['title' => $event->title]), 'Event', $event->id, null, ['title' => $event->title, 'team' => $event->team->name ?? '', 'type' => $event->type->value, 'status' => $event->status->value]);
|
||||
|
||||
$recurringCount = $this->generateRecurringEvents($event, $request);
|
||||
|
||||
$message = $recurringCount > 0
|
||||
? __('admin.recurrence_created', ['count' => $recurringCount + 1])
|
||||
: __('admin.event_created');
|
||||
|
||||
return redirect()->route('admin.events.index')
|
||||
->with('success', __('admin.event_created'));
|
||||
->with('success', $message);
|
||||
}
|
||||
|
||||
public function edit(Event $event): View
|
||||
@@ -172,8 +179,24 @@ class EventController extends Controller
|
||||
$newData = ['title' => $event->title, 'team_id' => $event->team_id, 'type' => $event->type->value, 'status' => $event->status->value, 'start_at' => $event->start_at?->toDateTimeString()];
|
||||
ActivityLog::logWithChanges('updated', __('admin.log_event_updated', ['title' => $event->title]), 'Event', $event->id, $oldData, $newData);
|
||||
|
||||
// Serien-Events: Alle folgenden aktualisieren
|
||||
$updatedFollowing = 0;
|
||||
if ($request->input('update_following') === '1' && $event->isPartOfSeries()) {
|
||||
$updatedFollowing = $this->updateFollowingSeriesEvents($event, $request);
|
||||
}
|
||||
|
||||
$recurringCount = $this->generateRecurringEvents($event, $request);
|
||||
|
||||
if ($recurringCount > 0) {
|
||||
$message = __('admin.recurrence_created', ['count' => $recurringCount + 1]);
|
||||
} elseif ($updatedFollowing > 0) {
|
||||
$message = __('admin.series_events_updated', ['count' => $updatedFollowing]);
|
||||
} else {
|
||||
$message = __('admin.event_updated');
|
||||
}
|
||||
|
||||
return redirect()->route('admin.events.index')
|
||||
->with('success', __('admin.event_updated'));
|
||||
->with('success', $message);
|
||||
}
|
||||
|
||||
public function updateParticipant(Request $request, Event $event)
|
||||
@@ -199,16 +222,34 @@ class EventController extends Controller
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function destroy(Event $event): RedirectResponse
|
||||
public function destroy(Request $request, Event $event): RedirectResponse
|
||||
{
|
||||
$deletedCount = 1;
|
||||
|
||||
// Serien-Events: Alle folgenden auch löschen
|
||||
if ($request->input('delete_following') === '1' && $event->isPartOfSeries()) {
|
||||
$followingEvents = $event->followingSeriesEvents()->get();
|
||||
foreach ($followingEvents as $futureEvent) {
|
||||
ActivityLog::logWithChanges('deleted', __('admin.log_event_deleted', ['title' => $futureEvent->title]), 'Event', $futureEvent->id, ['title' => $futureEvent->title, 'team' => $futureEvent->team->name ?? ''], null);
|
||||
$futureEvent->deleted_by = auth()->id();
|
||||
$futureEvent->save();
|
||||
$futureEvent->delete();
|
||||
$deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
ActivityLog::logWithChanges('deleted', __('admin.log_event_deleted', ['title' => $event->title]), 'Event', $event->id, ['title' => $event->title, 'team' => $event->team->name ?? ''], null);
|
||||
|
||||
$event->deleted_by = auth()->id();
|
||||
$event->save();
|
||||
$event->delete();
|
||||
|
||||
$message = $deletedCount > 1
|
||||
? __('admin.series_events_deleted', ['count' => $deletedCount])
|
||||
: __('admin.event_deleted');
|
||||
|
||||
return redirect()->route('admin.events.index')
|
||||
->with('success', __('admin.event_deleted'));
|
||||
->with('success', $message);
|
||||
}
|
||||
|
||||
public function restore(int $id): RedirectResponse
|
||||
@@ -333,6 +374,10 @@ class EventController extends Controller
|
||||
'opponent' => ['nullable', 'string', 'max:100'],
|
||||
'score_home' => ['nullable', 'integer', 'min:0', 'max:99'],
|
||||
'score_away' => ['nullable', 'integer', 'min:0', 'max:99'],
|
||||
'recurrence' => ['nullable', 'in:none,daily,weekly,biweekly'],
|
||||
'recurrence_end_type' => ['nullable', 'in:date,count'],
|
||||
'recurrence_end_date' => ['nullable', 'date'],
|
||||
'recurrence_count' => ['nullable', 'integer', 'min:1', 'max:52'],
|
||||
]);
|
||||
|
||||
// Datum und Uhrzeit zusammenführen
|
||||
@@ -340,6 +385,9 @@ class EventController extends Controller
|
||||
$validated['end_at'] = null;
|
||||
unset($validated['start_date'], $validated['start_time']);
|
||||
|
||||
// Recurrence-Felder aus validated entfernen (werden separat verarbeitet)
|
||||
unset($validated['recurrence'], $validated['recurrence_end_type'], $validated['recurrence_end_date'], $validated['recurrence_count']);
|
||||
|
||||
return $validated;
|
||||
}
|
||||
|
||||
@@ -514,4 +562,108 @@ class EventController extends Controller
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function updateFollowingSeriesEvents(Event $event, Request $request): int
|
||||
{
|
||||
$followingEvents = $event->followingSeriesEvents()->get();
|
||||
if ($followingEvents->isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$newTime = Carbon::parse($event->start_at);
|
||||
|
||||
foreach ($followingEvents as $futureEvent) {
|
||||
$futureEvent->title = $event->title;
|
||||
$futureEvent->location_name = $event->location_name;
|
||||
$futureEvent->address_text = $event->address_text;
|
||||
$futureEvent->location_lat = $event->location_lat;
|
||||
$futureEvent->location_lng = $event->location_lng;
|
||||
$futureEvent->description_html = $event->description_html;
|
||||
$futureEvent->min_players = $event->min_players;
|
||||
$futureEvent->min_catering = $event->min_catering;
|
||||
$futureEvent->min_timekeepers = $event->min_timekeepers;
|
||||
|
||||
// Uhrzeit anpassen (Datum behalten)
|
||||
$futureDate = Carbon::parse($futureEvent->start_at);
|
||||
$futureEvent->start_at = $futureDate->setTime($newTime->hour, $newTime->minute);
|
||||
|
||||
$futureEvent->updated_by = auth()->id();
|
||||
$futureEvent->save();
|
||||
|
||||
// Catering/Zeitnehmer-Zuweisungen neu synchen
|
||||
$this->syncAssignments($futureEvent, $request);
|
||||
}
|
||||
|
||||
return $followingEvents->count();
|
||||
}
|
||||
|
||||
private function generateRecurringEvents(Event $baseEvent, Request $request): int
|
||||
{
|
||||
$recurrence = $request->input('recurrence', 'none');
|
||||
if ($recurrence === 'none' || $baseEvent->type !== EventType::Training) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$interval = match ($recurrence) {
|
||||
'daily' => 1,
|
||||
'weekly' => 7,
|
||||
'biweekly' => 14,
|
||||
default => 0,
|
||||
};
|
||||
if ($interval === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Termine berechnen
|
||||
$dates = [];
|
||||
$startDate = Carbon::parse($baseEvent->start_at);
|
||||
$endType = $request->input('recurrence_end_type', 'count');
|
||||
|
||||
if ($endType === 'date') {
|
||||
$endDate = Carbon::parse($request->input('recurrence_end_date'));
|
||||
$current = $startDate->copy()->addDays($interval);
|
||||
while ($current->lte($endDate) && count($dates) < 52) {
|
||||
$dates[] = $current->copy();
|
||||
$current->addDays($interval);
|
||||
}
|
||||
} else {
|
||||
$count = min((int) $request->input('recurrence_count', 1), 52);
|
||||
for ($i = 1; $i <= $count; $i++) {
|
||||
$dates[] = $startDate->copy()->addDays($interval * $i);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($dates)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Series-ID zuweisen
|
||||
$seriesId = (string) Str::uuid();
|
||||
$baseEvent->event_series_id = $seriesId;
|
||||
$baseEvent->save();
|
||||
|
||||
// Basis-Dateien zum Verlinken
|
||||
$fileIds = $baseEvent->files()->pluck('files.id')->toArray();
|
||||
|
||||
foreach ($dates as $date) {
|
||||
$newEvent = $baseEvent->replicate(['id', 'created_at', 'updated_at', 'deleted_at', 'deleted_by']);
|
||||
$newEvent->start_at = $date;
|
||||
$newEvent->created_by = auth()->id();
|
||||
$newEvent->updated_by = auth()->id();
|
||||
$newEvent->save();
|
||||
|
||||
// Teilnehmer erstellen
|
||||
$this->createParticipantsForTeam($newEvent);
|
||||
|
||||
// Catering/Zeitnehmer-Zuweisungen kopieren
|
||||
$this->syncAssignments($newEvent, $request);
|
||||
|
||||
// Dateien verlinken (nur bestehende, keine neuen Uploads)
|
||||
if (!empty($fileIds)) {
|
||||
$newEvent->files()->attach($fileIds);
|
||||
}
|
||||
}
|
||||
|
||||
return count($dates);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user