feat/booking-panel #25
@ -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