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
@@ -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>