feat/booking-panel #25

Merged
Hammond merged 3 commits from feat/booking-panel into master 2025-11-25 21:33:40 +00:00
4 changed files with 249 additions and 171 deletions
Showing only changes of commit f6bd813c07 - Show all commits

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