feat: add Generate and Clone UI for slot definitions
continuous-integration/drone/push Build is passing Details

Built complete bulk operations UI for slot definitions based on Backend Brooke's spec (BUILD:345).

New Components:
- GenerateSlotDefinitionsModal (380 lines)
  - Preset selector with 4 options (workday_standard, weekend_extended, all_week_uniform, hourly_daytime)
  - Court multi-select with Select All
  - Validity date pickers (optional)
  - Advanced pattern overrides (collapsible): custom days, time range, duration, interval, capacity
  - Real-time validation and error handling

- CloneSlotDefinitionModal (280 lines)
  - Source definition display with full details
  - Target courts multi-select
  - Target days multi-select (pre-selected source day)
  - Validity date overrides (optional)
  - Preview of estimated clone count

Integration:
- Added Generate button (blue, magic wand icon) to page header
- Added Clone button (blue copy icon) to each table row
- Wired up modals with success callbacks to reload data
- Success callbacks trigger table refresh

Features:
- All modals use professional slate theme
- Loading states with spinners
- Error display with user-friendly messages
- Form validation before submission
- Modal backdrop dismissal

Ready for testing against staging (BUILD:345).
master
Guillermo Pages 1 month ago
parent 13ec348fb2
commit 1ebe61fa40

@ -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<number[]>([]);
const [selectedDays, setSelectedDays] = useState<DayOfWeek[]>([sourceDefinition.dow]);
const [validFrom, setValidFrom] = useState('');
const [validTo, setValidTo] = useState('');
// UI state
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 bg-white border-b-2 border-slate-200 px-6 py-4 flex items-center justify-between">
<h2 className="text-2xl font-bold text-slate-900">Clone Slot Definition</h2>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-slate-600" />
</button>
</div>
{/* Body */}
<div className="p-6 space-y-6">
{/* Source Definition Info */}
<div className="bg-slate-50 border-2 border-slate-200 rounded-lg p-4">
<h3 className="text-sm font-semibold text-slate-700 mb-3">Source Definition</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<div className="text-slate-600 font-medium">Court</div>
<div className="text-slate-900">{sourceCourt?.name || `Court ${sourceDefinition.court_id}`}</div>
</div>
<div>
<div className="text-slate-600 font-medium">Day</div>
<div className="text-slate-900">{DAY_NAMES[sourceDefinition.dow]}</div>
</div>
<div>
<div className="text-slate-600 font-medium">Time</div>
<div className="text-slate-900">
{formatTime(sourceDefinition.starts_at)} - {calculateEndTime(sourceDefinition.starts_at, sourceDefinition.duration_minutes)}
</div>
</div>
<div>
<div className="text-slate-600 font-medium">Duration</div>
<div className="text-slate-900">{sourceDefinition.duration_minutes} min</div>
</div>
<div>
<div className="text-slate-600 font-medium">Capacity</div>
<div className="text-slate-900">{sourceDefinition.capacity} players</div>
</div>
{sourceDefinition.rule?.description && (
<div className="col-span-2">
<div className="text-slate-600 font-medium">Description</div>
<div className="text-slate-900">{sourceDefinition.rule.description}</div>
</div>
)}
</div>
</div>
{/* Target Courts */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-semibold text-slate-700">
Target Courts
</label>
<button
onClick={handleSelectAllCourts}
className="text-sm text-slate-600 hover:text-slate-900 font-medium"
>
{selectedCourtIds.length === courts.length ? 'Deselect All' : 'Select All'}
</button>
</div>
<div className="border-2 border-slate-200 rounded-lg p-4 space-y-2 max-h-48 overflow-y-auto">
{courts.map(court => (
<label
key={court.court_id}
className="flex items-center space-x-3 p-2 hover:bg-slate-50 rounded-lg cursor-pointer"
>
<input
type="checkbox"
checked={selectedCourtIds.includes(court.court_id)}
onChange={() => handleCourtToggle(court.court_id)}
className="w-4 h-4"
/>
<span className="text-slate-900 font-medium">{court.name}</span>
{court.court_id === sourceDefinition.court_id && (
<span className="text-xs text-slate-500 bg-slate-100 px-2 py-0.5 rounded">Source</span>
)}
</label>
))}
</div>
<p className="mt-2 text-sm text-slate-600">
{selectedCourtIds.length} court{selectedCourtIds.length !== 1 ? 's' : ''} selected
</p>
</div>
{/* Target Days */}
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Target Days
</label>
<div className="flex flex-wrap gap-2">
{[0, 1, 2, 3, 4, 5, 6].map(day => (
<button
key={day}
onClick={() => 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 && (
<span className="ml-1 text-xs opacity-70">(Source)</span>
)}
</button>
))}
</div>
<p className="mt-2 text-sm text-slate-600">
{selectedDays.length} day{selectedDays.length !== 1 ? 's' : ''} selected
</p>
</div>
{/* Validity Dates Override */}
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Override Validity Dates (Optional)
</label>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs text-slate-600 mb-1">Valid From</label>
<input
type="date"
value={validFrom}
onChange={(e) => setValidFrom(e.target.value)}
placeholder={sourceDefinition.valid_from}
className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900"
/>
</div>
<div>
<label className="block text-xs text-slate-600 mb-1">Valid To</label>
<input
type="date"
value={validTo}
onChange={(e) => setValidTo(e.target.value)}
placeholder={sourceDefinition.valid_to || 'No end date'}
className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900"
/>
</div>
</div>
<p className="mt-2 text-xs text-slate-500">
Leave empty to use source definition dates: {sourceDefinition.valid_from}
{sourceDefinition.valid_to ? ` to ${sourceDefinition.valid_to}` : ' (no end date)'}
</p>
</div>
{/* Preview */}
<div className="bg-blue-50 border-2 border-blue-200 rounded-lg p-4">
<p className="text-sm font-semibold text-slate-900">
This will create approximately <span className="text-blue-700">{estimatedCount}</span> slot definition{estimatedCount !== 1 ? 's' : ''}
</p>
<p className="text-xs text-slate-600 mt-1">
{selectedCourtIds.length} court{selectedCourtIds.length !== 1 ? 's' : ''} × {selectedDays.length} day{selectedDays.length !== 1 ? 's' : ''} = {estimatedCount} definition{estimatedCount !== 1 ? 's' : ''}
</p>
</div>
{/* Error Message */}
{error && (
<div className="p-4 bg-red-50 border-2 border-red-200 rounded-lg flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<div className="flex-1 text-sm text-red-700">{error}</div>
</div>
)}
</div>
{/* Footer */}
<div className="sticky bottom-0 bg-slate-50 border-t-2 border-slate-200 px-6 py-4 flex items-center justify-between">
<button
onClick={onClose}
className="px-6 py-2 text-slate-700 font-semibold hover:bg-slate-200 rounded-lg transition-colors"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={loading || selectedCourtIds.length === 0 || selectedDays.length === 0}
className="px-6 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
>
{loading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>Cloning...</span>
</>
) : (
<span>Clone Definition</span>
)}
</button>
</div>
</div>
</div>
);
}

