diff --git a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java index 5e64afd9..4c9d402b 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -50,7 +50,7 @@ public class AppointmentController { .orElse(null); Long effectiveCustomerId = customerId; - if (role != null && role.equals("CUSTOMER")) { + if (role != null && (role.equals("CUSTOMER") || role.equals("ADMIN"))) { User user = AuthenticationHelper.getAuthenticatedUser(userRepository); effectiveCustomerId = user.getId(); } @@ -88,7 +88,7 @@ public class AppointmentController { .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - if (role != null && role.equals("CUSTOMER")) { + if (role != null && (role.equals("CUSTOMER") || role.equals("ADMIN"))) { User user = AuthenticationHelper.getAuthenticatedUser(userRepository); if (!request.getCustomerId().equals(user.getId())) { throw new org.springframework.security.access.AccessDeniedException("You can only create appointments for yourself"); diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index f8e4ec2d..b4dd5cee 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -195,6 +195,100 @@ function DatePicker({ value, minDate, onChange }) { ); } +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(); @@ -224,7 +318,9 @@ function AppointmentsPage() { const [appointments, setAppointments] = useState([]); const [loadingAppointments, setLoadingAppointments] = useState(false); - const canBookAppointments = user?.role === "CUSTOMER"; + const [showAddPetModal, setShowAddPetModal] = useState(false); + +const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; useEffect(() => { if (!authLoading && !user) { @@ -234,6 +330,16 @@ function AppointmentsPage() { }, [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; @@ -256,15 +362,8 @@ function AppointmentsPage() { .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]); + loadCustomerPets(); + }, [token, loadCustomerPets]); useEffect(() => { if (didPreselectRef.current) { @@ -392,7 +491,6 @@ function AppointmentsPage() { function getMinDate() { const d = new Date(); - d.setDate(d.getDate() + 1); return d.toISOString().split("T")[0]; } @@ -411,12 +509,6 @@ function AppointmentsPage() { 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."); @@ -427,7 +519,7 @@ function AppointmentsPage() { try { const body = { - customerId: user.customerId, + customerId: user.customerId || user.id, storeId: Number(storeId), serviceId: Number(serviceId), employeeId: employeeId ? Number(employeeId) : undefined, @@ -436,12 +528,8 @@ function AppointmentsPage() { appointmentStatus: "Booked", }; - if (isCustomerPetService) { - body.customerPetIds = selectedPetIds; - } - - else { - body.petIds = selectedPetIds; + if (selectedPetIds.length > 0) { + body.petId = selectedPetIds[0]; } const res = await fetch(`${API_BASE}/api/v1/appointments`, { @@ -493,10 +581,18 @@ function AppointmentsPage() { 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."; + : "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

@@ -600,6 +696,15 @@ function AppointmentsPage() { {serviceId && (
{petSectionLabel} + {isCustomerPetService && ( + + )} {petsToShow.length === 0 ? (

{noPetsMessage}

) : isAdoptionService ? ( diff --git a/web/app/globals.css b/web/app/globals.css index 8571bfeb..43ec07e6 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -1351,6 +1351,44 @@ body { accent-color: orange; } +.appt-add-pet-btn { + display: inline-block; + margin-top: 0.5rem; + padding: 0.4rem 0.85rem; + background: none; + border: 1.5px solid orange; + border-radius: 6px; + color: orange; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.appt-add-pet-btn:hover { + background: orange; + color: white; +} + +.appt-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.appt-modal { + background: white; + border-radius: 12px; + padding: 2rem; + width: 100%; + max-width: 420px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18); +} + .appt-link { color: orange; font-weight: 600; diff --git a/web/app/profile/page.js b/web/app/profile/page.js index ff79521e..cf2f0d2d 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -108,7 +108,7 @@ export default function ProfilePage() { }, [clearPetImageObjectUrls]); useEffect(() => { - if (user?.role === "CUSTOMER") { +if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { loadPets(); } }, [user, loadPets]); @@ -419,7 +419,7 @@ export default function ProfilePage() {
- {user.role === "CUSTOMER" && ( +{(user.role === "CUSTOMER" || user.role === "ADMIN") && (

My Pets