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:
iamkiddy 2025-11-25 21:04:22 +00:00
parent 6bcb6c5414
commit f6bd813c07
4 changed files with 249 additions and 171 deletions

View File

@ -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>

View File

@ -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 ${

View File

@ -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,

View File

@ -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();