From a1ec3e728b591b0b93620136b8b552298c26bbc0 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 16 Apr 2026 07:55:13 -0600 Subject: [PATCH 1/3] fix six app bugs --- .../petshop/backend/controller/AiChatController.java | 3 ++- .../backend/repository/AppointmentRepository.java | 3 +++ .../com/petshop/backend/repository/PetRepository.java | 1 + .../petshop/backend/service/AppointmentService.java | 11 +++++++++++ web/app/ai-chat/page.js | 9 ++------- web/app/chat/page.js | 5 ----- web/app/globals.css | 2 ++ 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AiChatController.java b/backend/src/main/java/com/petshop/backend/controller/AiChatController.java index ba08b418..f65dbaf0 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AiChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AiChatController.java @@ -56,7 +56,8 @@ public class AiChatController { List userPets; try { - userPets = petRepository.findAllByOwner_IdOrderByPetNameAsc(user.getId()); + userPets = petRepository.findAllByOwner_IdAndPetStatusInOrderByPetNameAsc( + user.getId(), List.of("Adopted", "Owned")); } catch (Exception e) { diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index e18a007a..59243df2 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -53,5 +53,8 @@ public interface AppointmentRepository extends JpaRepository List findByPet_Id(Long petId); + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.pet.petId = :petId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") + List findByPetIdAndAppointmentDate(@Param("petId") Long petId, @Param("date") LocalDate date); + List findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status); } diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index 9358fb72..e294898f 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -37,6 +37,7 @@ public interface PetRepository extends JpaRepository { List findAdoptablePetsByStore(@Param("storeId") Long storeId); List findAllByOwner_IdOrderByPetNameAsc(Long ownerId); + List findAllByOwner_IdAndPetStatusInOrderByPetNameAsc(Long ownerId, List statuses); Optional findByIdAndOwner_Id(Long id, Long ownerId); @Lock(LockModeType.PESSIMISTIC_WRITE) diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 999568a8..efc5d2c6 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -132,6 +132,7 @@ public class AppointmentService { validateStoreAccess(store.getStoreId(), authenticatedUser); validatePetServiceCompatibility(pet, service); validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null); + validatePetAvailability(pet, service, request.getAppointmentDate(), request.getAppointmentTime(), null); Appointment appointment = new Appointment(); appointment.setCustomer(customer); @@ -172,6 +173,7 @@ public class AppointmentService { validateStoreAccess(store.getStoreId(), authenticatedUser); validatePetServiceCompatibility(pet, service); validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id); + validatePetAvailability(pet, service, request.getAppointmentDate(), request.getAppointmentTime(), id); appointment.setCustomer(customer); appointment.setStore(store); @@ -387,6 +389,15 @@ public class AppointmentService { return true; } + private void validatePetAvailability(Pet pet, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { + if (pet == null) return; + List existingAppointments = appointmentRepository + .findByPetIdAndAppointmentDate(pet.getPetId(), date); + if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { + throw new IllegalArgumentException("This pet already has an appointment during this time slot"); + } + } + private void validateSpeciesServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) { if (pet == null || service == null) return; String species = pet.getPetSpecies(); diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index 8bfe0bb8..0cdd6429 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -45,9 +45,9 @@ function AiChatPage() { lastScrolledIdRef.current = lastMsg.id; const area = messagesAreaRef.current; if (!area) return; - const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 150; + const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 80; if (nearBottom) { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + area.scrollTop = area.scrollHeight; } }, [messages]); @@ -347,11 +347,6 @@ function AiChatPage() { if (pollRef.current) clearInterval(pollRef.current); setMessages([]); setError(null); - setLoadingConv(true); - await fetchConversation(convId); - await fetchMessages(convId); - setLoadingConv(false); - startPolling(convId); router.replace(`/ai-chat?id=${convId}`, { scroll: false }); } diff --git a/web/app/chat/page.js b/web/app/chat/page.js index 7feeb5dd..a566a487 100644 --- a/web/app/chat/page.js +++ b/web/app/chat/page.js @@ -352,11 +352,6 @@ function ChatPage() { if (pollRef.current) clearInterval(pollRef.current); setMessages([]); setError(null); - setLoadingConv(true); - await fetchConversation(convId); - await fetchMessages(convId); - setLoadingConv(false); - startPolling(convId); router.replace(`/chat?id=${convId}`, { scroll: false }); } diff --git a/web/app/globals.css b/web/app/globals.css index fb521491..0ba62bfb 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -58,6 +58,8 @@ body { align-items: center; gap: 1.25rem; justify-content: center; + min-width: 0; + overflow: hidden; } /* Indivdual Link Styles */ -- 2.49.1 From 417b27b909e0ee69245c536442466106327760f8 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 16 Apr 2026 07:56:57 -0600 Subject: [PATCH 2/3] guard stale init effects --- web/app/ai-chat/page.js | 11 +++++++++-- web/app/chat/page.js | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index 0cdd6429..fdc43d84 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -136,6 +136,8 @@ function AiChatPage() { useEffect(() => { if (!token || authLoading) return; + let stale = false; + async function init() { setLoadingConv(true); setError(null); @@ -147,6 +149,7 @@ function AiChatPage() { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { headers: { Authorization: `Bearer ${token}` }, }); + if (stale) return; if (res.ok) { const list = await res.json(); const openAi = Array.isArray(list) @@ -155,12 +158,12 @@ function AiChatPage() { if (openAi) convId = openAi.id; } } catch { + if (stale) return; setError("Failed to load conversations."); } } if (!convId) { - // Auto-create a new AI conversation try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { method: "POST", @@ -170,17 +173,19 @@ function AiChatPage() { }, body: JSON.stringify({ message: "Hello! I'd like to chat with the AI assistant." }), }); + if (stale) return; if (res.ok) { const conv = await res.json(); convId = conv.id; } } catch { - // silent + if (stale) return; } } if (!convId) { await fetchConversations(); + if (stale) return; setLoadingConv(false); return; } @@ -190,6 +195,7 @@ function AiChatPage() { fetchMessages(convId), fetchConversations(), ]); + if (stale) return; setLoadingConv(false); startPolling(convId); router.replace(`/ai-chat?id=${convId}`, { scroll: false }); @@ -198,6 +204,7 @@ function AiChatPage() { init(); return () => { + stale = true; if (pollRef.current) clearInterval(pollRef.current); }; }, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, startPolling, fetchConversations, router]); diff --git a/web/app/chat/page.js b/web/app/chat/page.js index a566a487..28e71a96 100644 --- a/web/app/chat/page.js +++ b/web/app/chat/page.js @@ -152,6 +152,8 @@ function ChatPage() { useEffect(() => { if (!token || authLoading) return; + let stale = false; + async function init() { setLoadingConv(true); setError(null); @@ -163,6 +165,7 @@ function ChatPage() { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { headers: { Authorization: `Bearer ${token}` }, }); + if (stale) return; if (res.ok) { const list = await res.json(); const open = Array.isArray(list) @@ -171,12 +174,14 @@ function ChatPage() { if (open) convId = open.id; } } catch { + if (stale) return; setError("Failed to load conversations."); } } if (!convId) { await fetchConversations(); + if (stale) return; setLoadingConv(false); setConversation(null); return; @@ -187,6 +192,7 @@ function ChatPage() { fetchMessages(convId), fetchConversations(), ]); + if (stale) return; setLoadingConv(false); startPolling(convId); } @@ -194,6 +200,7 @@ function ChatPage() { init(); return () => { + stale = true; if (pollRef.current) clearInterval(pollRef.current); }; }, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, startPolling, fetchConversations]); -- 2.49.1 From 4b6895ccd9159bebbb9daeef075c147020d14d73 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 16 Apr 2026 07:58:46 -0600 Subject: [PATCH 3/3] normalize pet status casing --- web/app/appointments/page.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index 58b741f9..e596bcde 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -603,7 +603,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; }, [storeId, serviceId, appointmentDate]); const eligiblePets = customerPets.filter( - (p) => p.petStatus === "Owned" || p.petStatus === "Adopted" + (p) => p.petStatus?.toLowerCase() === "owned" || p.petStatus?.toLowerCase() === "adopted" ); const selectedService = services.find((s) => s.serviceId === Number(serviceId)); @@ -663,7 +663,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; return; } - if (!adoptionMode && selectedPet && selectedPet.petStatus !== "Owned" && selectedPet.petStatus !== "Adopted") { + if (!adoptionMode && selectedPet && selectedPet.petStatus?.toLowerCase() !== "owned" && selectedPet.petStatus?.toLowerCase() !== "adopted") { setError("The selected pet is no longer eligible for appointments. Please refresh the page."); return; } -- 2.49.1