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:
@@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Calendar, Clock, MapPin, ChevronLeft, ChevronRight, Users, User } from 'lucide-react';
|
||||
import { Calendar, Clock, MapPin, ChevronLeft, ChevronRight, Users, User, Ban } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
interface Court {
|
||||
@@ -40,6 +40,8 @@ interface BookingSlot {
|
||||
bookingId?: string;
|
||||
bookedBy?: string;
|
||||
partner?: string;
|
||||
blocked?: boolean;
|
||||
blockReason?: string;
|
||||
}
|
||||
|
||||
interface TimeSlot {
|
||||
@@ -50,6 +52,15 @@ interface TimeSlot {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
interface CourtBlock {
|
||||
id: string;
|
||||
courtId: string | null;
|
||||
date: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface Settings {
|
||||
booking_window_days: string;
|
||||
booking_start_time: string;
|
||||
@@ -63,6 +74,7 @@ export function EnhancedBookingCalendar() {
|
||||
const [bookings, setBookings] = useState<Booking[]>([]);
|
||||
const [bookingSlots, setBookingSlots] = useState<BookingSlot[]>([]);
|
||||
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
|
||||
const [blocks, setBlocks] = useState<CourtBlock[]>([]);
|
||||
const [settings, setSettings] = useState<Settings | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [partnerName, setPartnerName] = useState('');
|
||||
@@ -75,6 +87,7 @@ export function EnhancedBookingCalendar() {
|
||||
fetchSettings();
|
||||
fetchCourts();
|
||||
fetchTimeSlots();
|
||||
fetchBlocks();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -156,6 +169,20 @@ export function EnhancedBookingCalendar() {
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch court blocks (closures)
|
||||
const fetchBlocks = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/blocks');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setBlocks(data.blocks || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching blocks:', error);
|
||||
// If blocks fetch fails, just proceed without block data
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBookings = async () => {
|
||||
try {
|
||||
const dateStr = selectedDate.toISOString().split('T')[0];
|
||||
@@ -236,6 +263,9 @@ export function EnhancedBookingCalendar() {
|
||||
const timeSlots = generateTimeSlots();
|
||||
const slots: BookingSlot[] = [];
|
||||
|
||||
// Get blocks for the selected date
|
||||
const dateBlocks = blocks.filter((block) => block.date === dateStr);
|
||||
|
||||
courts.forEach((court) => {
|
||||
timeSlots.forEach((time) => {
|
||||
const existingBooking = existingBookings.find(
|
||||
@@ -246,6 +276,17 @@ export function EnhancedBookingCalendar() {
|
||||
booking.status === 'active'
|
||||
);
|
||||
|
||||
// Check if this time slot is blocked
|
||||
const slotHour = parseInt(time.split(':')[0]);
|
||||
const blockingBlock = dateBlocks.find((block) => {
|
||||
const blockStartHour = parseInt(block.startTime.split(':')[0]);
|
||||
const blockEndHour = parseInt(block.endTime.split(':')[0]);
|
||||
const isTimeInBlock = slotHour >= blockStartHour && slotHour < blockEndHour;
|
||||
// Block applies if it's for this specific court or for all courts (courtId null/undefined/empty)
|
||||
const appliesToCourt = !block.courtId || block.courtId === court.id;
|
||||
return isTimeInBlock && appliesToCourt;
|
||||
});
|
||||
|
||||
const bookedBy = existingBooking?.user
|
||||
? `${existingBooking.user.name} ${existingBooking.user.surname}`
|
||||
: undefined;
|
||||
@@ -256,10 +297,12 @@ export function EnhancedBookingCalendar() {
|
||||
time,
|
||||
courtId: court.id,
|
||||
courtName: court.name,
|
||||
available: !existingBooking,
|
||||
available: !existingBooking && !blockingBlock,
|
||||
bookingId: existingBooking?.id,
|
||||
bookedBy,
|
||||
partner,
|
||||
blocked: !!blockingBlock,
|
||||
blockReason: blockingBlock?.reason,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -302,6 +345,15 @@ export function EnhancedBookingCalendar() {
|
||||
};
|
||||
|
||||
const handleSlotClick = (slot: BookingSlot) => {
|
||||
if (slot.blocked) {
|
||||
toast({
|
||||
title: 'Slot Blocked',
|
||||
description: slot.blockReason || 'This slot is blocked for an event',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!slot.available) return;
|
||||
|
||||
// Double-check that this day is actually bookable
|
||||
@@ -570,7 +622,9 @@ export function EnhancedBookingCalendar() {
|
||||
<div
|
||||
key={`${slot.courtId}-${slot.time}`}
|
||||
className={`p-3 border rounded-lg transition-all duration-200 ${
|
||||
slot.available
|
||||
slot.blocked
|
||||
? 'border-orange-300 bg-orange-50 cursor-not-allowed dark:border-orange-700 dark:bg-orange-950/50'
|
||||
: slot.available
|
||||
? 'border-green-200 bg-green-50 hover:bg-green-100 hover:shadow-sm cursor-pointer dark:border-green-700 dark:bg-green-950 dark:hover:bg-green-900'
|
||||
: 'border-muted bg-muted/50 cursor-not-allowed opacity-75 hover:opacity-100'
|
||||
}`}
|
||||
@@ -582,39 +636,59 @@ export function EnhancedBookingCalendar() {
|
||||
<MapPin className='h-4 w-4' />
|
||||
{slot.courtName}
|
||||
</div>
|
||||
{!slot.available && slot.bookedBy && (
|
||||
<div className='space-y-1'>
|
||||
<div className='flex items-center gap-2 text-xs text-muted-foreground'>
|
||||
<Users className='h-3 w-3' />
|
||||
Booked by {slot.bookedBy}
|
||||
</div>
|
||||
{slot.partner && (
|
||||
<div className='flex items-center gap-2 text-xs text-orange-600 dark:text-orange-400'>
|
||||
<User className='h-3 w-3' />
|
||||
Playing with: {slot.partner}
|
||||
{slot.blocked && (
|
||||
<div className='flex items-center gap-2 text-xs text-orange-600 dark:text-orange-400'>
|
||||
<Ban className='h-3 w-3' />
|
||||
{slot.blockReason || 'Blocked'}
|
||||
</div>
|
||||
)}
|
||||
{!slot.blocked &&
|
||||
!slot.available &&
|
||||
slot.bookedBy && (
|
||||
<div className='space-y-1'>
|
||||
<div className='flex items-center gap-2 text-xs text-muted-foreground'>
|
||||
<Users className='h-3 w-3' />
|
||||
Booked by {slot.bookedBy}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!slot.available && !slot.bookedBy && (
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
Already booked
|
||||
</div>
|
||||
)}
|
||||
{slot.partner && (
|
||||
<div className='flex items-center gap-2 text-xs text-orange-600 dark:text-orange-400'>
|
||||
<User className='h-3 w-3' />
|
||||
Playing with: {slot.partner}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!slot.blocked &&
|
||||
!slot.available &&
|
||||
!slot.bookedBy && (
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
Already booked
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size='sm'
|
||||
disabled={!slot.available}
|
||||
disabled={!slot.available || slot.blocked}
|
||||
variant={
|
||||
slot.available ? 'default' : 'secondary'
|
||||
slot.blocked
|
||||
? 'outline'
|
||||
: slot.available
|
||||
? 'default'
|
||||
: 'secondary'
|
||||
}
|
||||
className={
|
||||
slot.available
|
||||
slot.blocked
|
||||
? 'border-orange-400 text-orange-600 dark:border-orange-600 dark:text-orange-400 cursor-not-allowed'
|
||||
: slot.available
|
||||
? 'bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600 border-0'
|
||||
: 'opacity-50 cursor-not-allowed'
|
||||
}
|
||||
>
|
||||
{slot.available ? 'Book' : 'Booked'}
|
||||
{slot.blocked
|
||||
? 'Blocked'
|
||||
: slot.available
|
||||
? 'Book'
|
||||
: 'Booked'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user