"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 ? (
) : (
selectDay(day)}
disabled={isDisabled(day)}
aria-pressed={isSelected(day)}
>
{day}
)
)}
{parsed && (
Selected: {MONTHS[parsed.getMonth()]} {parsed.getDate()}, {parsed.getFullYear()}
)}
);
}
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 [services, setServices] = useState([]);
const [allPets, setAllPets] = useState([]);
const [customerPets, setCustomerPets] = useState([]);
const [availableSlots, setAvailableSlots] = useState([]);
const [storeId, setStoreId] = useState("");
const [serviceId, setServiceId] = 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 canBookAppointments = user?.role === "CUSTOMER";
useEffect(() => {
if (!authLoading && !user) {
const target = preselectedPetId ? `/appointments?petId=${encodeURIComponent(preselectedPetId)}` : "/appointments";
router.push(`/login?next=${encodeURIComponent(target)}`);
}
}, [authLoading, user, router, preselectedPetId]);
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=petId,asc`)
.then((r) => r.json())
.then((data) => setAllPets(data.content ?? []))
.catch(() => {});
if (canBookAppointments) {
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 (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 (!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();
d.setDate(d.getDate() + 1);
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 (!user?.customerId) {
setError("Customer account not found. Please contact support.");
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,
storeId: Number(storeId),
serviceId: Number(serviceId),
appointmentDate,
appointmentTime: appointmentTime + ":00",
appointmentStatus: "Booked",
};
if (isCustomerPetService) {
body.customerPetIds = selectedPetIds;
}
else {
body.petIds = selectedPetIds;
}
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. Please add your pets in your profile before booking.";
return (
Schedule an Appointment
Book a service for your pet or schedule a pet adoption visit
{canBookAppointments ? (
) : (
Appointment Booking
Web appointment booking is currently available for customer accounts only.
Admin and staff accounts can still review appointment activity below.
)}
{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,
});