"use client"; import dynamic from "next/dynamic"; import { useState, useEffect, useCallback, useRef } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; const API_BASE = ""; const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; function DatePicker({ value, minDate, onChange }) { const today = new Date(); today.setHours(0, 0, 0, 0); const min = minDate ? new Date(minDate + "T00:00:00") : today; const parsed = value ? new Date(value + "T00:00:00") : null; const [viewYear, setViewYear] = useState(parsed ? parsed.getFullYear() : min.getFullYear()); const [viewMonth, setViewMonth] = useState(parsed ? parsed.getMonth() : min.getMonth()); function prevMonth() { if (viewMonth === 0) { setViewMonth(11); setViewYear((y) => y - 1); } else { setViewMonth((m) => m - 1); } } function nextMonth() { if (viewMonth === 11) { setViewMonth(0); setViewYear((y) => y + 1); } else { setViewMonth((m) => m + 1); } } const firstDay = new Date(viewYear, viewMonth, 1).getDay(); const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); const minYear = min.getFullYear(); const minMonth = min.getMonth(); const isPrevDisabled = viewYear < minYear || (viewYear === minYear && viewMonth <= minMonth); function selectDay(day) { const d = new Date(viewYear, viewMonth, day); if (d < min) { return; } const iso = `${viewYear}-${String(viewMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; onChange(iso); } function isSelected(day) { if (!parsed) return false; return parsed.getFullYear() === viewYear && parsed.getMonth() === viewMonth && parsed.getDate() === day; } function isDisabled(day) { return new Date(viewYear, viewMonth, day) < min; } const cells = []; for (let i = 0; i < firstDay; i++) { cells.push({ key: `empty-${viewYear}-${viewMonth}-${String(i)}`, day: null }); } for (let d = 1; d <= daysInMonth; d++) { cells.push({ key: `day-${viewYear}-${viewMonth}-${String(d)}`, day: d }); } const s = { widget: { border: "1px solid #ddd", borderRadius: "10px", overflow: "hidden", background: "white", userSelect: "none", fontFamily: "inherit", }, header: { display: "flex", alignItems: "center", justifyContent: "space-between", background: "orange", padding: "0.55rem 0.75rem", }, monthLabel: { fontSize: "0.95rem", fontWeight: 700, color: "white", }, nav: { background: "none", border: "none", color: "white", fontSize: "1.5rem", lineHeight: 1, cursor: "pointer", padding: "0 0.4rem", borderRadius: "4px", }, grid: { display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: "3px", padding: "0.6rem", }, dayName: { textAlign: "center", fontSize: "0.7rem", fontWeight: 700, color: "#aaa", padding: "0.25rem 0", textTransform: "uppercase", }, dayBase: { display: "flex", alignItems: "center", justifyContent: "center", aspectRatio: "1 / 1", border: "none", borderRadius: "6px", background: "none", fontSize: "0.875rem", cursor: "pointer", color: "#333", fontFamily: "inherit", padding: 0, width: "100%", }, daySelected: { background: "orange", color: "white", fontWeight: 700, }, dayDisabled: { color: "#ccc", cursor: "default", }, selectedLabel: { textAlign: "center", fontSize: "0.82rem", color: "#666", padding: "0.35rem 0.5rem 0.5rem", borderTop: "1px solid #f0f0f0", }, }; return (
{MONTHS[viewMonth]} {viewYear}
{DAYS.map((d) => ( {d} ))} {cells.map(({ key, day }) => day === null ? ( ) : ( ) )}
{parsed && (
Selected: {MONTHS[parsed.getMonth()]} {parsed.getDate()}, {parsed.getFullYear()}
)}
); } function AddPetModal({ token, onClose, onAdded }) { const [petName, setPetName] = useState(""); const [species, setSpecies] = useState(""); const [breed, setBreed] = useState(""); const [submitting, setSubmitting] = useState(false); const [petError, setPetError] = useState(null); async function handleSubmit(e) { e.preventDefault(); setPetError(null); setSubmitting(true); try { const res = await fetch(`${API_BASE}/api/v1/my-pets`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ petName, species, breed: breed || null }), }); if (!res.ok) { const data = await res.json().catch(() => null); throw new Error(data?.message || `Request failed (${res.status})`); } onAdded(); onClose(); } catch (err) { setPetError(err.message); } finally { setSubmitting(false); } } return (
e.stopPropagation()}>

Add a New Pet

{petError &&
{petError}
}
); } function AppointmentsPage() { const { user, token, loading: authLoading } = useAuth(); const router = useRouter(); const searchParams = useSearchParams(); const preselectedPetId = searchParams.get("petId"); const didPreselectRef = useRef(false); const [stores, setStores] = useState([]); const [employees, setEmployees] = useState([]); const [services, setServices] = useState([]); const [allPets, setAllPets] = useState([]); const [customerPets, setCustomerPets] = useState([]); const [availableSlots, setAvailableSlots] = useState([]); const [storeId, setStoreId] = useState(""); const [serviceId, setServiceId] = useState(""); const [employeeId, setEmployeeId] = useState(""); const [appointmentDate, setAppointmentDate] = useState(""); const [appointmentTime, setAppointmentTime] = useState(""); const [selectedPetIds, setSelectedPetIds] = useState([]); const [loadingSlots, setLoadingSlots] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [appointments, setAppointments] = useState([]); const [loadingAppointments, setLoadingAppointments] = useState(false); const [showAddPetModal, setShowAddPetModal] = useState(false); const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; useEffect(() => { if (!authLoading && !user) { const target = preselectedPetId ? `/appointments?petId=${encodeURIComponent(preselectedPetId)}` : "/appointments"; router.push(`/login?next=${encodeURIComponent(target)}`); } }, [authLoading, user, router, preselectedPetId]); const loadCustomerPets = useCallback(() => { if (!token || !canBookAppointments) return; fetch(`${API_BASE}/api/v1/my-pets`, { headers: { Authorization: `Bearer ${token}` }, }) .then((r) => r.json()) .then((data) => setCustomerPets(Array.isArray(data) ? data : [])) .catch(() => {}); }, [token, canBookAppointments]); useEffect(() => { if (!token) { return; } fetch(`${API_BASE}/api/v1/dropdowns/stores`, { headers: { Authorization: `Bearer ${token}` }, }) .then((r) => r.json()) .then(setStores) .catch(() => {}); fetch(`${API_BASE}/api/v1/services?size=100`) .then((r) => r.json()) .then((data) => setServices(data.content ?? [])) .catch(() => {}); fetch(`${API_BASE}/api/v1/pets?size=200&sort=id,asc&status=Available`) .then((r) => r.json()) .then((data) => setAllPets(data.content ?? [])) .catch(() => {}); loadCustomerPets(); }, [token, loadCustomerPets]); useEffect(() => { if (didPreselectRef.current) { return; } if (!preselectedPetId || services.length === 0 || allPets.length === 0) { return; } const adoptionSvc = services.find((s) => s.serviceName.toLowerCase().includes("adopt") ); if (adoptionSvc) { setServiceId(String(adoptionSvc.serviceId)); } setSelectedPetIds([Number(preselectedPetId)]); didPreselectRef.current = true; }, [preselectedPetId, services, allPets]); const loadAppointments = useCallback(() => { if (!token) { return; } setLoadingAppointments(true); fetch(`${API_BASE}/api/v1/appointments?size=50&sort=appointmentDate,desc`, { headers: {Authorization: `Bearer ${token}`}, }) .then((r) => r.json()) .then((data) => setAppointments(data.content ?? [])) .catch(() => {}) .finally(() => setLoadingAppointments(false)); }, [token]); useEffect(() => { loadAppointments(); }, [loadAppointments]); useEffect(() => { if (!token || !storeId) { setEmployees([]); setEmployeeId(""); return; } fetch(`${API_BASE}/api/v1/dropdowns/stores/${storeId}/employees`, { headers: { Authorization: `Bearer ${token}` }, }) .then((r) => r.json()) .then((data) => setEmployees(Array.isArray(data) ? data : [])) .catch(() => setEmployees([])); }, [token, storeId]); useEffect(() => { if (!employees.length) { setEmployeeId(""); return; } const currentExists = employees.some((employee) => String(employee.id) === String(employeeId)); if (!currentExists) { setEmployeeId(String(employees[0].id)); } }, [employees, employeeId]); useEffect(() => { if (!storeId || !serviceId || !appointmentDate) { setAvailableSlots([]); setAppointmentTime(""); return; } setLoadingSlots(true); setAppointmentTime(""); const params = new URLSearchParams({ storeId, serviceId, date: appointmentDate }); fetch(`${API_BASE}/api/v1/appointments/availability?${params}`) .then((r) => { if (!r.ok) { throw new Error("Failed to check availability"); } return r.json(); }) .then(setAvailableSlots) .catch(() => setAvailableSlots([])) .finally(() => setLoadingSlots(false)); }, [storeId, serviceId, appointmentDate]); const selectedService = services.find((s) => s.serviceId === Number(serviceId)); const isAdoptionService = selectedService ? selectedService.serviceName.toLowerCase().includes("adopt") : false; const isCustomerPetService = !!selectedService && !isAdoptionService; const adoptablePets = allPets.filter( (p) => p.petStatus && p.petStatus.toLowerCase() === "available" ); function handleServiceChange(newServiceId) { setServiceId(newServiceId); setSelectedPetIds([]); } function togglePet(petId) { if (isAdoptionService) { setSelectedPetIds((prev) => prev.includes(petId) ? [] : [petId] ); } else { setSelectedPetIds((prev) => prev.includes(petId) ? prev.filter((id) => id !== petId) : [...prev, petId] ); } } function formatTime(timeStr) { const [h, m] = timeStr.split(":"); const hour = parseInt(h, 10); const ampm = hour >= 12 ? "PM" : "AM"; const display = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; return `${display}:${m} ${ampm}`; } function getMinDate() { const d = new Date(); return d.toISOString().split("T")[0]; } const formValid = storeId && serviceId && appointmentDate && appointmentTime && selectedPetIds.length > 0; async function handleSubmit(e) { e.preventDefault(); setError(null); setSuccess(null); if (!canBookAppointments) { setError("Only customer accounts can book appointments from the web app."); return; } if (selectedPetIds.length === 0) { setError(isAdoptionService ? "Please select a pet to adopt." : "Please select at least one pet."); return; } setSubmitting(true); try { const body = { customerId: user.customerId || user.id, storeId: Number(storeId), serviceId: Number(serviceId), employeeId: employeeId ? Number(employeeId) : undefined, appointmentDate, appointmentTime: appointmentTime + ":00", appointmentStatus: "Booked", }; if (selectedPetIds.length > 0) { body.petId = selectedPetIds[0]; } const res = await fetch(`${API_BASE}/api/v1/appointments`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(body), }); if (!res.ok) { const data = await res.json().catch(() => null); throw new Error(data?.message || data?.error || `Request failed (${res.status})`); } setSuccess("Appointment booked successfully!"); setStoreId(""); setServiceId(""); setAppointmentDate(""); setAppointmentTime(""); setSelectedPetIds([]); setAvailableSlots([]); loadAppointments(); } catch (err) { setError(err.message); } finally { setSubmitting(false); } } if (authLoading) { return (

Loading...

); } if (!user) return null; const petsToShow = isAdoptionService ? adoptablePets : isCustomerPetService ? customerPets : []; const petSectionLabel = isAdoptionService ? "Select a Pet to Adopt" : "Select Pet(s)"; const noPetsMessage = isAdoptionService ? "No pets are currently available for adoption." : "No pets found on your profile."; return (
{showAddPetModal && ( setShowAddPetModal(false)} onAdded={loadCustomerPets} /> )}

Schedule an Appointment

Book a service for your pet or schedule a pet adoption visit

{canBookAppointments ? (

New Appointment

{error &&
{error}
} {success &&
{success}
} {employees.length > 0 && ( )} {selectedService && (

{selectedService.serviceDesc}

)}
Date
{storeId && serviceId && appointmentDate && (
Available Time Slots {loadingSlots ? (

Checking availability...

) : availableSlots.length === 0 ? (

No available slots for this date. Please try another date.

) : (
{availableSlots.map((slot) => ( ))}
)}
)} {serviceId && (
{petSectionLabel} {isCustomerPetService && ( )} {petsToShow.length === 0 ? (

{noPetsMessage}

) : isAdoptionService ? (
{petsToShow.map((p) => ( ))}
) : (
{petsToShow.map((p) => ( ))}
)}
)}
) : null}

{canBookAppointments ? "Your Appointments" : "Appointments"}

{loadingAppointments ? (

Loading appointments...

) : appointments.length === 0 ? (

No appointments yet.

) : (
{appointments.map((a) => (
{a.serviceName} {a.appointmentStatus}
{a.storeName} {a.appointmentDate} at {formatTime(a.appointmentTime)}
{a.petNames && a.petNames.length > 0 && (
Pets: {a.petNames.join(", ")}
)} {a.customerPetNames && a.customerPetNames.length > 0 && (
Pets: {a.customerPetNames.join(", ")}
)}
))}
)}
); } export default dynamic(() => Promise.resolve(AppointmentsPage), { ssr: false, });