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) => (