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.
This commit is contained in:
parent
6bcb6c5414
commit
f6bd813c07
@ -53,7 +53,7 @@ export default function Booking() {
|
|||||||
const { adminAvailability, isLoadingAdminAvailability, updateAvailability, isUpdatingAvailability, refetchAdminAvailability } = useAppointments();
|
const { adminAvailability, isLoadingAdminAvailability, updateAvailability, isUpdatingAvailability, refetchAdminAvailability } = useAppointments();
|
||||||
const [selectedDays, setSelectedDays] = useState<number[]>([]);
|
const [selectedDays, setSelectedDays] = useState<number[]>([]);
|
||||||
const [availabilityDialogOpen, setAvailabilityDialogOpen] = useState(false);
|
const [availabilityDialogOpen, setAvailabilityDialogOpen] = useState(false);
|
||||||
const [dayTimeRanges, setDayTimeRanges] = useState<Record<number, { startTime: string; endTime: string }>>({});
|
const [dayTimeSlots, setDayTimeSlots] = useState<Record<number, string[]>>({});
|
||||||
|
|
||||||
const daysOfWeek = [
|
const daysOfWeek = [
|
||||||
{ value: 0, label: "Monday" },
|
{ value: 0, label: "Monday" },
|
||||||
@ -65,64 +65,56 @@ export default function Booking() {
|
|||||||
{ value: 6, label: "Sunday" },
|
{ value: 6, label: "Sunday" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Load time ranges from localStorage on mount
|
// Load time slots from localStorage on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedTimeRanges = localStorage.getItem("adminAvailabilityTimeRanges");
|
const savedTimeSlots = localStorage.getItem("adminAvailabilityTimeSlots");
|
||||||
if (savedTimeRanges) {
|
if (savedTimeSlots) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(savedTimeRanges);
|
const parsed = JSON.parse(savedTimeSlots);
|
||||||
setDayTimeRanges(parsed);
|
setDayTimeSlots(parsed);
|
||||||
} catch (error) {
|
} 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(() => {
|
useEffect(() => {
|
||||||
if (adminAvailability?.available_days) {
|
if (adminAvailability?.available_days) {
|
||||||
setSelectedDays(adminAvailability.available_days);
|
setSelectedDays(adminAvailability.available_days);
|
||||||
// Load saved time ranges or use defaults
|
// Load saved time slots or use defaults
|
||||||
const savedTimeRanges = localStorage.getItem("adminAvailabilityTimeRanges");
|
const savedTimeSlots = localStorage.getItem("adminAvailabilityTimeSlots");
|
||||||
let initialRanges: Record<number, { startTime: string; endTime: string }> = {};
|
let initialSlots: Record<number, string[]> = {};
|
||||||
|
|
||||||
if (savedTimeRanges) {
|
if (savedTimeSlots) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(savedTimeRanges);
|
const parsed = JSON.parse(savedTimeSlots);
|
||||||
// Only use saved ranges for days that are currently available
|
// Only use saved slots for days that are currently available
|
||||||
adminAvailability.available_days.forEach((day) => {
|
adminAvailability.available_days.forEach((day) => {
|
||||||
initialRanges[day] = parsed[day] || { startTime: "09:00", endTime: "17:00" };
|
initialSlots[day] = parsed[day] || ["morning", "lunchtime", "afternoon"];
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If parsing fails, use defaults
|
// If parsing fails, use defaults
|
||||||
adminAvailability.available_days.forEach((day) => {
|
adminAvailability.available_days.forEach((day) => {
|
||||||
initialRanges[day] = { startTime: "09:00", endTime: "17:00" };
|
initialSlots[day] = ["morning", "lunchtime", "afternoon"];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No saved ranges, use defaults
|
// No saved slots, use defaults
|
||||||
adminAvailability.available_days.forEach((day) => {
|
adminAvailability.available_days.forEach((day) => {
|
||||||
initialRanges[day] = { startTime: "09:00", endTime: "17:00" };
|
initialSlots[day] = ["morning", "lunchtime", "afternoon"];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setDayTimeRanges(initialRanges);
|
setDayTimeSlots(initialSlots);
|
||||||
}
|
}
|
||||||
}, [adminAvailability]);
|
}, [adminAvailability]);
|
||||||
|
|
||||||
// Generate time slots for time picker
|
const timeSlotOptions = [
|
||||||
const generateTimeSlots = () => {
|
{ value: "morning", label: "Morning" },
|
||||||
const slots = [];
|
{ value: "lunchtime", label: "Lunchtime" },
|
||||||
for (let hour = 0; hour < 24; hour++) {
|
{ value: "afternoon", label: "Evening" },
|
||||||
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 handleDayToggle = (day: number) => {
|
const handleDayToggle = (day: number) => {
|
||||||
setSelectedDays((prev) => {
|
setSelectedDays((prev) => {
|
||||||
@ -130,20 +122,20 @@ export default function Booking() {
|
|||||||
? prev.filter((d) => d !== day)
|
? prev.filter((d) => d !== day)
|
||||||
: [...prev, day].sort();
|
: [...prev, day].sort();
|
||||||
|
|
||||||
// Initialize time range for newly added day
|
// Initialize time slots for newly added day
|
||||||
if (!prev.includes(day) && !dayTimeRanges[day]) {
|
if (!prev.includes(day) && !dayTimeSlots[day]) {
|
||||||
setDayTimeRanges((prevRanges) => ({
|
setDayTimeSlots((prevSlots) => ({
|
||||||
...prevRanges,
|
...prevSlots,
|
||||||
[day]: { startTime: "09:00", endTime: "17:00" },
|
[day]: ["morning", "lunchtime", "afternoon"],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove time range for removed day
|
// Remove time slots for removed day
|
||||||
if (prev.includes(day)) {
|
if (prev.includes(day)) {
|
||||||
setDayTimeRanges((prevRanges) => {
|
setDayTimeSlots((prevSlots) => {
|
||||||
const newRanges = { ...prevRanges };
|
const newSlots = { ...prevSlots };
|
||||||
delete newRanges[day];
|
delete newSlots[day];
|
||||||
return newRanges;
|
return newSlots;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,14 +143,18 @@ export default function Booking() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeRangeChange = (day: number, field: "startTime" | "endTime", value: string) => {
|
const handleTimeSlotToggle = (day: number, slot: string) => {
|
||||||
setDayTimeRanges((prev) => ({
|
setDayTimeSlots((prev) => {
|
||||||
...prev,
|
const currentSlots = prev[day] || [];
|
||||||
[day]: {
|
const newSlots = currentSlots.includes(slot)
|
||||||
...prev[day],
|
? currentSlots.filter((s) => s !== slot)
|
||||||
[field]: value,
|
: [...currentSlots, slot];
|
||||||
},
|
|
||||||
}));
|
return {
|
||||||
|
...prev,
|
||||||
|
[day]: newSlots,
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveAvailability = async () => {
|
const handleSaveAvailability = async () => {
|
||||||
@ -167,15 +163,11 @@ export default function Booking() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate all time ranges
|
// Validate all time slots
|
||||||
for (const day of selectedDays) {
|
for (const day of selectedDays) {
|
||||||
const timeRange = dayTimeRanges[day];
|
const timeSlots = dayTimeSlots[day];
|
||||||
if (!timeRange || !timeRange.startTime || !timeRange.endTime) {
|
if (!timeSlots || timeSlots.length === 0) {
|
||||||
toast.error(`Please set time range for ${daysOfWeek.find(d => d.value === day)?.label}`);
|
toast.error(`Please select at least one time slot 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}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,8 +177,8 @@ export default function Booking() {
|
|||||||
const daysToSave = selectedDays.map(day => Number(day)).sort();
|
const daysToSave = selectedDays.map(day => Number(day)).sort();
|
||||||
await updateAvailability({ available_days: daysToSave });
|
await updateAvailability({ available_days: daysToSave });
|
||||||
|
|
||||||
// Save time ranges to localStorage
|
// Save time slots to localStorage
|
||||||
localStorage.setItem("adminAvailabilityTimeRanges", JSON.stringify(dayTimeRanges));
|
localStorage.setItem("adminAvailabilityTimeSlots", JSON.stringify(dayTimeSlots));
|
||||||
|
|
||||||
toast.success("Availability updated successfully!");
|
toast.success("Availability updated successfully!");
|
||||||
// Refresh availability data
|
// Refresh availability data
|
||||||
@ -204,12 +196,12 @@ export default function Booking() {
|
|||||||
const handleOpenAvailabilityDialog = () => {
|
const handleOpenAvailabilityDialog = () => {
|
||||||
if (adminAvailability?.available_days) {
|
if (adminAvailability?.available_days) {
|
||||||
setSelectedDays(adminAvailability.available_days);
|
setSelectedDays(adminAvailability.available_days);
|
||||||
// Initialize time ranges for each day
|
// Initialize time slots for each day
|
||||||
const initialRanges: Record<number, { startTime: string; endTime: string }> = {};
|
const initialSlots: Record<number, string[]> = {};
|
||||||
adminAvailability.available_days.forEach((day) => {
|
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);
|
setAvailabilityDialogOpen(true);
|
||||||
};
|
};
|
||||||
@ -441,7 +433,11 @@ export default function Booking() {
|
|||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{adminAvailability.available_days.map((dayNum, index) => {
|
{adminAvailability.available_days.map((dayNum, index) => {
|
||||||
const dayName = daysOfWeek.find(d => d.value === dayNum)?.label || adminAvailability.available_days_display[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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={dayNum}
|
key={dayNum}
|
||||||
@ -453,19 +449,11 @@ export default function Booking() {
|
|||||||
>
|
>
|
||||||
<Check className={`w-3.5 h-3.5 shrink-0 ${isDark ? "text-rose-400" : "text-rose-600"}`} />
|
<Check className={`w-3.5 h-3.5 shrink-0 ${isDark ? "text-rose-400" : "text-rose-600"}`} />
|
||||||
<span className="font-medium shrink-0">{dayName}</span>
|
<span className="font-medium shrink-0">{dayName}</span>
|
||||||
<span className={`text-sm shrink-0 ${isDark ? "text-rose-300" : "text-rose-600"}`}>
|
{slotLabels.length > 0 && (
|
||||||
({new Date(`2000-01-01T${timeRange.startTime}`).toLocaleTimeString("en-US", {
|
<span className={`text-sm shrink-0 ${isDark ? "text-rose-300" : "text-rose-600"}`}>
|
||||||
hour: "numeric",
|
({slotLabels.join(", ")})
|
||||||
minute: "2-digit",
|
</span>
|
||||||
hour12: true,
|
)}
|
||||||
})}{" "}
|
|
||||||
-{" "}
|
|
||||||
{new Date(`2000-01-01T${timeRange.endTime}`).toLocaleTimeString("en-US", {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
hour12: true,
|
|
||||||
})})
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -835,14 +823,13 @@ export default function Booking() {
|
|||||||
Available Days & Times *
|
Available Days & Times *
|
||||||
</label>
|
</label>
|
||||||
<p className={`text-xs mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
<p className={`text-xs mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
Select days and set time ranges for each day
|
Select days and choose time slots (Morning, Lunchtime, Evening) for each day
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{daysOfWeek.map((day) => {
|
{daysOfWeek.map((day) => {
|
||||||
const isSelected = selectedDays.includes(day.value);
|
const isSelected = selectedDays.includes(day.value);
|
||||||
const timeRange = dayTimeRanges[day.value] || { startTime: "09:00", endTime: "17:00" };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -886,80 +873,30 @@ export default function Booking() {
|
|||||||
|
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="mt-3 pt-3 border-t border-gray-300 dark:border-gray-600">
|
<div className="mt-3 pt-3 border-t border-gray-300 dark:border-gray-600">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<label className={`text-xs font-medium mb-2 block ${isDark ? "text-gray-400" : "text-gray-600"}`}>
|
||||||
{/* Start Time */}
|
Available Time Slots
|
||||||
<div className="space-y-1.5">
|
</label>
|
||||||
<label className={`text-xs font-medium ${isDark ? "text-gray-400" : "text-gray-600"}`}>
|
<div className="flex flex-wrap gap-2">
|
||||||
Start Time
|
{timeSlotOptions.map((slot) => {
|
||||||
</label>
|
const isSelectedSlot = dayTimeSlots[day.value]?.includes(slot.value) || false;
|
||||||
<Select
|
return (
|
||||||
value={timeRange.startTime}
|
<button
|
||||||
onValueChange={(value) => handleTimeRangeChange(day.value, "startTime", value)}
|
key={slot.value}
|
||||||
>
|
type="button"
|
||||||
<SelectTrigger className={`h-9 text-sm ${isDark ? "bg-gray-700 border-gray-600 text-white" : "bg-white border-gray-300"}`}>
|
onClick={() => handleTimeSlotToggle(day.value, slot.value)}
|
||||||
<SelectValue />
|
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-all ${
|
||||||
</SelectTrigger>
|
isSelectedSlot
|
||||||
<SelectContent className={isDark ? "bg-gray-800 border-gray-700" : "bg-white"}>
|
? isDark
|
||||||
{timeSlotsForPicker.map((time) => (
|
? "bg-rose-600 border-rose-500 text-white"
|
||||||
<SelectItem
|
: "bg-rose-500 border-rose-500 text-white"
|
||||||
key={time}
|
: isDark
|
||||||
value={time}
|
? "bg-gray-700 border-gray-600 text-gray-300 hover:border-rose-500"
|
||||||
className={isDark ? "text-white hover:bg-gray-700" : ""}
|
: "bg-white border-gray-300 text-gray-700 hover:border-rose-500"
|
||||||
>
|
}`}
|
||||||
{new Date(`2000-01-01T${time}`).toLocaleTimeString("en-US", {
|
>
|
||||||
hour: "numeric",
|
{slot.label}
|
||||||
minute: "2-digit",
|
</button>
|
||||||
hour12: true,
|
);
|
||||||
})}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* End Time */}
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className={`text-xs font-medium ${isDark ? "text-gray-400" : "text-gray-600"}`}>
|
|
||||||
End Time
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={timeRange.endTime}
|
|
||||||
onValueChange={(value) => handleTimeRangeChange(day.value, "endTime", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className={`h-9 text-sm ${isDark ? "bg-gray-700 border-gray-600 text-white" : "bg-white border-gray-300"}`}>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className={isDark ? "bg-gray-800 border-gray-700" : "bg-white"}>
|
|
||||||
{timeSlotsForPicker.map((time) => (
|
|
||||||
<SelectItem
|
|
||||||
key={time}
|
|
||||||
value={time}
|
|
||||||
className={isDark ? "text-white hover:bg-gray-700" : ""}
|
|
||||||
>
|
|
||||||
{new Date(`2000-01-01T${time}`).toLocaleTimeString("en-US", {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
hour12: true,
|
|
||||||
})}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Time Range Preview */}
|
|
||||||
<div className={`mt-2 p-2 rounded text-xs ${isDark ? "bg-gray-800/50 text-gray-300" : "bg-white text-gray-600"}`}>
|
|
||||||
{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,
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@ -24,6 +24,7 @@ import {
|
|||||||
CheckCircle,
|
CheckCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
CalendarCheck,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@ -34,6 +35,8 @@ import { useAuth } from "@/hooks/useAuth";
|
|||||||
import { useAppointments } from "@/hooks/useAppointments";
|
import { useAppointments } from "@/hooks/useAppointments";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { Appointment } from "@/lib/models/appointments";
|
import type { Appointment } from "@/lib/models/appointments";
|
||||||
|
import { getPublicAvailability } from "@/lib/actions/appointments";
|
||||||
|
import type { AdminAvailability } from "@/lib/models/appointments";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
ID: number;
|
ID: number;
|
||||||
@ -80,7 +83,7 @@ export default function BookNowPage() {
|
|||||||
const { theme } = useAppTheme();
|
const { theme } = useAppTheme();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
const { isAuthenticated, logout } = useAuth();
|
const { isAuthenticated, logout } = useAuth();
|
||||||
const { create, isCreating } = useAppointments();
|
const { create, isCreating, availableDates, availableDatesResponse, isLoadingAvailableDates } = useAppointments();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
@ -95,6 +98,68 @@ export default function BookNowPage() {
|
|||||||
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
||||||
const [showSignupDialog, setShowSignupDialog] = useState(false);
|
const [showSignupDialog, setShowSignupDialog] = useState(false);
|
||||||
const [loginPrefillEmail, setLoginPrefillEmail] = useState<string | undefined>(undefined);
|
const [loginPrefillEmail, setLoginPrefillEmail] = useState<string | undefined>(undefined);
|
||||||
|
const [publicAvailability, setPublicAvailability] = useState<AdminAvailability | null>(null);
|
||||||
|
const [availableTimeSlots, setAvailableTimeSlots] = useState<Record<number, string[]>>({});
|
||||||
|
|
||||||
|
// 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<string>();
|
||||||
|
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 = () => {
|
const handleLogout = () => {
|
||||||
logout();
|
logout();
|
||||||
@ -566,7 +631,7 @@ export default function BookNowPage() {
|
|||||||
Appointment Details
|
Appointment Details
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label
|
<label
|
||||||
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
|
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
|
||||||
@ -574,8 +639,19 @@ export default function BookNowPage() {
|
|||||||
<Calendar className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
|
<Calendar className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
|
||||||
Available Days *
|
Available Days *
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-3">
|
{isLoadingAvailableDates ? (
|
||||||
{['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'].map((day) => (
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Loading available days...
|
||||||
|
</div>
|
||||||
|
) : availableDaysOfWeek.length === 0 ? (
|
||||||
|
<p className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||||
|
No available days at the moment. Please check back later.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{availableDaysOfWeek.map((day) => (
|
||||||
<label
|
<label
|
||||||
key={day}
|
key={day}
|
||||||
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border transition-all ${
|
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border transition-all ${
|
||||||
@ -597,7 +673,9 @@ export default function BookNowPage() {
|
|||||||
<span className="text-sm font-medium">{day}</span>
|
<span className="text-sm font-medium">{day}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -608,11 +686,33 @@ export default function BookNowPage() {
|
|||||||
Preferred Time *
|
Preferred Time *
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{[
|
{(() => {
|
||||||
{ value: 'morning', label: 'Morning' },
|
// Get available time slots based on selected days
|
||||||
{ value: 'lunchtime', label: 'Lunchtime' },
|
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
{ value: 'afternoon', label: 'Afternoon' }
|
const dayIndices = formData.preferredDays.map(day => dayNames.indexOf(day));
|
||||||
].map((time) => (
|
|
||||||
|
// Get all unique time slots from selected days
|
||||||
|
let allAvailableSlots = new Set<string>();
|
||||||
|
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) => (
|
||||||
<label
|
<label
|
||||||
key={time.value}
|
key={time.value}
|
||||||
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border transition-all ${
|
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border transition-all ${
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export function useAppointments() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Get available dates query
|
// Get available dates query
|
||||||
const availableDatesQuery = useQuery<string[]>({
|
const availableDatesQuery = useQuery<AvailableDatesResponse>({
|
||||||
queryKey: ["appointments", "available-dates"],
|
queryKey: ["appointments", "available-dates"],
|
||||||
queryFn: () => getAvailableDates(),
|
queryFn: () => getAvailableDates(),
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
@ -160,7 +160,8 @@ export function useAppointments() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
// Queries
|
// Queries
|
||||||
availableDates: availableDatesQuery.data || [],
|
availableDates: availableDatesQuery.data?.dates || [],
|
||||||
|
availableDatesResponse: availableDatesQuery.data,
|
||||||
appointments: appointmentsQuery.data || [],
|
appointments: appointmentsQuery.data || [],
|
||||||
userAppointments: userAppointmentsQuery.data || [],
|
userAppointments: userAppointmentsQuery.data || [],
|
||||||
adminAvailability: adminAvailabilityQuery.data,
|
adminAvailability: adminAvailabilityQuery.data,
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export async function createAppointment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get available dates
|
// Get available dates
|
||||||
export async function getAvailableDates(): Promise<string[]> {
|
export async function getAvailableDates(): Promise<AvailableDatesResponse> {
|
||||||
const response = await fetch(API_ENDPOINTS.meetings.availableDates, {
|
const response = await fetch(API_ENDPOINTS.meetings.availableDates, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
@ -94,11 +94,14 @@ export async function getAvailableDates(): Promise<string[]> {
|
|||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// API returns array of dates in YYYY-MM-DD format
|
// If API returns array directly, wrap it in response object
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return data;
|
return {
|
||||||
|
dates: data,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return (data as AvailableDatesResponse).dates || [];
|
|
||||||
|
return data as AvailableDatesResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List appointments (Admin sees all, users see their own)
|
// List appointments (Admin sees all, users see their own)
|
||||||
@ -279,6 +282,43 @@ export async function rejectAppointment(
|
|||||||
return data as unknown as Appointment;
|
return data as unknown as Appointment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get admin availability (public version - tries without auth first)
|
||||||
|
export async function getPublicAvailability(): Promise<AdminAvailability | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_ENDPOINTS.meetings.adminAvailability, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: any = await response.json();
|
||||||
|
|
||||||
|
// Handle both string and array formats for available_days
|
||||||
|
let availableDays: number[] = [];
|
||||||
|
if (typeof data.available_days === 'string') {
|
||||||
|
try {
|
||||||
|
availableDays = JSON.parse(data.available_days);
|
||||||
|
} catch {
|
||||||
|
availableDays = data.available_days.split(',').map((d: string) => parseInt(d.trim())).filter((d: number) => !isNaN(d));
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(data.available_days)) {
|
||||||
|
availableDays = data.available_days;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
available_days: availableDays,
|
||||||
|
available_days_display: data.available_days_display || [],
|
||||||
|
} as AdminAvailability;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get admin availability
|
// Get admin availability
|
||||||
export async function getAdminAvailability(): Promise<AdminAvailability> {
|
export async function getAdminAvailability(): Promise<AdminAvailability> {
|
||||||
const tokens = getStoredTokens();
|
const tokens = getStoredTokens();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user