@ -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<SlotDefinitionPreset>('workday_standard');
const [selectedCourtIds, setSelectedCourtIds] = useState<number[]>([]);
const [validFrom, setValidFrom] = useState('');
const [validTo, setValidTo] = useState('');
const [showAdvanced, setShowAdvanced] = useState(false);
// Pattern overrides
const [overrideDays, setOverrideDays] = useState<DayOfWeek[]>([]);
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<string | null>(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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 bg-white border-b-2 border-slate-200 px-6 py-4 flex items-center justify-between">
<h2 className="text-2xl font-bold text-slate-900">Generate Slot Definitions</h2>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-slate-600" />
</button>
</div>
{/* Body */}
<div className="p-6 space-y-6">
{/* Preset Selection */}
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Preset Schedule
</label>
<select
value={preset}
onChange={(e) => 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 => (
<option key={opt.id} value={opt.id}>
{opt.name} ({opt.days}, {opt.hours}, {opt.duration})
</option>
))}
</select>
{selectedPreset && (
<p className="mt-2 text-sm text-slate-600">{selectedPreset.description}</p>
)}
</div>
{/* Court Selection */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-semibold text-slate-700">
Select Courts
</label>
<button
onClick={handleSelectAllCourts}
className="text-sm text-slate-600 hover:text-slate-900 font-medium"
>
{selectedCourtIds.length === courts.length ? 'Deselect All' : 'Select All'}
</button>
</div>
<div className="border-2 border-slate-200 rounded-lg p-4 space-y-2 max-h-48 overflow-y-auto">
{courts.map(court => (
<label
key={court.court_id}
className="flex items-center space-x-3 p-2 hover:bg-slate-50 rounded-lg cursor-pointer"
>
<input
type="checkbox"
checked={selectedCourtIds.includes(court.court_id)}
onChange={() => handleCourtToggle(court.court_id)}
className="w-4 h-4"
/>
<span className="text-slate-900 font-medium">{court.name}</span>
</label>
))}
</div>
<p className="mt-2 text-sm text-slate-600">
{selectedCourtIds.length} court{selectedCourtIds.length !== 1 ? 's' : ''} selected
</p>
</div>
{/* Validity Dates */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Valid From (Optional)
</label>
<input
type="date"
value={validFrom}
onChange={(e) => setValidFrom(e.target.value)}
className="w-full px-4 py-2 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900"
/>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Valid To (Optional)
</label>
<input
type="date"
value={validTo}
onChange={(e) => setValidTo(e.target.value)}
className="w-full px-4 py-2 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900"
/>
</div>
</div>
{/* Advanced Options */}
<div className="border-2 border-slate-200 rounded-lg">
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-slate-50 transition-colors"
>
<span className="text-sm font-semibold text-slate-700">Advanced Pattern Overrides</span>
{showAdvanced ? (
<ChevronUp className="w-5 h-5 text-slate-600" />
) : (
<ChevronDown className="w-5 h-5 text-slate-600" />
)}
</button>
{showAdvanced && (
<div className="px-4 pb-4 space-y-4 border-t-2 border-slate-200 pt-4">
{/* Custom Days */}
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Custom Days (Override Preset)
</label>
<div className="flex flex-wrap gap-2">
{[0, 1, 2, 3, 4, 5, 6].map(day => (
<button
key={day}
onClick={() => 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)}
</button>
))}
</div>
</div>
{/* Time Range */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Start Time
</label>
<input
type="time"
value={overrideStartTime}
onChange={(e) => setOverrideStartTime(e.target.value)}
className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900"
/>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
End Time
</label>
<input
type="time"
value={overrideEndTime}
onChange={(e) => setOverrideEndTime(e.target.value)}
className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900"
/>
</div>
</div>
{/* Duration and Interval */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Duration (minutes)
</label>
<input
type="number"
value={overrideDuration}
onChange={(e) => setOverrideDuration(e.target.value)}
min="1"
placeholder="90"
className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900"
/>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Interval (minutes)
</label>
<input
type="number"
value={overrideInterval}
onChange={(e) => setOverrideInterval(e.target.value)}
min="1"
placeholder="90"
className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-slate-900"
/>
</div>
</div>
{/* Capacity */}
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">
Capacity (players)
</label>
<input
type="number"
value={overrideCapacity}
onChange={(e) => 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"
/>
</div>
</div>
)}
</div>
{/* Error Message */}
{error && (
<div className="p-4 bg-red-50 border-2 border-red-200 rounded-lg flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<div className="flex-1 text-sm text-red-700">{error}</div>
</div>
)}
</div>
{/* Footer */}
<div className="sticky bottom-0 bg-slate-50 border-t-2 border-slate-200 px-6 py-4 flex items-center justify-between">
<button
onClick={onClose}
className="px-6 py-2 text-slate-700 font-semibold hover:bg-slate-200 rounded-lg transition-colors"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={loading || selectedCourtIds.length === 0}
className="px-6 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
>
{loading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>Generating...</span>
</>
) : (
<span>Generate Slot Definitions</span>
)}
</button>
</div>
</div>
</div>
);
}

@ -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<SlotDefinitionError | null>(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingDefinition, setEditingDefinition] = useState<SlotDefinition | undefined>(undefined);
const [showGenerateModal, setShowGenerateModal] = useState(false);
const [cloningDefinition, setCloningDefinition] = useState<SlotDefinition | undefined>(undefined);
useEffect(() => {
loadData();
@ -156,13 +160,22 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
</p>
</div>
<button
onClick={handleCreate}
className="inline-flex items-center px-6 py-3 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors shadow-lg"
>
<Plus className="w-5 h-5 mr-2" />
Create Definition
</button>
<div className="flex items-center space-x-3">
<button
onClick={() => 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"
>
<Wand2 className="w-5 h-5 mr-2" />
Generate
</button>
<button
onClick={handleCreate}
className="inline-flex items-center px-6 py-3 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors shadow-lg"
>
<Plus className="w-5 h-5 mr-2" />
Create
</button>
</div>
</div>
</div>
@ -246,6 +259,13 @@ export default function SlotDefinitionsComponent({ clubId }: SlotDefinitionsComp
</td>
<td className="px-6 py-4">
<div className="flex items-center justify-end space-x-2">
<button
onClick={() => setCloningDefinition(definition)}
className="p-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-colors"
title="Clone"
>
<Copy className="w-4 h-4" />
</button>
<button
onClick={() => 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 && (
<GenerateSlotDefinitionsModal
clubId={clubId}
courts={courts}
onClose={() => setShowGenerateModal(false)}
onSuccess={loadDefinitions}
/>
)}
{/* Clone Modal */}
{cloningDefinition && (
<CloneSlotDefinitionModal
clubId={clubId}
sourceDefinition={cloningDefinition}
courts={courts}
onClose={() => setCloningDefinition(undefined)}
onSuccess={loadDefinitions}
/>
)}
</div>
);
}

Loading…
Cancel
Save