"use client"; import React from "react"; import { useEffect, useState, useCallback } from "react"; import { Permission } from "../auth-context"; import { api } from "../api"; import { Header } from "../components/Header"; import { useRequireAuth } from "../hooks/useRequireAuth"; import { components } from "../generated/api"; import constants from "../../../shared/constants.json"; import { formatDate, formatTime } from "../utils/date"; const { slotDurationMinutes, maxAdvanceDays, minAdvanceDays, noteMaxLength } = constants.booking; type BookableSlot = components["schemas"]["BookableSlot"]; type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"]; type AppointmentResponse = components["schemas"]["AppointmentResponse"]; // Get date range for booking (tomorrow to +30 days) function getBookableDates(): Date[] { const dates: Date[] = []; const today = new Date(); for (let i = minAdvanceDays; i <= maxAdvanceDays; i++) { const d = new Date(today); d.setDate(today.getDate() + i); dates.push(d); } return dates; } const styles: Record = { main: { minHeight: "100vh", background: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b4e 100%)", display: "flex", flexDirection: "column", }, loader: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "'DM Sans', system-ui, sans-serif", color: "rgba(255, 255, 255, 0.5)", }, content: { flex: 1, padding: "2rem", maxWidth: "900px", margin: "0 auto", width: "100%", }, pageTitle: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "1.75rem", fontWeight: 600, color: "#fff", marginBottom: "0.5rem", }, pageSubtitle: { fontFamily: "'DM Sans', system-ui, sans-serif", color: "rgba(255, 255, 255, 0.5)", fontSize: "0.9rem", marginBottom: "1.5rem", }, successBanner: { background: "rgba(34, 197, 94, 0.15)", border: "1px solid rgba(34, 197, 94, 0.3)", color: "#4ade80", padding: "1rem", borderRadius: "8px", marginBottom: "1rem", fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "0.875rem", }, errorBanner: { background: "rgba(239, 68, 68, 0.15)", border: "1px solid rgba(239, 68, 68, 0.3)", color: "#f87171", padding: "1rem", borderRadius: "8px", marginBottom: "1rem", fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "0.875rem", }, section: { marginBottom: "2rem", }, sectionTitle: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "1.1rem", fontWeight: 500, color: "#fff", marginBottom: "1rem", }, dateGrid: { display: "flex", flexWrap: "wrap", gap: "0.5rem", }, dateButton: { fontFamily: "'DM Sans', system-ui, sans-serif", padding: "0.75rem 1rem", background: "rgba(255, 255, 255, 0.03)", border: "1px solid rgba(255, 255, 255, 0.08)", borderRadius: "10px", cursor: "pointer", minWidth: "90px", textAlign: "center" as const, transition: "all 0.2s", }, dateButtonSelected: { background: "rgba(167, 139, 250, 0.15)", borderColor: "#a78bfa", }, dateWeekday: { color: "#fff", fontWeight: 500, fontSize: "0.875rem", marginBottom: "0.25rem", }, dateDay: { color: "rgba(255, 255, 255, 0.5)", fontSize: "0.8rem", }, slotGrid: { display: "flex", flexWrap: "wrap", gap: "0.5rem", }, slotButton: { fontFamily: "'DM Sans', system-ui, sans-serif", padding: "0.6rem 1.25rem", background: "rgba(255, 255, 255, 0.03)", border: "1px solid rgba(255, 255, 255, 0.08)", borderRadius: "8px", color: "#fff", cursor: "pointer", fontSize: "0.9rem", transition: "all 0.2s", }, slotButtonSelected: { background: "rgba(167, 139, 250, 0.15)", borderColor: "#a78bfa", }, emptyState: { fontFamily: "'DM Sans', system-ui, sans-serif", color: "rgba(255, 255, 255, 0.4)", padding: "1rem 0", }, confirmCard: { background: "rgba(255, 255, 255, 0.03)", border: "1px solid rgba(255, 255, 255, 0.08)", borderRadius: "12px", padding: "1.5rem", maxWidth: "400px", }, confirmTitle: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "1.1rem", fontWeight: 500, color: "#fff", marginBottom: "1rem", }, confirmTime: { fontFamily: "'DM Sans', system-ui, sans-serif", color: "rgba(255, 255, 255, 0.7)", marginBottom: "1rem", }, inputLabel: { fontFamily: "'DM Sans', system-ui, sans-serif", display: "block", color: "rgba(255, 255, 255, 0.7)", fontSize: "0.875rem", marginBottom: "0.5rem", }, textarea: { fontFamily: "'DM Sans', system-ui, sans-serif", width: "100%", padding: "0.75rem", background: "rgba(255, 255, 255, 0.05)", border: "1px solid rgba(255, 255, 255, 0.1)", borderRadius: "8px", color: "#fff", fontSize: "0.875rem", minHeight: "80px", resize: "vertical" as const, }, charCount: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "0.75rem", color: "rgba(255, 255, 255, 0.4)", textAlign: "right" as const, marginTop: "0.25rem", }, charCountWarning: { color: "#f87171", }, buttonRow: { display: "flex", gap: "0.75rem", marginTop: "1.5rem", }, bookButton: { fontFamily: "'DM Sans', system-ui, sans-serif", flex: 1, padding: "0.75rem", background: "linear-gradient(135deg, #a78bfa 0%, #7c3aed 100%)", border: "none", borderRadius: "8px", color: "#fff", fontWeight: 500, cursor: "pointer", transition: "all 0.2s", }, bookButtonDisabled: { opacity: 0.5, cursor: "not-allowed", }, cancelButton: { fontFamily: "'DM Sans', system-ui, sans-serif", padding: "0.75rem 1.25rem", background: "rgba(255, 255, 255, 0.05)", border: "1px solid rgba(255, 255, 255, 0.1)", borderRadius: "8px", color: "rgba(255, 255, 255, 0.7)", cursor: "pointer", transition: "all 0.2s", }, }; export default function BookingPage() { const { user, isLoading, isAuthorized } = useRequireAuth({ requiredPermission: Permission.BOOK_APPOINTMENT, fallbackRedirect: "/", }); const [selectedDate, setSelectedDate] = useState(null); const [availableSlots, setAvailableSlots] = useState([]); const [selectedSlot, setSelectedSlot] = useState(null); const [note, setNote] = useState(""); const [isLoadingSlots, setIsLoadingSlots] = useState(false); const [isBooking, setIsBooking] = useState(false); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const dates = getBookableDates(); const fetchSlots = useCallback(async (date: Date) => { setIsLoadingSlots(true); setError(null); setAvailableSlots([]); setSelectedSlot(null); try { const dateStr = formatDate(date); const data = await api.get(`/api/booking/slots?date=${dateStr}`); setAvailableSlots(data.slots); } catch (err) { console.error("Failed to fetch slots:", err); setError("Failed to load available slots"); } finally { setIsLoadingSlots(false); } }, []); useEffect(() => { if (selectedDate && user && isAuthorized) { fetchSlots(selectedDate); } }, [selectedDate, user, isAuthorized, fetchSlots]); const handleDateSelect = (date: Date) => { setSelectedDate(date); setSuccessMessage(null); }; const handleSlotSelect = (slot: BookableSlot) => { setSelectedSlot(slot); setNote(""); setError(null); }; const handleBook = async () => { if (!selectedSlot) return; setIsBooking(true); setError(null); try { const appointment = await api.post("/api/booking", { slot_start: selectedSlot.start_time, note: note || null, }); setSuccessMessage(`Appointment booked for ${formatTime(appointment.slot_start)} - ${formatTime(appointment.slot_end)}`); setSelectedSlot(null); setNote(""); // Refresh slots to show the booked one is gone if (selectedDate) { await fetchSlots(selectedDate); } } catch (err) { if (err instanceof Error) { setError(err.message); } else { setError("Failed to book appointment"); } } finally { setIsBooking(false); } }; const cancelSlotSelection = () => { setSelectedSlot(null); setNote(""); setError(null); }; if (isLoading) { return (
Loading...
); } if (!isAuthorized) { return null; } return (

Book an Appointment

Select a date to see available {slotDurationMinutes}-minute slots

{successMessage && (
{successMessage}
)} {error && (
{error}
)} {/* Date Selection */}

Select a Date

{dates.map((date) => { const isSelected = selectedDate && formatDate(selectedDate) === formatDate(date); return ( ); })}
{/* Available Slots */} {selectedDate && (

Available Slots for {selectedDate.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" })}

{isLoadingSlots ? (
Loading slots...
) : availableSlots.length === 0 ? (
No available slots for this date
) : (
{availableSlots.map((slot) => { const isSelected = selectedSlot?.start_time === slot.start_time; return ( ); })}
)}
)} {/* Booking Form */} {selectedSlot && (

Confirm Booking

Time: {formatTime(selectedSlot.start_time)} - {formatTime(selectedSlot.end_time)}