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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user