diff --git a/src/app/[locale]/admin/clubs/[club_id]/slot-definitions/CloneSlotDefinitionModal.tsx b/src/app/[locale]/admin/clubs/[club_id]/slot-definitions/CloneSlotDefinitionModal.tsx
new file mode 100644
index 0000000..3705551
--- /dev/null
+++ b/src/app/[locale]/admin/clubs/[club_id]/slot-definitions/CloneSlotDefinitionModal.tsx
@@ -0,0 +1,294 @@
+'use client';
+
+import { useState } from 'react';
+import { X, Loader2, AlertCircle } from 'lucide-react';
+import { cloneSlotDefinition } from '@/src/lib/api/slot-definitions';
+import type { Court } from '@/src/types/courts';
+import type {
+ SlotDefinition,
+ CloneSlotDefinitionRequest,
+ DayOfWeek,
+} from '@/src/types/slot-definitions';
+import { DAY_NAMES, formatTime, calculateEndTime } from '@/src/types/slot-definitions';
+
+interface CloneSlotDefinitionModalProps {
+ clubId: number;
+ sourceDefinition: SlotDefinition;
+ courts: Court[];
+ onClose: () => void;
+ onSuccess: () => void;
+}
+
+export default function CloneSlotDefinitionModal({
+ clubId,
+ sourceDefinition,
+ courts,
+ onClose,
+ onSuccess,
+}: CloneSlotDefinitionModalProps) {
+ // Form state
+ const [selectedCourtIds, setSelectedCourtIds] = useState([]);
+ const [selectedDays, setSelectedDays] = useState([sourceDefinition.dow]);
+ const [validFrom, setValidFrom] = useState('');
+ const [validTo, setValidTo] = useState('');
+
+ // UI state
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ function handleSelectAllCourts() {
+ if (selectedCourtIds.length === courts.length) {
+ setSelectedCourtIds([]);
+ } else {
+ setSelectedCourtIds(courts.map(c => c.court_id));
+ }
+ }
+
+ function handleCourtToggle(courtId: number) {
+ setSelectedCourtIds(prev =>
+ prev.includes(courtId)
+ ? prev.filter(id => id !== courtId)
+ : [...prev, courtId]
+ );
+ }
+
+ function handleDayToggle(day: DayOfWeek) {
+ setSelectedDays(prev =>
+ prev.includes(day)
+ ? prev.filter(d => d !== day)
+ : [...prev, day]
+ );
+ }
+
+ async function handleSubmit() {
+ if (selectedCourtIds.length === 0) {
+ setError('Please select at least one target court');
+ return;
+ }
+
+ if (selectedDays.length === 0) {
+ setError('Please select at least one target day');
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ const request: CloneSlotDefinitionRequest = {
+ target_court_ids: selectedCourtIds,
+ target_days: selectedDays,
+ };
+
+ if (validFrom) request.valid_from = validFrom;
+ if (validTo) request.valid_to = validTo;
+
+ const result = await cloneSlotDefinition(clubId, sourceDefinition.slot_definition_id, request);
+
+ if (result.success) {
+ onSuccess();
+ onClose();
+ } else {
+ setError(result.error.detail);
+ }
+
+ setLoading(false);
+ }
+
+ const sourceCourt = courts.find(c => c.court_id === sourceDefinition.court_id);
+ const estimatedCount = selectedCourtIds.length * selectedDays.length;
+
+ return (
+
+
+ {/* Header */}
+
+
Clone Slot Definition
+
+
+
+
+
+ {/* Body */}
+
+ {/* Source Definition Info */}
+
+
Source Definition
+
+
+
Court
+
{sourceCourt?.name || `Court ${sourceDefinition.court_id}`}
+
+
+
Day
+
{DAY_NAMES[sourceDefinition.dow]}
+
+
+
Time
+
+ {formatTime(sourceDefinition.starts_at)} - {calculateEndTime(sourceDefinition.starts_at, sourceDefinition.duration_minutes)}
+
+
+
+
Duration
+
{sourceDefinition.duration_minutes} min
+
+
+
Capacity
+
{sourceDefinition.capacity} players
+
+ {sourceDefinition.rule?.description && (
+
+
Description
+
{sourceDefinition.rule.description}
+
+ )}
+
+
+
+ {/* Target Courts */}
+
+
+
+ Target Courts
+
+
+ {selectedCourtIds.length === courts.length ? 'Deselect All' : 'Select All'}
+
+
+
+ {courts.map(court => (
+
+ handleCourtToggle(court.court_id)}
+ className="w-4 h-4"
+ />
+ {court.name}
+ {court.court_id === sourceDefinition.court_id && (
+ Source
+ )}
+
+ ))}
+
+
+ {selectedCourtIds.length} court{selectedCourtIds.length !== 1 ? 's' : ''} selected
+
+
+
+ {/* Target Days */}
+
+
+ Target Days
+
+
+ {[0, 1, 2, 3, 4, 5, 6].map(day => (
+ handleDayToggle(day as DayOfWeek)}
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
+ selectedDays.includes(day as DayOfWeek)
+ ? 'bg-slate-900 text-white'
+ : 'bg-slate-100 text-slate-700 hover:bg-slate-200'
+ }`}
+ >
+ {DAY_NAMES[day as DayOfWeek]}
+ {day === sourceDefinition.dow && (
+ (Source)
+ )}
+
+ ))}
+
+
+ {selectedDays.length} day{selectedDays.length !== 1 ? 's' : ''} selected
+
+
+
+ {/* Validity Dates Override */}
+
+
+ Override Validity Dates (Optional)
+
+
+
+ Leave empty to use source definition dates: {sourceDefinition.valid_from}
+ {sourceDefinition.valid_to ? ` to ${sourceDefinition.valid_to}` : ' (no end date)'}
+
+
+
+ {/* Preview */}
+
+
+ This will create approximately {estimatedCount} slot definition{estimatedCount !== 1 ? 's' : ''}
+
+
+ {selectedCourtIds.length} court{selectedCourtIds.length !== 1 ? 's' : ''} × {selectedDays.length} day{selectedDays.length !== 1 ? 's' : ''} = {estimatedCount} definition{estimatedCount !== 1 ? 's' : ''}
+
+
+
+ {/* Error Message */}
+ {error && (
+
+ )}
+
+
+ {/* Footer */}
+
+
+ Cancel
+
+
+ {loading ? (
+ <>
+
+ Cloning...
+ >
+ ) : (
+ Clone Definition
+ )}
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/admin/clubs/[club_id]/slot-definitions/GenerateSlotDefinitionsModal.tsx b/src/app/[locale]/admin/clubs/[club_id]/slot-definitions/GenerateSlotDefinitionsModal.tsx
new file mode 100644
index 0000000..3a63142
--- /dev/null
+++ b/src/app/[locale]/admin/clubs/[club_id]/slot-definitions/GenerateSlotDefinitionsModal.tsx
@@ -0,0 +1,362 @@
+'use client';
+
+import { useState } from 'react';
+import { X, Loader2, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react';
+import { generateSlotDefinitions } from '@/src/lib/api/slot-definitions';
+import type { Court } from '@/src/types/courts';
+import type {
+ SlotDefinitionPreset,
+ GenerateSlotDefinitionsRequest,
+ PatternOverrides,
+ DayOfWeek,
+} from '@/src/types/slot-definitions';
+import { PRESET_OPTIONS, DAY_NAMES } from '@/src/types/slot-definitions';
+
+interface GenerateSlotDefinitionsModalProps {
+ clubId: number;
+ courts: Court[];
+ onClose: () => void;
+ onSuccess: () => void;
+}
+
+export default function GenerateSlotDefinitionsModal({
+ clubId,
+ courts,
+ onClose,
+ onSuccess,
+}: GenerateSlotDefinitionsModalProps) {
+ // Form state
+ const [preset, setPreset] = useState('workday_standard');
+ const [selectedCourtIds, setSelectedCourtIds] = useState([]);
+ const [validFrom, setValidFrom] = useState('');
+ const [validTo, setValidTo] = useState('');
+ const [showAdvanced, setShowAdvanced] = useState(false);
+
+ // Pattern overrides
+ const [overrideDays, setOverrideDays] = useState([]);
+ const [overrideStartTime, setOverrideStartTime] = useState('');
+ const [overrideEndTime, setOverrideEndTime] = useState('');
+ const [overrideDuration, setOverrideDuration] = useState('');
+ const [overrideInterval, setOverrideInterval] = useState('');
+ const [overrideCapacity, setOverrideCapacity] = useState('');
+
+ // UI state
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ function handleSelectAllCourts() {
+ if (selectedCourtIds.length === courts.length) {
+ setSelectedCourtIds([]);
+ } else {
+ setSelectedCourtIds(courts.map(c => c.court_id));
+ }
+ }
+
+ function handleCourtToggle(courtId: number) {
+ setSelectedCourtIds(prev =>
+ prev.includes(courtId)
+ ? prev.filter(id => id !== courtId)
+ : [...prev, courtId]
+ );
+ }
+
+ function handleDayToggle(day: DayOfWeek) {
+ setOverrideDays(prev =>
+ prev.includes(day)
+ ? prev.filter(d => d !== day)
+ : [...prev, day]
+ );
+ }
+
+ function buildPatternOverrides(): PatternOverrides | undefined {
+ if (!showAdvanced) return undefined;
+
+ const overrides: PatternOverrides = {};
+
+ if (overrideDays.length > 0) overrides.days = overrideDays;
+ if (overrideStartTime) overrides.start_time = `${overrideStartTime}:00`;
+ if (overrideEndTime) overrides.end_time = `${overrideEndTime}:00`;
+ if (overrideDuration) overrides.duration_minutes = parseInt(overrideDuration, 10);
+ if (overrideInterval) overrides.interval_minutes = parseInt(overrideInterval, 10);
+ if (overrideCapacity) overrides.capacity = parseInt(overrideCapacity, 10);
+
+ return Object.keys(overrides).length > 0 ? overrides : undefined;
+ }
+
+ async function handleSubmit() {
+ if (selectedCourtIds.length === 0) {
+ setError('Please select at least one court');
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ const request: GenerateSlotDefinitionsRequest = {
+ preset,
+ court_ids: selectedCourtIds,
+ pattern_overrides: buildPatternOverrides(),
+ };
+
+ if (validFrom) request.valid_from = validFrom;
+ if (validTo) request.valid_to = validTo;
+
+ const result = await generateSlotDefinitions(clubId, request);
+
+ if (result.success) {
+ onSuccess();
+ onClose();
+ } else {
+ setError(result.error.detail);
+ }
+
+ setLoading(false);
+ }
+
+ const selectedPreset = PRESET_OPTIONS.find(p => p.id === preset);
+
+ return (
+
+
+ {/* Header */}
+
+
Generate Slot Definitions
+
+
+
+
+
+ {/* Body */}
+
+ {/* Preset Selection */}
+
+
+ Preset Schedule
+
+
setPreset(e.target.value as SlotDefinitionPreset)}
+ className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900 font-medium"
+ >
+ {PRESET_OPTIONS.map(opt => (
+
+ {opt.name} ({opt.days}, {opt.hours}, {opt.duration})
+
+ ))}
+
+ {selectedPreset && (
+
{selectedPreset.description}
+ )}
+
+
+ {/* Court Selection */}
+
+
+
+ Select Courts
+
+
+ {selectedCourtIds.length === courts.length ? 'Deselect All' : 'Select All'}
+
+
+
+ {courts.map(court => (
+
+ handleCourtToggle(court.court_id)}
+ className="w-4 h-4"
+ />
+ {court.name}
+
+ ))}
+
+
+ {selectedCourtIds.length} court{selectedCourtIds.length !== 1 ? 's' : ''} selected
+
+
+
+ {/* Validity Dates */}
+
+
+ {/* Advanced Options */}
+
+
setShowAdvanced(!showAdvanced)}
+ className="w-full px-4 py-3 flex items-center justify-between hover:bg-slate-50 transition-colors"
+ >
+ Advanced Pattern Overrides
+ {showAdvanced ? (
+
+ ) : (
+
+ )}
+
+
+ {showAdvanced && (
+
+ {/* Custom Days */}
+
+
+ Custom Days (Override Preset)
+
+
+ {[0, 1, 2, 3, 4, 5, 6].map(day => (
+ handleDayToggle(day as DayOfWeek)}
+ className={`px-3 py-1 rounded-lg text-sm font-medium transition-colors ${
+ overrideDays.includes(day as DayOfWeek)
+ ? 'bg-slate-900 text-white'
+ : 'bg-slate-100 text-slate-700 hover:bg-slate-200'
+ }`}
+ >
+ {DAY_NAMES[day as DayOfWeek].slice(0, 3)}
+
+ ))}
+
+
+
+ {/* Time Range */}
+
+
+ {/* Duration and Interval */}
+
+
+ {/* Capacity */}
+
+
+ Capacity (players)
+
+ setOverrideCapacity(e.target.value)}
+ min="1"
+ placeholder="4"
+ className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900"
+ />
+
+
+ )}
+
+
+ {/* Error Message */}
+ {error && (
+
+ )}
+
+
+ {/* Footer */}
+
+
+ Cancel
+
+
+ {loading ? (
+ <>
+
+ Generating...
+ >
+ ) : (
+ Generate Slot Definitions
+ )}
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/admin/clubs/[club_id]/slot-definitions/SlotDefinitionsComponent.tsx b/src/app/[locale]/admin/clubs/[club_id]/slot-definitions/SlotDefinitionsComponent.tsx
index a06cfa3..89623d7 100644
--- a/src/app/[locale]/admin/clubs/[club_id]/slot-definitions/SlotDefinitionsComponent.tsx
+++ b/src/app/[locale]/admin/clubs/[club_id]/slot-definitions/SlotDefinitionsComponent.tsx
@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
-import { Calendar, Plus, Loader2, AlertCircle, Edit, Trash2, ArrowLeft } from 'lucide-react';
+import { Calendar, Plus, Loader2, AlertCircle, Edit, Trash2, ArrowLeft, Wand2, Copy } from 'lucide-react';
import Link from 'next/link';
import useTranslation from '@/src/hooks/useTranslation';
import { getSlotDefinitions, deleteSlotDefinition } from '@/src/lib/api/slot-definitions';
@@ -11,6 +11,8 @@ import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
import type { Court } from '@/src/types/courts';
import SlotDefinitionForm from './SlotDefinitionForm';
import MaterialisationStatusPanel from './MaterialisationStatusPanel';
+import GenerateSlotDefinitionsModal from './GenerateSlotDefinitionsModal';
+import CloneSlotDefinitionModal from './CloneSlotDefinitionModal';
interface SlotDefinitionsComponentProps {
clubId: number;
@@ -24,6 +26,8 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
const [error, setError] = useState(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingDefinition, setEditingDefinition] = useState(undefined);
+ const [showGenerateModal, setShowGenerateModal] = useState(false);
+ const [cloningDefinition, setCloningDefinition] = useState(undefined);
useEffect(() => {
loadData();
@@ -156,13 +160,22 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
-
-
- Create Definition
-
+
+
setShowGenerateModal(true)}
+ className="inline-flex items-center px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors shadow-lg"
+ >
+
+ Generate
+
+
+
+ Create
+
+
@@ -246,6 +259,13 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
+ setCloningDefinition(definition)}
+ className="p-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-colors"
+ title="Clone"
+ >
+
+
handleEdit(definition)}
className="p-2 text-slate-600 hover:text-slate-900 hover:bg-slate-100 rounded-lg transition-colors"
@@ -281,6 +301,27 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
onSuccess={handleFormSuccess}
/>
)}
+
+ {/* Generate Modal */}
+ {showGenerateModal && (
+ setShowGenerateModal(false)}
+ onSuccess={loadDefinitions}
+ />
+ )}
+
+ {/* Clone Modal */}
+ {cloningDefinition && (
+ setCloningDefinition(undefined)}
+ onSuccess={loadDefinitions}
+ />
+ )}
);
}