feat: implement admin blocks management feature

- Added AdminBlocksManagement component for managing court blocks.
- Implemented functionality to create, edit, and delete blocks.
- Integrated fetching of courts and blocks from the API.
- Added validation for block creation and editing forms.
- Enhanced UI with responsive design for mobile and desktop views.
- Created database migration for court_blocks table and updated users table with theme_preference.
This commit is contained in:
mikicvi
2025-12-29 17:04:16 +00:00
parent 54240a2cfd
commit 40c56770a2
13 changed files with 2164 additions and 215 deletions
+753
View File
@@ -0,0 +1,753 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Badge } from '@/components/ui/badge';
import { Calendar, Trash2, Plus, Ban, CalendarX, Edit, Bell } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
interface Court {
id: string;
name: string;
}
interface CourtBlock {
id: string;
courtId: string | null;
courtName: string | null;
date: string;
startTime: string;
endTime: string;
reason: string;
createdBy: string;
creatorName: string;
createdAt: string;
}
export function AdminBlocksManagement() {
const { toast } = useToast();
const [blocks, setBlocks] = useState<CourtBlock[]>([]);
const [courts, setCourts] = useState<Court[]>([]);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
// Form state for creating
const [selectedCourt, setSelectedCourt] = useState<string>('all');
const [blockDate, setBlockDate] = useState<string>('');
const [startTime, setStartTime] = useState<string>('18:00');
const [endTime, setEndTime] = useState<string>('23:00');
const [reason, setReason] = useState<string>('');
const [createAnnouncement, setCreateAnnouncement] = useState<boolean>(true);
// Edit modal state
const [editingBlock, setEditingBlock] = useState<CourtBlock | null>(null);
const [editCourt, setEditCourt] = useState<string>('all');
const [editDate, setEditDate] = useState<string>('');
const [editStartTime, setEditStartTime] = useState<string>('');
const [editEndTime, setEditEndTime] = useState<string>('');
const [editReason, setEditReason] = useState<string>('');
// Generate time options (e.g., 06:00 to 23:00)
const timeOptions = [];
for (let h = 6; h <= 23; h++) {
timeOptions.push(`${String(h).padStart(2, '0')}:00`);
}
const fetchBlocks = useCallback(async () => {
try {
const response = await fetch('/api/admin/blocks');
if (response.ok) {
const data = await response.json();
setBlocks(data.blocks || []);
} else {
toast({
title: 'Error',
description: 'Failed to fetch blocks',
variant: 'destructive',
});
}
} catch (error) {
console.error('Error fetching blocks:', error);
toast({
title: 'Error',
description: 'Failed to fetch blocks',
variant: 'destructive',
});
} finally {
setLoading(false);
}
}, [toast]);
const fetchCourts = useCallback(async () => {
try {
const response = await fetch('/api/courts');
if (response.ok) {
const data = await response.json();
setCourts(data.courts || []);
}
} catch (error) {
console.error('Error fetching courts:', error);
}
}, []);
useEffect(() => {
fetchBlocks();
fetchCourts();
}, [fetchBlocks, fetchCourts]);
const handleCreateBlock = async (e: React.FormEvent) => {
e.preventDefault();
if (!blockDate) {
toast({
title: 'Validation Error',
description: 'Please select a date',
variant: 'destructive',
});
return;
}
if (!reason.trim()) {
toast({
title: 'Validation Error',
description: 'Please provide a reason for the block',
variant: 'destructive',
});
return;
}
if (startTime >= endTime) {
toast({
title: 'Validation Error',
description: 'End time must be after start time',
variant: 'destructive',
});
return;
}
setSubmitting(true);
try {
const response = await fetch('/api/admin/blocks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
courtId: selectedCourt === 'all' ? null : selectedCourt,
date: blockDate,
startTime,
endTime,
reason: reason.trim(),
createAnnouncement,
}),
});
if (response.ok) {
const data = await response.json();
toast({
title: 'Success',
description: data.announcementCreated
? 'Block created and announcement posted'
: 'Block created successfully',
});
// Reset form
setBlockDate('');
setReason('');
setSelectedCourt('all');
setStartTime('18:00');
setEndTime('22:00');
setCreateAnnouncement(true);
// Refresh blocks list
fetchBlocks();
} else {
const data = await response.json();
toast({
title: 'Error',
description: data.error || 'Failed to create block',
variant: 'destructive',
});
}
} catch (error) {
console.error('Error creating block:', error);
toast({
title: 'Error',
description: 'Failed to create block',
variant: 'destructive',
});
} finally {
setSubmitting(false);
}
};
const handleEditClick = (block: CourtBlock) => {
setEditingBlock(block);
setEditCourt(block.courtId || 'all');
setEditDate(block.date);
setEditStartTime(block.startTime);
setEditEndTime(block.endTime);
setEditReason(block.reason);
};
const handleEditSave = async () => {
if (!editingBlock) return;
if (!editDate || !editReason.trim()) {
toast({
title: 'Validation Error',
description: 'Please fill in all required fields',
variant: 'destructive',
});
return;
}
if (editStartTime >= editEndTime) {
toast({
title: 'Validation Error',
description: 'End time must be after start time',
variant: 'destructive',
});
return;
}
setSubmitting(true);
try {
const response = await fetch(`/api/admin/blocks/${editingBlock.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
courtId: editCourt === 'all' ? null : editCourt,
date: editDate,
startTime: editStartTime,
endTime: editEndTime,
reason: editReason.trim(),
}),
});
if (response.ok) {
toast({
title: 'Success',
description: 'Block updated successfully',
});
setEditingBlock(null);
fetchBlocks();
} else {
const data = await response.json();
toast({
title: 'Error',
description: data.error || 'Failed to update block',
variant: 'destructive',
});
}
} catch (error) {
console.error('Error updating block:', error);
toast({
title: 'Error',
description: 'Failed to update block',
variant: 'destructive',
});
} finally {
setSubmitting(false);
}
};
const handleDeleteBlock = async (blockId: string) => {
try {
const response = await fetch(`/api/admin/blocks/${blockId}`, {
method: 'DELETE',
});
if (response.ok) {
toast({
title: 'Success',
description: 'Block removed successfully',
});
fetchBlocks();
} else {
const data = await response.json();
toast({
title: 'Error',
description: data.error || 'Failed to delete block',
variant: 'destructive',
});
}
} catch (error) {
console.error('Error deleting block:', error);
toast({
title: 'Error',
description: 'Failed to delete block',
variant: 'destructive',
});
}
};
// Get min date (today) and max date (e.g., 12 weeks from now)
const today = new Date();
const minDate = today.toISOString().split('T')[0];
const maxDate = new Date(today.getTime() + 84 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; // 12 weeks
// Format date for display
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString('en-IE', {
weekday: 'short',
day: 'numeric',
month: 'short',
year: 'numeric',
});
};
// Check if a block is in the past
const isBlockPast = (dateStr: string) => {
const blockDate = new Date(dateStr);
blockDate.setHours(23, 59, 59, 999);
return blockDate < new Date();
};
// Sort blocks by date
const sortedBlocks = [...blocks].sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateA.getTime() - dateB.getTime();
});
return (
<div className='space-y-6'>
{/* Create Block Form */}
<Card>
<CardHeader>
<CardTitle className='flex items-center gap-2'>
<CalendarX className='h-5 w-5' />
Block Courts/Hall
</CardTitle>
<CardDescription>
Block court availability for tournaments, AGM, maintenance, or other events. Blocked slots will
be visible to members but cannot be booked.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleCreateBlock} className='space-y-4'>
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'>
{/* Court Selection */}
<div className='space-y-2'>
<Label htmlFor='court'>Court</Label>
<Select value={selectedCourt} onValueChange={setSelectedCourt}>
<SelectTrigger>
<SelectValue placeholder='Select court' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>All Courts (Entire Hall)</SelectItem>
{courts.map((court) => (
<SelectItem key={court.id} value={court.id}>
{court.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Date Selection */}
<div className='space-y-2'>
<Label htmlFor='date'>Date</Label>
<Input
id='date'
type='date'
value={blockDate}
onChange={(e) => setBlockDate(e.target.value)}
min={minDate}
max={maxDate}
required
/>
</div>
{/* Reason */}
<div className='space-y-2 md:col-span-2 lg:col-span-1'>
<Label htmlFor='reason'>Reason</Label>
<Input
id='reason'
type='text'
placeholder='e.g., Tournament, AGM, Maintenance'
value={reason}
onChange={(e) => setReason(e.target.value)}
maxLength={200}
required
/>
</div>
</div>
<div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
{/* Start Time */}
<div className='space-y-2'>
<Label htmlFor='startTime'>Start Time</Label>
<Select value={startTime} onValueChange={setStartTime}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{timeOptions.map((time) => (
<SelectItem key={time} value={time}>
{time}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* End Time */}
<div className='space-y-2'>
<Label htmlFor='endTime'>End Time</Label>
<Select value={endTime} onValueChange={setEndTime}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{timeOptions.map((time) => (
<SelectItem key={time} value={time}>
{time}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Create Announcement Toggle */}
<div className='col-span-2 flex items-center justify-between p-3 bg-muted/50 rounded-lg'>
<div className='flex items-center gap-2'>
<Bell className='h-4 w-4 text-muted-foreground' />
<Label htmlFor='createAnnouncement' className='text-sm cursor-pointer'>
Create announcement
</Label>
</div>
<Switch
id='createAnnouncement'
checked={createAnnouncement}
onCheckedChange={setCreateAnnouncement}
/>
</div>
</div>
{createAnnouncement && (
<p className='text-xs text-muted-foreground'>
An announcement will be created and automatically expire when the block ends.
</p>
)}
{/* Submit Button */}
<Button type='submit' disabled={submitting} className='w-full md:w-auto'>
<Plus className='h-4 w-4 mr-2' />
{submitting ? 'Creating...' : 'Create Block'}
</Button>
</form>
</CardContent>
</Card>
{/* Existing Blocks List */}
<Card>
<CardHeader>
<CardTitle className='flex items-center gap-2'>
<Ban className='h-5 w-5' />
Scheduled Blocks
</CardTitle>
<CardDescription>
Upcoming and current court blocks. Past blocks are shown for reference.
</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className='text-center py-8 text-muted-foreground'>Loading blocks...</div>
) : sortedBlocks.length === 0 ? (
<div className='text-center py-8 text-muted-foreground'>
No blocks scheduled. Create a block above to reserve courts for events.
</div>
) : (
<>
{/* Desktop Table */}
<div className='hidden md:block overflow-x-auto'>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Court</TableHead>
<TableHead>Time</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Created By</TableHead>
<TableHead className='text-right'>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedBlocks.map((block) => (
<TableRow
key={block.id}
className={isBlockPast(block.date) ? 'opacity-50' : ''}
>
<TableCell>
<div className='flex items-center gap-2'>
<Calendar className='h-4 w-4 text-muted-foreground' />
{formatDate(block.date)}
{isBlockPast(block.date) && (
<Badge variant='outline' className='text-xs'>
Past
</Badge>
)}
</div>
</TableCell>
<TableCell>
{block.courtName ? (
<Badge variant='secondary'>{block.courtName}</Badge>
) : (
<Badge variant='destructive'>All Courts</Badge>
)}
</TableCell>
<TableCell>
{block.startTime} - {block.endTime}
</TableCell>
<TableCell className='max-w-[200px] truncate' title={block.reason}>
{block.reason}
</TableCell>
<TableCell>{block.creatorName}</TableCell>
<TableCell className='text-right'>
<div className='flex items-center justify-end gap-1'>
{!isBlockPast(block.date) && (
<Button
variant='ghost'
size='sm'
onClick={() => handleEditClick(block)}
>
<Edit className='h-4 w-4' />
</Button>
)}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant='ghost'
size='sm'
className='text-destructive hover:text-destructive'
>
<Trash2 className='h-4 w-4' />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Remove Block?</AlertDialogTitle>
<AlertDialogDescription>
This will remove the block for{' '}
{formatDate(block.date)} ({block.reason}).
Members will be able to book this time slot
again.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDeleteBlock(block.id)}
>
Remove Block
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Mobile Cards */}
<div className='md:hidden space-y-4'>
{sortedBlocks.map((block) => (
<Card key={block.id} className={isBlockPast(block.date) ? 'opacity-50' : ''}>
<CardContent className='pt-4'>
<div className='flex justify-between items-start mb-2'>
<div className='flex items-center gap-2'>
<Calendar className='h-4 w-4 text-muted-foreground' />
<span className='font-medium'>{formatDate(block.date)}</span>
{isBlockPast(block.date) && (
<Badge variant='outline' className='text-xs'>
Past
</Badge>
)}
</div>
<div className='flex items-center gap-1'>
{!isBlockPast(block.date) && (
<Button
variant='ghost'
size='sm'
onClick={() => handleEditClick(block)}
>
<Edit className='h-4 w-4' />
</Button>
)}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant='ghost'
size='sm'
className='text-destructive hover:text-destructive'
>
<Trash2 className='h-4 w-4' />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Remove Block?</AlertDialogTitle>
<AlertDialogDescription>
This will remove the block for{' '}
{formatDate(block.date)} ({block.reason}).
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDeleteBlock(block.id)}
>
Remove Block
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className='space-y-1 text-sm'>
<div className='flex items-center gap-2'>
<span className='text-muted-foreground'>Court:</span>
{block.courtName ? (
<Badge variant='secondary'>{block.courtName}</Badge>
) : (
<Badge variant='destructive'>All Courts</Badge>
)}
</div>
<div>
<span className='text-muted-foreground'>Time: </span>
{block.startTime} - {block.endTime}
</div>
<div>
<span className='text-muted-foreground'>Reason: </span>
{block.reason}
</div>
<div className='text-xs text-muted-foreground'>
Created by {block.creatorName}
</div>
</div>
</CardContent>
</Card>
))}
</div>
</>
)}
</CardContent>
</Card>
{/* Edit Block Dialog */}
<Dialog open={!!editingBlock} onOpenChange={(open) => !open && setEditingBlock(null)}>
<DialogContent className='sm:max-w-[425px]'>
<DialogHeader>
<DialogTitle>Edit Block</DialogTitle>
<DialogDescription>Modify the court closure details.</DialogDescription>
</DialogHeader>
<div className='grid gap-4 py-4'>
<div className='grid gap-2'>
<Label htmlFor='edit-court'>Court</Label>
<Select value={editCourt || 'all'} onValueChange={setEditCourt}>
<SelectTrigger>
<SelectValue placeholder='Select a court' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>All Courts</SelectItem>
{courts.map((court) => (
<SelectItem key={court.id} value={court.id.toString()}>
{court.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='grid gap-2'>
<Label htmlFor='edit-date'>Date</Label>
<Input
id='edit-date'
type='date'
value={editDate}
onChange={(e) => setEditDate(e.target.value)}
min={new Date().toISOString().split('T')[0]}
/>
</div>
<div className='grid grid-cols-2 gap-4'>
<div className='grid gap-2'>
<Label htmlFor='edit-start-time'>Start Time</Label>
<Select value={editStartTime} onValueChange={setEditStartTime}>
<SelectTrigger>
<SelectValue placeholder='Start time' />
</SelectTrigger>
<SelectContent>
{timeOptions.map((time) => (
<SelectItem key={time} value={time}>
{time}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='grid gap-2'>
<Label htmlFor='edit-end-time'>End Time</Label>
<Select value={editEndTime} onValueChange={setEditEndTime}>
<SelectTrigger>
<SelectValue placeholder='End time' />
</SelectTrigger>
<SelectContent>
{timeOptions.map((time) => (
<SelectItem key={time} value={time}>
{time}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className='grid gap-2'>
<Label htmlFor='edit-reason'>Reason</Label>
<Input
id='edit-reason'
type='text'
value={editReason}
onChange={(e) => setEditReason(e.target.value)}
placeholder='e.g., Tournament, Maintenance'
/>
</div>
</div>
<DialogFooter>
<Button variant='outline' onClick={() => setEditingBlock(null)}>
Cancel
</Button>
<Button onClick={handleEditSave} disabled={submitting}>
{submitting ? 'Saving...' : 'Save Changes'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
+170 -144
View File
@@ -19,9 +19,10 @@ import {
} from '@/components/ui/alert-dialog';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { toast } from '@/hooks/use-toast';
import { Plus, Edit, Trash2, MapPin, Settings, RefreshCw } from 'lucide-react';
import { Plus, Edit, Trash2, MapPin, Settings, RefreshCw, Ban } from 'lucide-react';
import { AdminBlocksManagement } from './AdminBlocksManagement';
interface Court {
id: string;
@@ -219,7 +220,7 @@ export function AdminCourtManagement() {
return (
<Card>
<CardHeader>
<CardTitle>Court Management</CardTitle>
<CardTitle>Courts & Closures</CardTitle>
</CardHeader>
<CardContent>
<div className='space-y-4'>
@@ -236,151 +237,176 @@ export function AdminCourtManagement() {
}
return (
<Card>
<CardHeader className='flex flex-row items-center justify-between'>
<CardTitle className='flex items-center gap-2'>
<Settings className='h-5 w-5' />
Court Management
</CardTitle>
<div className='flex gap-2'>
<Button size='sm' variant='outline' onClick={fetchCourts}>
<RefreshCw className='h-4 w-4 mr-2' />
Refresh
</Button>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button size='sm' onClick={() => setEditingCourt(null)}>
<Plus className='h-4 w-4 mr-2' />
Add Court
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingCourt ? 'Edit Court' : 'Create New Court'}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className='space-y-4'>
<div>
<Label htmlFor='name'>Court Name</Label>
<Input
id='name'
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder='e.g., Court 1, Main Court'
required
/>
</div>
<div className='space-y-6'>
<Tabs defaultValue='courts' className='w-full'>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='courts' className='flex items-center gap-2'>
<MapPin className='h-4 w-4' />
Courts
</TabsTrigger>
<TabsTrigger value='closures' className='flex items-center gap-2'>
<Ban className='h-4 w-4' />
Closures
</TabsTrigger>
</TabsList>
<div className='flex items-center space-x-2'>
<Switch
id='isActive'
checked={formData.isActive}
onCheckedChange={(checked: boolean) =>
setFormData({ ...formData, isActive: checked })
}
/>
<Label htmlFor='isActive'>Active (available for booking)</Label>
</div>
<TabsContent value='courts'>
<Card>
<CardHeader className='flex flex-row items-center justify-between'>
<CardTitle className='flex items-center gap-2'>
<Settings className='h-5 w-5' />
Court Management
</CardTitle>
<div className='flex gap-2'>
<Button size='sm' variant='outline' onClick={fetchCourts}>
<RefreshCw className='h-4 w-4 mr-2' />
Refresh
</Button>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button size='sm' onClick={() => setEditingCourt(null)}>
<Plus className='h-4 w-4 mr-2' />
Add Court
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingCourt ? 'Edit Court' : 'Create New Court'}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className='space-y-4'>
<div>
<Label htmlFor='name'>Court Name</Label>
<Input
id='name'
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder='e.g., Court 1, Main Court'
required
/>
</div>
<div className='flex justify-end space-x-2'>
<Button type='button' variant='outline' onClick={resetForm}>
Cancel
</Button>
<Button type='submit' disabled={creating || Boolean(editing)}>
{creating || editing ? (
<>
<RefreshCw className='h-4 w-4 mr-2 animate-spin' />
{editingCourt ? 'Updating...' : 'Creating...'}
</>
) : editingCourt ? (
'Update Court'
) : (
'Create Court'
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
{courts.length === 0 ? (
<div className='text-center py-8 text-gray-500'>
<MapPin className='h-12 w-12 mx-auto mb-4 text-gray-300' />
<p>No courts found. Create your first court to get started.</p>
</div>
) : (
<div className='space-y-4'>
{courts.map((court) => (
<div key={court.id} className='border rounded-lg p-4'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<MapPin className='h-5 w-5 text-blue-600' />
<div>
<h3 className='font-medium'>{court.name}</h3>
<p className='text-sm text-gray-500'>
Created {new Date(court.createdAt).toLocaleDateString('en-IE')}
</p>
</div>
</div>
<div className='flex items-center space-x-2'>
<Switch
id='isActive'
checked={formData.isActive}
onCheckedChange={(checked: boolean) =>
setFormData({ ...formData, isActive: checked })
}
/>
<Label htmlFor='isActive'>Active (available for booking)</Label>
</div>
<div className='flex items-center gap-3'>
<Badge variant={court.isActive ? 'default' : 'secondary'}>
{court.isActive ? 'Active' : 'Inactive'}
</Badge>
<div className='flex gap-1'>
<Button
size='sm'
variant='outline'
onClick={() => handleEdit(court)}
disabled={editing === court.id}
>
<Edit className='h-4 w-4' />
</Button>
<Button
size='sm'
variant='outline'
onClick={() => openDeleteDialog(court)}
disabled={deleting === court.id}
className='text-red-600 hover:text-red-700'
>
{deleting === court.id ? (
<RefreshCw className='h-4 w-4 animate-spin' />
) : (
<Trash2 className='h-4 w-4' />
)}
</Button>
</div>
</div>
</div>
<div className='flex justify-end space-x-2'>
<Button type='button' variant='outline' onClick={resetForm}>
Cancel
</Button>
<Button type='submit' disabled={creating || Boolean(editing)}>
{creating || editing ? (
<>
<RefreshCw className='h-4 w-4 mr-2 animate-spin' />
{editingCourt ? 'Updating...' : 'Creating...'}
</>
) : editingCourt ? (
'Update Court'
) : (
'Create Court'
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
))}
</div>
)}
</CardContent>
</CardHeader>
<CardContent>
{courts.length === 0 ? (
<div className='text-center py-8 text-gray-500'>
<MapPin className='h-12 w-12 mx-auto mb-4 text-gray-300' />
<p>No courts found. Create your first court to get started.</p>
</div>
) : (
<div className='space-y-4'>
{courts.map((court) => (
<div key={court.id} className='border rounded-lg p-4'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<MapPin className='h-5 w-5 text-blue-600' />
<div>
<h3 className='font-medium'>{court.name}</h3>
<p className='text-sm text-gray-500'>
Created{' '}
{new Date(court.createdAt).toLocaleDateString('en-IE')}
</p>
</div>
</div>
{/* Delete Confirmation Dialog */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {courtToDelete ? `"${courtToDelete.name}"` : 'this court'}?
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteCourt}
className='bg-destructive hover:bg-destructive/90'
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card>
<div className='flex items-center gap-3'>
<Badge variant={court.isActive ? 'default' : 'secondary'}>
{court.isActive ? 'Active' : 'Inactive'}
</Badge>
<div className='flex gap-1'>
<Button
size='sm'
variant='outline'
onClick={() => handleEdit(court)}
disabled={editing === court.id}
>
<Edit className='h-4 w-4' />
</Button>
<Button
size='sm'
variant='outline'
onClick={() => openDeleteDialog(court)}
disabled={deleting === court.id}
className='text-red-600 hover:text-red-700'
>
{deleting === court.id ? (
<RefreshCw className='h-4 w-4 animate-spin' />
) : (
<Trash2 className='h-4 w-4' />
)}
</Button>
</div>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
{/* Delete Confirmation Dialog */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete{' '}
{courtToDelete ? `"${courtToDelete.name}"` : 'this court'}? This action cannot
be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteCourt}
className='bg-destructive hover:bg-destructive/90'
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card>
</TabsContent>
<TabsContent value='closures'>
<AdminBlocksManagement />
</TabsContent>
</Tabs>
</div>
);
}
+1 -1
View File
@@ -274,7 +274,7 @@ export function AdminDashboard() {
</TabsContent>
<TabsContent value='courts'>
<AdminCourtManagement />
</TabsContent>{' '}
</TabsContent>
<TabsContent value='settings'>
<div className='space-y-6'>
<AdminSettingsManagement />