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:
@@ -91,6 +91,61 @@
|
||||
@error('status')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
|
||||
{{-- Wiederholung (nur Training) --}}
|
||||
<div class="mb-4" x-data="recurrenceData()" x-show="isTraining" x-cloak>
|
||||
<div class="border border-gray-200 rounded-md p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.recurrence') }}</h3>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.recurrence') }}</label>
|
||||
<select x-model="recurrence" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm">
|
||||
<option value="none">{{ __('admin.recurrence_none') }}</option>
|
||||
<option value="daily">{{ __('admin.recurrence_daily') }}</option>
|
||||
<option value="weekly">{{ __('admin.recurrence_weekly') }}</option>
|
||||
<option value="biweekly">{{ __('admin.recurrence_biweekly') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div x-show="recurrence !== 'none'" x-cloak>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.recurrence_end_type') }}</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-1.5 text-sm text-gray-700 cursor-pointer">
|
||||
<input type="radio" x-model="endType" value="date" class="text-blue-600">
|
||||
{{ __('admin.recurrence_end_date') }}
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-sm text-gray-700 cursor-pointer">
|
||||
<input type="radio" x-model="endType" value="count" class="text-blue-600">
|
||||
{{ __('admin.recurrence_end_count') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="endType === 'date'" class="mb-3">
|
||||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.recurrence_end_date') }}</label>
|
||||
<input type="date" x-model="endDate" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
|
||||
<div x-show="endType === 'count'" class="mb-3">
|
||||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.recurrence_count_label') }}</label>
|
||||
<input type="number" x-model.number="count" min="1" max="52" class="w-24 px-3 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
|
||||
<div x-show="previewCount > 0" class="text-sm text-blue-600 bg-blue-50 rounded-md px-3 py-2">
|
||||
<span x-text="previewText"></span>
|
||||
</div>
|
||||
<div x-show="previewCount >= 52" class="text-xs text-orange-600 mt-1">
|
||||
{{ __('admin.recurrence_max_warning', ['max' => 52]) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="recurrence" :value="recurrence">
|
||||
<input type="hidden" name="recurrence_end_type" :value="endType">
|
||||
<input type="hidden" name="recurrence_end_date" :value="endDate">
|
||||
<input type="hidden" name="recurrence_count" :value="count">
|
||||
</div>
|
||||
|
||||
{{-- Mindestanforderungen --}}
|
||||
<div class="mb-4" x-data="minRequirementsData()" x-init="listenTypeChange()">
|
||||
<div x-show="showDropdowns" x-cloak class="border border-gray-200 rounded-md p-4">
|
||||
@@ -105,6 +160,7 @@
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
@if (\App\Models\Setting::isFeatureEnabled('catering'))
|
||||
<div x-show="showCatering">
|
||||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.min_catering') }}</label>
|
||||
<select name="min_catering" x-model="minCatering" class="w-full px-2 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
@@ -114,6 +170,8 @@
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
@if (\App\Models\Setting::isFeatureEnabled('timekeepers'))
|
||||
<div x-show="showTimekeepers">
|
||||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.min_timekeepers') }}</label>
|
||||
<select name="min_timekeepers" x-model="minTimekeepers" class="w-full px-2 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
@@ -123,6 +181,7 @@
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,6 +243,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Catering/Zeitnehmer-Zuweisungen (nicht für away_game/meeting) --}}
|
||||
@if (\App\Models\Setting::isFeatureEnabled('catering') || \App\Models\Setting::isFeatureEnabled('timekeepers'))
|
||||
<div class="mb-4" x-data="assignmentData()" x-init="listenTeamChange()"
|
||||
x-show="!['away_game', 'meeting'].includes(currentType)" x-cloak>
|
||||
<template x-if="parents.length > 0">
|
||||
@@ -193,24 +253,32 @@
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200">
|
||||
<th class="text-left py-2 font-medium text-gray-600">{{ __('ui.name') }}</th>
|
||||
@if (\App\Models\Setting::isFeatureEnabled('catering'))
|
||||
<th class="text-center py-2 font-medium text-gray-600">{{ __('admin.catering_assignment') }}</th>
|
||||
@endif
|
||||
@if (\App\Models\Setting::isFeatureEnabled('timekeepers'))
|
||||
<th class="text-center py-2 font-medium text-gray-600">{{ __('admin.timekeeper_assignment') }}</th>
|
||||
@endif
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="p in parents" :key="p.id">
|
||||
<tr class="border-b border-gray-100">
|
||||
<td class="py-2" x-text="p.name"></td>
|
||||
@if (\App\Models\Setting::isFeatureEnabled('catering'))
|
||||
<td class="py-2 text-center">
|
||||
<input type="checkbox" name="catering_users[]" :value="p.id"
|
||||
:checked="assignedCatering.includes(p.id)"
|
||||
class="rounded border-gray-300 text-blue-600">
|
||||
</td>
|
||||
@endif
|
||||
@if (\App\Models\Setting::isFeatureEnabled('timekeepers'))
|
||||
<td class="py-2 text-center">
|
||||
<input type="checkbox" name="timekeeper_users[]" :value="p.id"
|
||||
:checked="assignedTimekeeper.includes(p.id)"
|
||||
class="rounded border-gray-300 text-blue-600">
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
@@ -218,6 +286,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Dateien --}}
|
||||
<div class="mb-4" x-data="{ showPicker: false, newFileCount: 0 }">
|
||||
@@ -294,6 +363,41 @@
|
||||
document.getElementById('description_html').value = quill.root.innerHTML;
|
||||
});
|
||||
|
||||
// Recurrence data for Training events
|
||||
function recurrenceData() {
|
||||
const typeSel = document.getElementById('type');
|
||||
const dateSel = document.getElementById('start_date');
|
||||
const previewTpl = @js(__('admin.recurrence_preview', ['count' => '__COUNT__']));
|
||||
return {
|
||||
recurrence: @js(old('recurrence', 'none')),
|
||||
endType: @js(old('recurrence_end_type', 'date')),
|
||||
endDate: @js(old('recurrence_end_date', '')),
|
||||
count: @js((int) old('recurrence_count', 10)),
|
||||
isTraining: typeSel.value === 'training',
|
||||
init() {
|
||||
typeSel.addEventListener('change', () => {
|
||||
this.isTraining = typeSel.value === 'training';
|
||||
if (!this.isTraining) this.recurrence = 'none';
|
||||
});
|
||||
},
|
||||
get startDate() { return dateSel.value; },
|
||||
get previewCount() {
|
||||
if (this.recurrence === 'none') return 0;
|
||||
if (this.endType === 'count') return Math.min(Math.max(1, this.count || 0), 52);
|
||||
if (!this.startDate || !this.endDate) return 0;
|
||||
const days = { daily: 1, weekly: 7, biweekly: 14 };
|
||||
const start = new Date(this.startDate);
|
||||
const end = new Date(this.endDate);
|
||||
const diff = Math.floor((end - start) / (1000 * 60 * 60 * 24));
|
||||
if (diff <= 0) return 0;
|
||||
return Math.min(Math.floor(diff / days[this.recurrence]), 52);
|
||||
},
|
||||
get previewText() {
|
||||
return previewTpl.replace('__COUNT__', this.previewCount);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Assignment data for Catering/Timekeeper
|
||||
function assignmentData() {
|
||||
const teamParents = @js($teamParents);
|
||||
|
||||
Reference in New Issue
Block a user