From 88d0d00e29f6b3205c3cfcfd1161e1126b4c3e78 Mon Sep 17 00:00:00 2001 From: Kanishk Sachdev Date: Wed, 22 Oct 2025 01:34:04 -0400 Subject: [PATCH] Enhance reservation system to track room capacity and occupancy levels --- src/components/ReservationSystem/index.tsx | 141 ++++++++++++++++----- src/lib/api/location/entity.ts | 1 + 2 files changed, 107 insertions(+), 35 deletions(-) diff --git a/src/components/ReservationSystem/index.tsx b/src/components/ReservationSystem/index.tsx index 8fb1bc9d..deb15573 100644 --- a/src/components/ReservationSystem/index.tsx +++ b/src/components/ReservationSystem/index.tsx @@ -265,32 +265,55 @@ const ReservationSystem: React.FC = () => { })); }, [locations]); - // Build availability map (normalize incoming reservation times) + // Build availability map with capacity tracking (normalize incoming reservation times) const availability = useMemo(() => { const availabilityMap: { - [key: string]: { available: boolean; reservationId?: string }; + [key: string]: { + available: boolean; + reservationId?: string; + currentOccupancy: number; + maxCapacity: number; + }; } = {}; if (!reservations || !rooms) return availabilityMap; - // Initialize all slots as available + // Initialize all slots with capacity information rooms.forEach((room) => { timeSlots.forEach((time) => { - availabilityMap[`${room.id}-${time}`] = { available: true }; + availabilityMap[`${room.id}-${time}`] = { + available: true, + currentOccupancy: 0, + maxCapacity: room.capacity, + }; }); }); - // Mark reserved slots + // Count participant reservations per slot reservations.forEach((reservation) => { + // Only count participant reservations toward capacity + if (reservation.reservationType !== 'participant') return; + const startLabel = timestampToTime(reservation.startTime); const key = `${reservation.locationId}-${startLabel}`; + if (!availabilityMap[key]) return; + const isUserTeamReservation = reservation.teamId === userTeam?.id; - availabilityMap[key] = { - available: false, - reservationId: isUserTeamReservation ? reservation.id : undefined, - }; + // Increment occupancy count + availabilityMap[key].currentOccupancy += 1; + + // Store user's own reservation ID + if (isUserTeamReservation) { + availabilityMap[key].reservationId = reservation.id; + } + + // Mark as unavailable if at capacity (capacity > 0 means limited, 0 means unlimited) + const room = rooms.find(r => r.id === reservation.locationId); + if (room && room.capacity > 0 && availabilityMap[key].currentOccupancy >= room.capacity) { + availabilityMap[key].available = false; + } }); return availabilityMap; @@ -299,7 +322,12 @@ const ReservationSystem: React.FC = () => { const handleSlotClick = async ( roomId: number, time: string, - slotInfo: { available: boolean; reservationId?: string } + slotInfo: { + available: boolean; + reservationId?: string; + currentOccupancy: number; + maxCapacity: number; + } ) => { if (!userTeam) { toast.error("You must be part of a team to make reservations"); @@ -314,9 +342,10 @@ const ReservationSystem: React.FC = () => { return; } - // If slot is not available, do nothing + // If slot is not available (at capacity), show detailed message if (!slotInfo.available) { - toast.error("This time slot is not available"); + const capacityText = slotInfo.maxCapacity === 0 ? 'unlimited' : slotInfo.maxCapacity; + toast.error(`This time slot is at full capacity (${slotInfo.currentOccupancy}/${capacityText})`); return; } @@ -579,7 +608,11 @@ const ReservationSystem: React.FC = () => {
- Available + Available (Low occupancy) +
+
+
+ Filling up (75%+)
@@ -587,7 +620,7 @@ const ReservationSystem: React.FC = () => {
- Unavailable + Full
@@ -633,19 +666,24 @@ const ReservationSystem: React.FC = () => { {/* Room Name Cell */}
- + {room.name} + + {room.capacity === 0 ? 'Unlimited' : `Cap: ${room.capacity}`} +
{/* Time Slot Cells */} {timeSlots.map((time) => { const slotInfo = availability[`${room.id}-${time}`] || { available: true, + currentOccupancy: 0, + maxCapacity: room.capacity, }; const isSelected = selectedSlots?.roomId === room.id && @@ -654,31 +692,64 @@ const ReservationSystem: React.FC = () => { slotInfo.reservationId ); + // Calculate fill percentage for visual indicator + const fillPercentage = slotInfo.maxCapacity > 0 + ? (slotInfo.currentOccupancy / slotInfo.maxCapacity) * 100 + : 0; + + // Determine background color based on occupancy + let bgColor = "bg-green-400 hover:bg-green-500 hover:ring-2 hover:ring-green-600 hover:ring-inset"; + if (isUserReservation) { + bgColor = "bg-purple-400 hover:bg-purple-500"; + } else if (isSelected) { + bgColor = "bg-blue-500 hover:bg-blue-600 ring-2 ring-blue-700 ring-inset"; + } else if (!slotInfo.available) { + bgColor = "bg-gray-300 cursor-not-allowed hover:bg-gray-300"; + } else if (slotInfo.maxCapacity > 0) { + // Show color gradient based on occupancy + if (fillPercentage >= 75) { + bgColor = "bg-yellow-300 hover:bg-yellow-400 hover:ring-2 hover:ring-yellow-500 hover:ring-inset"; + } else if (fillPercentage >= 50) { + bgColor = "bg-green-300 hover:bg-green-400 hover:ring-2 hover:ring-green-500 hover:ring-inset"; + } + } + + // Build tooltip text + let tooltipText = ""; + if (isUserReservation) { + tooltipText = `Your reservation at ${room.name} - Click to cancel`; + } else if (slotInfo.available) { + const capacityText = slotInfo.maxCapacity === 0 + ? 'Unlimited capacity' + : `${slotInfo.currentOccupancy}/${slotInfo.maxCapacity} spots filled`; + if (isSelected) { + tooltipText = `Deselect ${time} (${capacityText})`; + } else { + tooltipText = `Select ${time} at ${room.name} (${capacityText})`; + } + } else { + const capacityText = slotInfo.maxCapacity === 0 ? 'unlimited' : slotInfo.maxCapacity; + tooltipText = `Full capacity (${slotInfo.currentOccupancy}/${capacityText})`; + } + return (
handleSlotClick(room.id, time, slotInfo) } - title={ - isUserReservation - ? `Your reservation at ${room.name} - Click to cancel` - : slotInfo.available - ? isSelected - ? `Deselect ${time}` - : `Select ${time} at ${room.name}` - : "Not available" - } - /> + title={tooltipText} + > + {/* Show occupancy indicator for non-empty slots */} + {!isUserReservation && !isSelected && slotInfo.currentOccupancy > 0 && slotInfo.maxCapacity > 0 && ( +
+ + {slotInfo.currentOccupancy}/{slotInfo.maxCapacity} + +
+ )} +
); })}
diff --git a/src/lib/api/location/entity.ts b/src/lib/api/location/entity.ts index f294d968..8625a67e 100644 --- a/src/lib/api/location/entity.ts +++ b/src/lib/api/location/entity.ts @@ -1,6 +1,7 @@ export interface LocationEntity { id: number; name: string; + capacity: number; } export interface LocationCreateEntity extends Omit {}