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:
Rhino
2026-03-03 08:38:45 +01:00
parent 0990e4249c
commit 8ccadbe89f
27 changed files with 1968 additions and 698 deletions

View File

@@ -29,6 +29,7 @@ use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class DemoDataSeeder extends Seeder
{
@@ -324,7 +325,8 @@ class DemoDataSeeder extends Seeder
]
);
// 1: Training (Zukunft)
// 1: Training (Zukunft) — Teil einer Trainingsserie
$trainingSeriesId = (string) Str::uuid();
$events[] = Event::updateOrCreate(
['title' => 'Training nächste Woche', 'team_id' => $team->id, 'start_at' => now()->next('Tuesday')->setTime(17, 0)],
[
@@ -338,6 +340,7 @@ class DemoDataSeeder extends Seeder
'min_players' => 12,
'min_catering' => 1,
'min_timekeepers' => 1,
'event_series_id' => $trainingSeriesId,
'description_html' => '<p>Training mit Schwerpunkt Passspiel. Bitte pünktlich kommen!</p>',
'created_by' => $admin->id,
]
@@ -571,6 +574,29 @@ class DemoDataSeeder extends Seeder
]
);
// Serien-Folgetermine: 3 weitere Trainings (gleiche event_series_id wie Event 1)
for ($i = 1; $i <= 3; $i++) {
$tuesday = now()->next('Tuesday')->addWeeks($i);
Event::updateOrCreate(
['title' => 'Training Dienstag (Serie)', 'team_id' => $team->id, 'start_at' => $tuesday->copy()->setTime(17, 0)],
[
'type' => EventType::Training,
'end_at' => $tuesday->copy()->setTime(18, 30),
'status' => EventStatus::Published,
'location_name' => $locations[0]->name,
'address_text' => $locations[0]->address_text,
'location_lat' => $locations[0]->location_lat,
'location_lng' => $locations[0]->location_lng,
'min_players' => 12,
'min_catering' => 1,
'min_timekeepers' => 1,
'event_series_id' => $trainingSeriesId,
'description_html' => '<p>Wöchentliches Training. Bitte <strong>Hallenschuhe</strong> und ausreichend Trinken mitbringen.</p>',
'created_by' => $admin->id,
]
);
}
return $events;
}

View File

@@ -223,6 +223,30 @@ HTML;
]);
}
// Globale Feature-Toggles (Master-Schalter)
$featureToggles = [
['key' => 'feature_statistics', 'label' => 'Feature: Statistiken'],
['key' => 'feature_finances', 'label' => 'Feature: Finanzen'],
['key' => 'feature_catering', 'label' => 'Feature: Catering'],
['key' => 'feature_timekeepers', 'label' => 'Feature: Zeitnehmer'],
['key' => 'feature_carpools', 'label' => 'Feature: Fahrgemeinschaften'],
['key' => 'feature_comments', 'label' => 'Feature: Kommentare'],
['key' => 'feature_files', 'label' => 'Feature: Dateien'],
['key' => 'feature_faqs', 'label' => 'Feature: FAQs'],
['key' => 'feature_list_generator', 'label' => 'Feature: Listenerstellung'],
['key' => 'feature_invitations', 'label' => 'Feature: Einladungen'],
['key' => 'feature_player_stats', 'label' => 'Feature: Spielerstatistiken'],
];
foreach ($featureToggles as $toggle) {
$existing = Setting::where('key', $toggle['key'])->first();
if ($existing) {
$existing->update(['label' => $toggle['label'], 'type' => 'number']);
} else {
$this->createSetting(array_merge($toggle, ['type' => 'number', 'value' => '1']));
}
}
// Sichtbarkeits-Einstellungen (pro Feature pro Rolle)
$visibilitySettings = [
['key' => 'visibility_statistics_coach', 'label' => 'Statistik: Trainer', 'type' => 'number', 'value' => '1'],
@@ -231,6 +255,24 @@ HTML;
['key' => 'visibility_finances_parent_rep', 'label' => 'Finanzen: Elternvertretung', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_catering_history_coach', 'label' => 'Catering-Verlauf: Trainer', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_catering_history_parent_rep', 'label' => 'Catering-Verlauf: Elternvertretung', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_catering_coach', 'label' => 'Catering: Trainer', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_catering_parent_rep', 'label' => 'Catering: Elternvertretung', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_timekeepers_coach', 'label' => 'Zeitnehmer: Trainer', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_timekeepers_parent_rep', 'label' => 'Zeitnehmer: Elternvertretung', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_carpools_coach', 'label' => 'Fahrgemeinschaften: Trainer', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_carpools_parent_rep', 'label' => 'Fahrgemeinschaften: Elternvertretung', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_comments_coach', 'label' => 'Kommentare: Trainer', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_comments_parent_rep', 'label' => 'Kommentare: Elternvertretung', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_files_coach', 'label' => 'Dateien: Trainer', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_files_parent_rep', 'label' => 'Dateien: Elternvertretung', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_faqs_coach', 'label' => 'FAQs: Trainer', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_faqs_parent_rep', 'label' => 'FAQs: Elternvertretung', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_list_generator_coach', 'label' => 'Listenerstellung: Trainer', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_list_generator_parent_rep', 'label' => 'Listenerstellung: Elternvertretung', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_invitations_coach', 'label' => 'Einladungen: Trainer', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_invitations_parent_rep', 'label' => 'Einladungen: Elternvertretung', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_player_stats_coach', 'label' => 'Spielerstatistiken: Trainer', 'type' => 'number', 'value' => '1'],
['key' => 'visibility_player_stats_parent_rep', 'label' => 'Spielerstatistiken: Elternvertretung', 'type' => 'number', 'value' => '1'],
];
foreach ($visibilitySettings as $setting) {