From f6bd813c07b4e1ce78f896fa5eab3d30dd0a3224 Mon Sep 17 00:00:00 2001 From: iamkiddy Date: Tue, 25 Nov 2025 21:04:22 +0000 Subject: [PATCH] Refactor Booking component to manage time slots instead of time ranges. Update localStorage handling for time slots, enhance UI for selecting time slots, and improve validation for selected slots. Integrate public availability fetching in BookNowPage to display available time slots based on admin settings. --- app/(admin)/admin/booking/page.tsx | 245 +++++++++++------------------ app/(pages)/book-now/page.tsx | 122 ++++++++++++-- hooks/useAppointments.ts | 5 +- lib/actions/appointments.ts | 48 +++++- 4 files changed, 249 insertions(+), 171 deletions(-) diff --git a/app/(admin)/admin/booking/page.tsx b/app/(admin)/admin/booking/page.tsx index 89eab3d..12c68af 100644 --- a/app/(admin)/admin/booking/page.tsx +++ b/app/(admin)/admin/booking/page.tsx @@ -53,7 +53,7 @@ export default function Booking() { const { adminAvailability, isLoadingAdminAvailability, updateAvailability, isUpdatingAvailability, refetchAdminAvailability } = useAppointments(); const [selectedDays, setSelectedDays] = useState([]); const [availabilityDialogOpen, setAvailabilityDialogOpen] = useState(false); - const [dayTimeRanges, setDayTimeRanges] = useState>({}); + const [dayTimeSlots, setDayTimeSlots] = useState>({}); const daysOfWeek = [ { value: 0, label: "Monday" }, @@ -65,64 +65,56 @@ export default function Booking() { { value: 6, label: "Sunday" }, ]; - // Load time ranges from localStorage on mount + // Load time slots from localStorage on mount useEffect(() => { - const savedTimeRanges = localStorage.getItem("adminAvailabilityTimeRanges"); - if (savedTimeRanges) { + const savedTimeSlots = localStorage.getItem("adminAvailabilityTimeSlots"); + if (savedTimeSlots) { try { - const parsed = JSON.parse(savedTimeRanges); - setDayTimeRanges(parsed); + const parsed = JSON.parse(savedTimeSlots); + setDayTimeSlots(parsed); } catch (error) { - console.error("Failed to parse saved time ranges:", error); + console.error("Failed to parse saved time slots:", error); } } }, []); - // Initialize selected days and time ranges when availability is loaded + // Initialize selected days and time slots when availability is loaded useEffect(() => { if (adminAvailability?.available_days) { setSelectedDays(adminAvailability.available_days); - // Load saved time ranges or use defaults - const savedTimeRanges = localStorage.getItem("adminAvailabilityTimeRanges"); - let initialRanges: Record = {}; + // Load saved time slots or use defaults + const savedTimeSlots = localStorage.getItem("adminAvailabilityTimeSlots"); + let initialSlots: Record = {}; - if (savedTimeRanges) { + if (savedTimeSlots) { try { - const parsed = JSON.parse(savedTimeRanges); - // Only use saved ranges for days that are currently available + const parsed = JSON.parse(savedTimeSlots); + // Only use saved slots for days that are currently available adminAvailability.available_days.forEach((day) => { - initialRanges[day] = parsed[day] || { startTime: "09:00", endTime: "17:00" }; + initialSlots[day] = parsed[day] || ["morning", "lunchtime", "afternoon"]; }); } catch (error) { // If parsing fails, use defaults adminAvailability.available_days.forEach((day) => { - initialRanges[day] = { startTime: "09:00", endTime: "17:00" }; + initialSlots[day] = ["morning", "lunchtime", "afternoon"]; }); } } else { - // No saved ranges, use defaults + // No saved slots, use defaults adminAvailability.available_days.forEach((day) => { - initialRanges[day] = { startTime: "09:00", endTime: "17:00" }; + initialSlots[day] = ["morning", "lunchtime", "afternoon"]; }); } - setDayTimeRanges(initialRanges); + setDayTimeSlots(initialSlots); } }, [adminAvailability]); - // Generate time slots for time picker - const generateTimeSlots = () => { - const slots = []; - for (let hour = 0; hour < 24; hour++) { - for (let minute = 0; minute < 60; minute += 30) { - const timeString = `${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}`; - slots.push(timeString); - } - } - return slots; - }; - - const timeSlotsForPicker = generateTimeSlots(); + const timeSlotOptions = [ + { value: "morning", label: "Morning" }, + { value: "lunchtime", label: "Lunchtime" }, + { value: "afternoon", label: "Evening" }, + ]; const handleDayToggle = (day: number) => { setSelectedDays((prev) => { @@ -130,20 +122,20 @@ export default function Booking() { ? prev.filter((d) => d !== day) : [...prev, day].sort(); - // Initialize time range for newly added day - if (!prev.includes(day) && !dayTimeRanges[day]) { - setDayTimeRanges((prevRanges) => ({ - ...prevRanges, - [day]: { startTime: "09:00", endTime: "17:00" }, + // Initialize time slots for newly added day + if (!prev.includes(day) && !dayTimeSlots[day]) { + setDayTimeSlots((prevSlots) => ({ + ...prevSlots, + [day]: ["morning", "lunchtime", "afternoon"], })); } - // Remove time range for removed day + // Remove time slots for removed day if (prev.includes(day)) { - setDayTimeRanges((prevRanges) => { - const newRanges = { ...prevRanges }; - delete newRanges[day]; - return newRanges; + setDayTimeSlots((prevSlots) => { + const newSlots = { ...prevSlots }; + delete newSlots[day]; + return newSlots; }); } @@ -151,14 +143,18 @@ export default function Booking() { }); }; - const handleTimeRangeChange = (day: number, field: "startTime" | "endTime", value: string) => { - setDayTimeRanges((prev) => ({ - ...prev, - [day]: { - ...prev[day], - [field]: value, - }, - })); + const handleTimeSlotToggle = (day: number, slot: string) => { + setDayTimeSlots((prev) => { + const currentSlots = prev[day] || []; + const newSlots = currentSlots.includes(slot) + ? currentSlots.filter((s) => s !== slot) + : [...currentSlots, slot]; + + return { + ...prev, + [day]: newSlots, + }; + }); }; const handleSaveAvailability = async () => { @@ -167,15 +163,11 @@ export default function Booking() { return; } - // Validate all time ranges + // Validate all time slots for (const day of selectedDays) { - const timeRange = dayTimeRanges[day]; - if (!timeRange || !timeRange.startTime || !timeRange.endTime) { - toast.error(`Please set time range for ${daysOfWeek.find(d => d.value === day)?.label}`); - return; - } - if (timeRange.startTime >= timeRange.endTime) { - toast.error(`End time must be after start time for ${daysOfWeek.find(d => d.value === day)?.label}`); + const timeSlots = dayTimeSlots[day]; + if (!timeSlots || timeSlots.length === 0) { + toast.error(`Please select at least one time slot for ${daysOfWeek.find(d => d.value === day)?.label}`); return; } } @@ -185,8 +177,8 @@ export default function Booking() { const daysToSave = selectedDays.map(day => Number(day)).sort(); await updateAvailability({ available_days: daysToSave }); - // Save time ranges to localStorage - localStorage.setItem("adminAvailabilityTimeRanges", JSON.stringify(dayTimeRanges)); + // Save time slots to localStorage + localStorage.setItem("adminAvailabilityTimeSlots", JSON.stringify(dayTimeSlots)); toast.success("Availability updated successfully!"); // Refresh availability data @@ -204,12 +196,12 @@ export default function Booking() { const handleOpenAvailabilityDialog = () => { if (adminAvailability?.available_days) { setSelectedDays(adminAvailability.available_days); - // Initialize time ranges for each day - const initialRanges: Record = {}; + // Initialize time slots for each day + const initialSlots: Record = {}; adminAvailability.available_days.forEach((day) => { - initialRanges[day] = dayTimeRanges[day] || { startTime: "09:00", endTime: "17:00" }; + initialSlots[day] = dayTimeSlots[day] || ["morning", "lunchtime", "afternoon"]; }); - setDayTimeRanges(initialRanges); + setDayTimeSlots(initialSlots); } setAvailabilityDialogOpen(true); }; @@ -441,7 +433,11 @@ export default function Booking() {
{adminAvailability.available_days.map((dayNum, index) => { const dayName = daysOfWeek.find(d => d.value === dayNum)?.label || adminAvailability.available_days_display[index]; - const timeRange = dayTimeRanges[dayNum] || { startTime: "09:00", endTime: "17:00" }; + const timeSlots = dayTimeSlots[dayNum] || []; + const slotLabels = timeSlots.map(slot => { + const option = timeSlotOptions.find(opt => opt.value === slot); + return option ? option.label : slot; + }); return (
{dayName} - - ({new Date(`2000-01-01T${timeRange.startTime}`).toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - })}{" "} - -{" "} - {new Date(`2000-01-01T${timeRange.endTime}`).toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - })}) - + {slotLabels.length > 0 && ( + + ({slotLabels.join(", ")}) + + )}
); })} @@ -835,14 +823,13 @@ export default function Booking() { Available Days & Times *

- Select days and set time ranges for each day + Select days and choose time slots (Morning, Lunchtime, Evening) for each day

{daysOfWeek.map((day) => { const isSelected = selectedDays.includes(day.value); - const timeRange = dayTimeRanges[day.value] || { startTime: "09:00", endTime: "17:00" }; return (
-
- {/* Start Time */} -
- - -
- - {/* End Time */} -
- - -
-
- - {/* Time Range Preview */} -
- {new Date(`2000-01-01T${timeRange.startTime}`).toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - })}{" "} - -{" "} - {new Date(`2000-01-01T${timeRange.endTime}`).toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, + +
+ {timeSlotOptions.map((slot) => { + const isSelectedSlot = dayTimeSlots[day.value]?.includes(slot.value) || false; + return ( + + ); })}
diff --git a/app/(pages)/book-now/page.tsx b/app/(pages)/book-now/page.tsx index 3ab1a2c..f2a7967 100644 --- a/app/(pages)/book-now/page.tsx +++ b/app/(pages)/book-now/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { useAppTheme } from "@/components/ThemeProvider"; import { Input } from "@/components/ui/input"; @@ -24,6 +24,7 @@ import { CheckCircle, Loader2, LogOut, + CalendarCheck, } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; @@ -34,6 +35,8 @@ import { useAuth } from "@/hooks/useAuth"; import { useAppointments } from "@/hooks/useAppointments"; import { toast } from "sonner"; import type { Appointment } from "@/lib/models/appointments"; +import { getPublicAvailability } from "@/lib/actions/appointments"; +import type { AdminAvailability } from "@/lib/models/appointments"; interface User { ID: number; @@ -80,7 +83,7 @@ export default function BookNowPage() { const { theme } = useAppTheme(); const isDark = theme === "dark"; const { isAuthenticated, logout } = useAuth(); - const { create, isCreating } = useAppointments(); + const { create, isCreating, availableDates, availableDatesResponse, isLoadingAvailableDates } = useAppointments(); const [formData, setFormData] = useState({ firstName: "", lastName: "", @@ -95,6 +98,68 @@ export default function BookNowPage() { const [showLoginDialog, setShowLoginDialog] = useState(false); const [showSignupDialog, setShowSignupDialog] = useState(false); const [loginPrefillEmail, setLoginPrefillEmail] = useState(undefined); + const [publicAvailability, setPublicAvailability] = useState(null); + const [availableTimeSlots, setAvailableTimeSlots] = useState>({}); + + // Fetch public availability to get time slots + useEffect(() => { + const fetchAvailability = async () => { + try { + const availability = await getPublicAvailability(); + if (availability) { + setPublicAvailability(availability); + // Try to get time slots from localStorage (if admin has set them) + // Note: This won't work for public users, but we can try + const savedTimeSlots = localStorage.getItem("adminAvailabilityTimeSlots"); + if (savedTimeSlots) { + try { + const parsed = JSON.parse(savedTimeSlots); + setAvailableTimeSlots(parsed); + } catch (e) { + console.error("Failed to parse time slots:", e); + } + } + } + } catch (error) { + console.error("Failed to fetch public availability:", error); + } + }; + fetchAvailability(); + }, []); + + // Use available_days_display from API if available, otherwise extract from dates + const availableDaysOfWeek = useMemo(() => { + // If API provides available_days_display, use it directly + if (availableDatesResponse?.available_days_display && availableDatesResponse.available_days_display.length > 0) { + return availableDatesResponse.available_days_display; + } + + // Otherwise, extract from dates + if (!availableDates || availableDates.length === 0) { + return []; + } + + const daysSet = new Set(); + const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + + availableDates.forEach((dateStr) => { + try { + // Parse date string (YYYY-MM-DD format) + const [year, month, day] = dateStr.split('-').map(Number); + const date = new Date(year, month - 1, day); + if (!isNaN(date.getTime())) { + const dayIndex = date.getDay(); + daysSet.add(dayNames[dayIndex]); + } + } catch (e) { + console.error('Invalid date:', dateStr, e); + } + }); + + // Return in weekday order (Monday first) + const weekdayOrder = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + return weekdayOrder.filter(day => daysSet.has(day)); + }, [availableDates, availableDatesResponse]); const handleLogout = () => { logout(); @@ -566,7 +631,7 @@ export default function BookNowPage() { Appointment Details -
+
-
- {['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'].map((day) => ( + {isLoadingAvailableDates ? ( +
+ + Loading available days... +
+ ) : availableDaysOfWeek.length === 0 ? ( +

+ No available days at the moment. Please check back later. +

+ ) : ( + <> +
+ {availableDaysOfWeek.map((day) => ( ))} -
+
+ + )}
@@ -608,11 +686,33 @@ export default function BookNowPage() { Preferred Time *
- {[ - { value: 'morning', label: 'Morning' }, - { value: 'lunchtime', label: 'Lunchtime' }, - { value: 'afternoon', label: 'Afternoon' } - ].map((time) => ( + {(() => { + // Get available time slots based on selected days + const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + const dayIndices = formData.preferredDays.map(day => dayNames.indexOf(day)); + + // Get all unique time slots from selected days + let allAvailableSlots = new Set(); + dayIndices.forEach(dayIndex => { + if (dayIndex !== -1 && availableTimeSlots[dayIndex]) { + availableTimeSlots[dayIndex].forEach(slot => allAvailableSlots.add(slot)); + } + }); + + // If no time slots found in localStorage, show all (fallback) + const slotsToShow = allAvailableSlots.size > 0 + ? Array.from(allAvailableSlots) + : ['morning', 'lunchtime', 'afternoon']; + + const timeSlotMap = [ + { value: 'morning', label: 'Morning' }, + { value: 'lunchtime', label: 'Lunchtime' }, + { value: 'afternoon', label: 'Evening' } + ]; + + // Only show time slots that are available + return timeSlotMap.filter(ts => slotsToShow.includes(ts.value)); + })().map((time) => (