From 580813792ac4742812624274c5dfa51c551b9ddc Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Tue, 14 Apr 2026 07:14:54 -0600 Subject: [PATCH 01/14] Pet adoption appointments (currently has a small issue) --- .../controller/AdoptionController.java | 10 ++ .../dto/adoption/CustomerAdoptionRequest.java | 37 +++++ .../backend/service/AdoptionService.java | 27 ++++ web/app/adopt/[id]/page.js | 2 + web/app/appointments/page.js | 127 +++++++++++++----- web/app/globals.css | 11 ++ web/components/PetProfile.js | 7 +- 7 files changed, 187 insertions(+), 34 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java index 23e372bf..ac10dfdc 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -2,6 +2,7 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.adoption.AdoptionRequest; import com.petshop.backend.dto.adoption.AdoptionResponse; +import com.petshop.backend.dto.adoption.CustomerAdoptionRequest; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; @@ -81,6 +82,15 @@ public class AdoptionController { return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request)); } + @PostMapping("/request") + @PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')") + public ResponseEntity requestAdoption(@Valid @RequestBody CustomerAdoptionRequest request) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + return ResponseEntity.status(HttpStatus.CREATED).body( + adoptionService.requestAdoption(user.getId(), request.getPetId(), request.getEmployeeId(), request.getSourceStoreId()) + ); + } + @PutMapping("/{id}") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateAdoption( diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java b/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java new file mode 100644 index 00000000..8312ed38 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java @@ -0,0 +1,37 @@ +package com.petshop.backend.dto.adoption; + +import jakarta.validation.constraints.NotNull; + +public class CustomerAdoptionRequest { + + @NotNull(message = "Pet ID is required") + private Long petId; + + private Long employeeId; + + private Long sourceStoreId; + + public Long getPetId() { + return petId; + } + + public void setPetId(Long petId) { + this.petId = petId; + } + + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public Long getSourceStoreId() { + return sourceStoreId; + } + + public void setSourceStoreId(Long sourceStoreId) { + this.sourceStoreId = sourceStoreId; + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 0874aded..c4a1ae03 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -147,6 +147,33 @@ public class AdoptionService { return mapToResponse(adoption); } + @Transactional + public AdoptionResponse requestAdoption(Long customerId, Long petId, Long employeeId, Long sourceStoreId) { + Pet pet = petRepository.findById(petId) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId)); + User customer = userRepository.findById(customerId) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + customerId)); + User employee = resolveAdoptionEmployee(employeeId); + StoreLocation sourceStore = sourceStoreId != null + ? storeRepository.findById(sourceStoreId) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + sourceStoreId)) + : null; + + validatePetAvailability(pet, null, null); + + Adoption adoption = new Adoption(); + adoption.setPet(pet); + adoption.setCustomer(customer); + adoption.setEmployee(employee); + adoption.setSourceStore(sourceStore); + adoption.setAdoptionDate(null); + adoption.setAdoptionStatus(ADOPTION_STATUS_PENDING); + + adoption = adoptionRepository.save(adoption); + syncPetStatus(pet, ADOPTION_STATUS_PENDING, adoption.getAdoptionId(), customer); + return mapToResponse(adoption); + } + @Transactional public void deleteAdoption(Long id) { if (!adoptionRepository.existsById(id)) { diff --git a/web/app/adopt/[id]/page.js b/web/app/adopt/[id]/page.js index 6c7b70e0..f2ade475 100644 --- a/web/app/adopt/[id]/page.js +++ b/web/app/adopt/[id]/page.js @@ -44,6 +44,8 @@ export default function PetDetailPage() { petStatus={pet.petStatus} petPrice={pet.petPrice} imageUrl={pet.imageUrl} + storeId={pet.storeId} + storeName={pet.storeName} /> )} diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index b4dd5cee..f6cd1b6c 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -294,6 +294,16 @@ function AppointmentsPage() { const router = useRouter(); const searchParams = useSearchParams(); const preselectedPetId = searchParams.get("petId"); + + // Adoption mode — set when arriving from a pet detail page + const adoptionMode = searchParams.get("adoptionMode") === "true"; + const adoptionPetId = searchParams.get("petId"); + const adoptionPetName = searchParams.get("petName") || ""; + const adoptionPetSpecies = searchParams.get("petSpecies") || ""; + const adoptionPetBreed = searchParams.get("petBreed") || ""; + const adoptionStoreId = searchParams.get("storeId") || ""; + const adoptionStoreName = searchParams.get("storeName") || ""; + const didPreselectRef = useRef(false); const [stores, setStores] = useState([]); @@ -366,9 +376,24 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; }, [token, loadCustomerPets]); useEffect(() => { - if (didPreselectRef.current) { + if (didPreselectRef.current) return; + + if (adoptionMode) { + // Need both the store (so employees load) and a serviceId (so availability slots load) + if (adoptionStoreId && services.length > 0) { + setStoreId(adoptionStoreId); + // Prefer a service named "adopt", fall back to the first available service + const adoptionSvc = + services.find((s) => s.serviceName.toLowerCase().includes("adopt")) || + services[0]; + if (adoptionSvc) { + setServiceId(String(adoptionSvc.serviceId)); + didPreselectRef.current = true; + } + } return; } + if (!preselectedPetId || services.length === 0 || allPets.length === 0) { return; } @@ -382,7 +407,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; } setSelectedPetIds([Number(preselectedPetId)]); didPreselectRef.current = true; - }, [preselectedPetId, services, allPets]); + }, [adoptionMode, adoptionStoreId, preselectedPetId, services, allPets]); const loadAppointments = useCallback(() => { @@ -495,8 +520,9 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; return d.toISOString().split("T")[0]; } - const formValid = - storeId && serviceId && appointmentDate && appointmentTime && selectedPetIds.length > 0; + const formValid = adoptionMode + ? Boolean(employeeId && appointmentDate && appointmentTime) + : storeId && serviceId && appointmentDate && appointmentTime && selectedPetIds.length > 0; async function handleSubmit(e) { e.preventDefault(); @@ -509,7 +535,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; return; } - if (selectedPetIds.length === 0) { + if (!adoptionMode && selectedPetIds.length === 0) { setError(isAdoptionService ? "Please select a pet to adopt." : "Please select at least one pet."); return; @@ -518,6 +544,33 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; setSubmitting(true); try { + if (adoptionMode) { + // Submit an adoption request directly to the adoption table + const body = { + petId: Number(adoptionPetId), + employeeId: employeeId ? Number(employeeId) : undefined, + sourceStoreId: adoptionStoreId ? Number(adoptionStoreId) : undefined, + }; + + const res = await fetch(`${API_BASE}/api/v1/adoptions/request`, { + 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(`Adoption request submitted! ${adoptionPetName} is now marked as Pending. We'll be in touch soon.`); + setEmployeeId(""); + return; + } + const body = { customerId: user.customerId || user.id, storeId: Number(storeId), @@ -609,34 +662,44 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; {employees.length > 0 && ( @@ -654,7 +717,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; )} - {selectedService && ( + {!adoptionMode && selectedService && (

{selectedService.serviceDesc}

@@ -693,7 +756,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; )} - {serviceId && ( + {!adoptionMode && serviceId && (
{petSectionLabel} {isCustomerPetService && ( @@ -762,7 +825,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; className="appt-submit-btn" disabled={!formValid || submitting} > - {submitting ? "Booking..." : isAdoptionService ? "Schedule Adoption Visit" : "Book Appointment"} + {submitting ? "Booking..." : adoptionMode ? "Schedule Appointment" : isAdoptionService ? "Schedule Adoption Visit" : "Book Appointment"} ) : null} diff --git a/web/app/globals.css b/web/app/globals.css index dd607b01..0b263f02 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -1356,6 +1356,17 @@ body { box-shadow: 0 0 0 3px rgba(255, 165, 0, 0.2); } +.appt-locked-field { + padding: 0.6rem 0.85rem; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 1rem; + background: #f5f5f5; + color: #555; + font-weight: 600; + cursor: not-allowed; +} + .appt-service-info { background: #fff8f0; border: 1px solid #ffd180; diff --git a/web/components/PetProfile.js b/web/components/PetProfile.js index 8b7fd6f0..7242dcd6 100644 --- a/web/components/PetProfile.js +++ b/web/components/PetProfile.js @@ -1,7 +1,7 @@ import Link from "next/link"; import { getStatusClass } from "@/components/petUtils"; -export default function PetProfile({ petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl }) { +export default function PetProfile({ petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, storeId, storeName }) { return (
@@ -53,7 +53,10 @@ export default function PetProfile({ petId, petName, petSpecies, petBreed, petAg

Interested in adopting {petName}? Visit us in store or schedule an appointment.

- + Schedule an Appointment
From 505560e7daea469b5cda57c04bcdc74a5c2bc4da Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Tue, 14 Apr 2026 07:35:06 -0600 Subject: [PATCH 02/14] Favicon updated --- web/app/favicon.ico | Bin 25931 -> 180453 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/web/app/favicon.ico b/web/app/favicon.ico index 718d6fea4835ec2d246af9800eddb7ffb276240c..5e11bd61cef16f77290fdf25a7b92cfc8e322187 100644 GIT binary patch literal 180453 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%0DHT|1T7s-e*~#I7O!b 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 LrJugUA?W`A8`#=m From deccb2721334912d3e1cca94b20b369eb0e0dee7 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Tue, 14 Apr 2026 12:20:48 -0600 Subject: [PATCH 03/14] Fixes for appointments and My Pets fields. --- .../controller/AdoptionController.java | 20 +- .../controller/AppointmentController.java | 18 + .../backend/controller/MyPetController.java | 4 +- .../dto/adoption/CustomerAdoptionRequest.java | 12 + .../backend/dto/pet/MyPetResponse.java | 12 +- .../repository/AppointmentRepository.java | 2 + .../backend/security/SecurityConfig.java | 2 +- .../backend/service/AdoptionService.java | 44 +- .../backend/service/AppointmentService.java | 59 +++ .../petshop/backend/service/PetService.java | 23 +- web/app/appointments/page.js | 451 ++++++++++++------ web/app/globals.css | 33 ++ web/app/profile/page.js | 83 ++-- 13 files changed, 570 insertions(+), 193 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java index ac10dfdc..ee577f34 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -87,10 +87,28 @@ public class AdoptionController { public ResponseEntity requestAdoption(@Valid @RequestBody CustomerAdoptionRequest request) { User user = AuthenticationHelper.getAuthenticatedUser(userRepository); return ResponseEntity.status(HttpStatus.CREATED).body( - adoptionService.requestAdoption(user.getId(), request.getPetId(), request.getEmployeeId(), request.getSourceStoreId()) + adoptionService.requestAdoption(user.getId(), request.getPetId(), request.getEmployeeId(), request.getSourceStoreId(), request.getAdoptionDate()) ); } + @PatchMapping("/{id}/cancel") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity cancelAdoption(@PathVariable Long id) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = null; + if ("CUSTOMER".equals(role)) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); + } + + return ResponseEntity.ok(adoptionService.cancelAdoption(id, customerId)); + } + @PutMapping("/{id}") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateAdoption( 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 0dc828a1..e0214fa7 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -98,6 +98,24 @@ public class AppointmentController { return ResponseEntity.status(HttpStatus.CREATED).body(appointmentService.createAppointment(request)); } + @PatchMapping("/{id}/cancel") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity cancelAppointment(@PathVariable Long id) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String role = authentication.getAuthorities().stream() + .findFirst() + .map(authority -> authority.getAuthority().replace("ROLE_", "")) + .orElse(null); + + Long customerId = null; + if ("CUSTOMER".equals(role)) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); + } + + return ResponseEntity.ok(appointmentService.cancelAppointment(id, customerId)); + } + @PutMapping("/{id}") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateAppointment( diff --git a/backend/src/main/java/com/petshop/backend/controller/MyPetController.java b/backend/src/main/java/com/petshop/backend/controller/MyPetController.java index e43dbc42..18e8e914 100644 --- a/backend/src/main/java/com/petshop/backend/controller/MyPetController.java +++ b/backend/src/main/java/com/petshop/backend/controller/MyPetController.java @@ -38,8 +38,8 @@ public class MyPetController { } @GetMapping - public ResponseEntity> getMyPets() { - return ResponseEntity.ok(petService.getMyPets(currentUserId())); + public ResponseEntity> getMyPets(@RequestParam(required = false) String status) { + return ResponseEntity.ok(petService.getMyPets(currentUserId(), status)); } @PostMapping diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java b/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java index 8312ed38..287c1d6d 100644 --- a/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java @@ -1,6 +1,7 @@ package com.petshop.backend.dto.adoption; import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; public class CustomerAdoptionRequest { @@ -11,6 +12,9 @@ public class CustomerAdoptionRequest { private Long sourceStoreId; + @NotNull(message = "Appointment date is required") + private LocalDate adoptionDate; + public Long getPetId() { return petId; } @@ -34,4 +38,12 @@ public class CustomerAdoptionRequest { public void setSourceStoreId(Long sourceStoreId) { this.sourceStoreId = sourceStoreId; } + + public LocalDate getAdoptionDate() { + return adoptionDate; + } + + public void setAdoptionDate(LocalDate adoptionDate) { + this.adoptionDate = adoptionDate; + } } diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java index 7063b2f8..bea9c276 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java @@ -7,16 +7,18 @@ public class MyPetResponse { private String species; private String breed; private String imageUrl; + private String petStatus; public MyPetResponse() { } - public MyPetResponse(Long customerPetId, String petName, String species, String breed, String imageUrl) { + public MyPetResponse(Long customerPetId, String petName, String species, String breed, String imageUrl, String petStatus) { this.customerPetId = customerPetId; this.petName = petName; this.species = species; this.breed = breed; this.imageUrl = imageUrl; + this.petStatus = petStatus; } public Long getCustomerPetId() { @@ -58,4 +60,12 @@ public class MyPetResponse { public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } + + public String getPetStatus() { + return petStatus; + } + + public void setPetStatus(String petStatus) { + this.petStatus = petStatus; + } } 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 9da6efdd..b547c299 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -50,4 +50,6 @@ public interface AppointmentRepository extends JpaRepository @Query("SELECT a FROM Appointment a WHERE (a.appointmentDate < :currentDate OR (a.appointmentDate = :currentDate AND a.appointmentTime < :currentTime)) AND LOWER(a.appointmentStatus) = 'booked'") List findPastBookedAppointments(@Param("currentDate") LocalDate currentDate, @Param("currentTime") LocalTime currentTime); + + List findByPet_Id(Long petId); } diff --git a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java index b15d4a96..fae6deda 100644 --- a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java +++ b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java @@ -97,7 +97,7 @@ public class SecurityConfig { public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(List.of("http://localhost:*", "http://127.0.0.1:*")); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index c4a1ae03..155790c7 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -148,25 +148,30 @@ public class AdoptionService { } @Transactional - public AdoptionResponse requestAdoption(Long customerId, Long petId, Long employeeId, Long sourceStoreId) { + public AdoptionResponse requestAdoption(Long customerId, Long petId, Long employeeId, Long sourceStoreId, LocalDate adoptionDate) { Pet pet = petRepository.findById(petId) - .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId)); - User customer = userRepository.findById(customerId) - .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + customerId)); - User employee = resolveAdoptionEmployee(employeeId); - StoreLocation sourceStore = sourceStoreId != null - ? storeRepository.findById(sourceStoreId) - .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + sourceStoreId)) - : null; + .orElseThrow(() -> new ResourceNotFoundException("Pet not found")); + // Verify the pet is actually located at the claimed store + if (pet.getStore() == null || !pet.getStore().getStoreId().equals(sourceStoreId)) { + throw new IllegalArgumentException("The specified pet is not located at the selected store."); + } + + // Verify the pet is available for adoption validatePetAvailability(pet, null, null); + User customer = userRepository.findById(customerId) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found")); + User employee = resolveAdoptionEmployee(employeeId); + StoreLocation sourceStore = storeRepository.findById(sourceStoreId) + .orElseThrow(() -> new ResourceNotFoundException("Store not found")); + Adoption adoption = new Adoption(); adoption.setPet(pet); adoption.setCustomer(customer); adoption.setEmployee(employee); adoption.setSourceStore(sourceStore); - adoption.setAdoptionDate(null); + adoption.setAdoptionDate(adoptionDate); adoption.setAdoptionStatus(ADOPTION_STATUS_PENDING); adoption = adoptionRepository.save(adoption); @@ -174,6 +179,25 @@ public class AdoptionService { return mapToResponse(adoption); } + @Transactional + public AdoptionResponse cancelAdoption(Long adoptionId, Long requestingCustomerId) { + Adoption adoption = adoptionRepository.findById(adoptionId) + .orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + adoptionId)); + + if (requestingCustomerId != null && !adoption.getCustomer().getId().equals(requestingCustomerId)) { + throw new ResourceNotFoundException("Adoption not found"); + } + + if (!ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoption.getAdoptionStatus())) { + throw new IllegalArgumentException("Only pending adoptions can be cancelled"); + } + + adoption.setAdoptionStatus(ADOPTION_STATUS_CANCELLED); + adoption = adoptionRepository.save(adoption); + syncPetStatus(adoption.getPet(), ADOPTION_STATUS_CANCELLED, adoption.getAdoptionId(), adoption.getCustomer()); + return mapToResponse(adoption); + } + @Transactional public void deleteAdoption(Long id) { if (!adoptionRepository.existsById(id)) { 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 26e0a512..e974bf57 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -101,6 +101,22 @@ public class AppointmentService { Pet pet = request.getPetId() != null ? fetchPet(request.getPetId()) : null; User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); + // Customers must supply a pet that is Adopted and owned by them + if (User.Role.CUSTOMER.equals(authenticatedUser.getRole())) { + if (pet == null) { + throw new IllegalArgumentException("A pet must be selected for your appointment"); + } + if (pet.getOwner() == null || !pet.getOwner().getId().equals(authenticatedUser.getId())) { + throw new IllegalArgumentException("The selected pet does not belong to your account"); + } + String petStatus = pet.getPetStatus(); + if (!"Owned".equalsIgnoreCase(petStatus) && !"Adopted".equalsIgnoreCase(petStatus)) { + throw new IllegalArgumentException("Only your own pets can be booked for appointments"); + } + } + + validateSpeciesServiceCompatibility(pet, service); + validateStoreAccess(store.getStoreId(), authenticatedUser); validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null); @@ -155,6 +171,23 @@ public class AppointmentService { return mapToResponse(appointment); } + @Transactional + public AppointmentResponse cancelAppointment(Long appointmentId, Long requestingCustomerId) { + Appointment appointment = appointmentRepository.findById(appointmentId) + .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + appointmentId)); + + if (requestingCustomerId != null && !appointment.getCustomer().getId().equals(requestingCustomerId)) { + throw new ResourceNotFoundException("Appointment not found"); + } + + if (!"Booked".equalsIgnoreCase(appointment.getAppointmentStatus())) { + throw new IllegalArgumentException("Only booked appointments can be cancelled"); + } + + appointment.setAppointmentStatus("Cancelled"); + return mapToResponse(appointmentRepository.save(appointment)); + } + @Transactional public void deleteAppointment(Long id) { if (!appointmentRepository.existsById(id)) { @@ -313,6 +346,32 @@ public class AppointmentService { return true; } + private void validateSpeciesServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) { + if (pet == null || service == null) return; + String species = pet.getPetSpecies(); + if (species == null) return; + String serviceName = service.getServiceName().toLowerCase(); + + switch (species.toLowerCase()) { + case "bird": + if (!serviceName.contains("wing clipping") && !serviceName.contains("beak and nail")) { + throw new IllegalArgumentException( + "Service '" + service.getServiceName() + "' is not available for birds. " + + "Allowed services: Wing Clipping, Beak and Nail Care."); + } + break; + case "fish": + if (!serviceName.contains("aquarium health")) { + throw new IllegalArgumentException( + "Service '" + service.getServiceName() + "' is not available for fish. " + + "Allowed service: Aquarium Health Check."); + } + break; + default: + break; + } + } + private void validateStoreAccess(Long requestedStoreId, User user) { if (user.getRole() != User.Role.STAFF) { return; diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index ae414601..1fe51b19 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -12,6 +12,7 @@ import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.repository.AdoptionRepository; +import com.petshop.backend.repository.AppointmentRepository; import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; @@ -35,13 +36,15 @@ public class PetService { private final PetRepository petRepository; private final AdoptionRepository adoptionRepository; + private final AppointmentRepository appointmentRepository; private final UserRepository userRepository; private final StoreRepository storeRepository; private final CatalogImageStorageService catalogImageStorageService; - public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, UserRepository userRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) { + public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, AppointmentRepository appointmentRepository, UserRepository userRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) { this.petRepository = petRepository; this.adoptionRepository = adoptionRepository; + this.appointmentRepository = appointmentRepository; this.userRepository = userRepository; this.storeRepository = storeRepository; this.catalogImageStorageService = catalogImageStorageService; @@ -87,8 +90,9 @@ public class PetService { } @Transactional(readOnly = true) - public List getMyPets(Long ownerUserId) { + public List getMyPets(Long ownerUserId, String status) { return petRepository.findAllByOwner_IdOrderByPetNameAsc(ownerUserId).stream() + .filter(p -> status == null || status.isBlank() || status.equalsIgnoreCase(p.getPetStatus())) .map(this::mapToMyPetResponse) .toList(); } @@ -117,6 +121,18 @@ public class PetService { @Transactional public void deleteMyPet(Long ownerUserId, Long petId) { Pet pet = findOwnedPet(ownerUserId, petId); + List linkedAppointments = appointmentRepository.findByPet_Id(petId); + boolean hasBooked = linkedAppointments.stream() + .anyMatch(a -> "Booked".equalsIgnoreCase(a.getAppointmentStatus())); + if (hasBooked) { + throw new IllegalArgumentException( + "Your pet has a booked appointment. Please cancel the appointment before removing your pet from our database."); + } + // Nullify the pet reference on non-booked appointments to avoid FK constraint violations + for (com.petshop.backend.entity.Appointment appt : linkedAppointments) { + appt.setPet(null); + } + appointmentRepository.saveAll(linkedAppointments); deleteStoredImageIfPresent(pet.getImageUrl()); petRepository.delete(pet); } @@ -341,7 +357,8 @@ public class PetService { pet.getPetName(), pet.getPetSpecies(), pet.getPetBreed(), - pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null + pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null, + pet.getPetStatus() ); } diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index f6cd1b6c..67170c59 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -7,6 +7,34 @@ import { useAuth } from "@/context/AuthContext"; const API_BASE = ""; +const SPECIES_BREEDS = { + Dog: ["Beagle", "Boxer", "Bulldog", "Chihuahua", "Dachshund", "German Shepherd", "Golden Retriever", "Labrador Retriever", "Poodle", "Rottweiler", "Shih Tzu", "Siberian Husky", "Yorkshire Terrier", "Mixed / Other"], + Cat: ["Abyssinian", "Bengal", "British Shorthair", "Maine Coon", "Persian", "Ragdoll", "Scottish Fold", "Siamese", "Sphynx", "Mixed / Other"], + Bird: ["Canary", "Cockatiel", "Cockatoo", "Finch", "Lovebird", "Macaw", "Parakeet", "Parrot", "Other"], + Rabbit: ["Dutch", "Flemish Giant", "Holland Lop", "Lionhead", "Mini Rex", "Other"], + Hamster: ["Dwarf", "Roborovski", "Syrian", "Other"], + "Guinea Pig": ["Abyssinian", "American", "Peruvian", "Teddy", "Other"], + Reptile: ["Ball Python", "Bearded Dragon", "Blue-tongued Skink", "Corn Snake", "Leopard Gecko", "Other"], + Fish: ["Angelfish", "Betta", "Cichlid", "Clownfish", "Goldfish", "Guppy", "Tetra", "Other"], + Other: ["Other"], +}; + +// Explicit allowlists for species with restricted service availability. +// Species not listed here may use all services. +const SPECIES_SERVICE_ALLOWLIST = { + Bird: ["wing clipping", "beak and nail"], + Fish: ["aquarium health"], +}; + +function getAvailableServices(services, species) { + if (!species) return services; + const allowlist = SPECIES_SERVICE_ALLOWLIST[species]; + if (!allowlist) return services; + return services.filter((s) => + allowlist.some((kw) => s.serviceName.toLowerCase().includes(kw)) + ); +} + const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; @@ -254,26 +282,32 @@ function AddPetModal({ token, onClose, onAdded }) {
- {storeId && serviceId && appointmentDate && ( + {!adoptionMode && storeId && serviceId && appointmentDate && (
Available Time Slots {loadingSlots ? ( @@ -756,83 +930,62 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
)} - {!adoptionMode && serviceId && ( -
- {petSectionLabel} - {isCustomerPetService && ( - - )} - {petsToShow.length === 0 ? ( -

{noPetsMessage}

- ) : isAdoptionService ? ( -
- {petsToShow.map((p) => ( - - ))} -
- ) : ( -
- {petsToShow.map((p) => ( - - ))} -
- )} -
- )} - + + {success &&
{success}
} + + )} + + )} ) : null}
-

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

- {loadingAppointments ? ( +

+ {adoptionMode ? "Your Adoptions" : canBookAppointments ? "Your Appointments" : "Appointments"} +

+ {adoptionMode ? ( + loadingAdoptions ? ( +

Loading adoptions...

+ ) : adoptions.length === 0 ? ( +

No adoption appointments yet.

+ ) : ( +
+ {adoptions.map((a) => ( +
+
+ {a.petName} + + {a.adoptionStatus} + +
+
+ {a.sourceStoreName} + {a.adoptionDate} +
+ {a.adoptionStatus?.toLowerCase() === "pending" && ( +
+ +
+ )} +
+ ))} +
+ ) + ) : loadingAppointments ? (

Loading appointments...

) : appointments.length === 0 ? (

No appointments yet.

@@ -860,6 +1013,18 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; Pets: {a.customerPetNames.join(", ")}
)} + {a.appointmentStatus?.toLowerCase() === "booked" && ( +
+ +
+ )}

))}
diff --git a/web/app/globals.css b/web/app/globals.css index 0b263f02..4932c2b5 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -1627,6 +1627,11 @@ body { color: #c62828; } +.appt-card-status--pending { + background: #fff8e1; + color: #f57f17; +} + .appt-card-details { display: flex; justify-content: space-between; @@ -1640,6 +1645,34 @@ body { margin-top: 0.35rem; } +.appt-card-actions { + display: flex; + justify-content: flex-end; + margin-top: 0.6rem; +} + +.appt-cancel-btn { + font-size: 0.8rem; + font-weight: 600; + padding: 0.25rem 0.85rem; + border-radius: 6px; + border: 1px solid #e53935; + background: transparent; + color: #e53935; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.appt-cancel-btn:hover:not(:disabled) { + background: #e53935; + color: #fff; +} + +.appt-cancel-btn:disabled { + opacity: 0.5; + cursor: default; +} + /* Adoption Pet Selection */ .appt-adopt-grid { diff --git a/web/app/profile/page.js b/web/app/profile/page.js index cf2f0d2d..6920ad6a 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -6,6 +6,18 @@ import { useAuth } from "@/context/AuthContext"; const API_BASE = ""; +const SPECIES_BREEDS = { + Dog: ["Beagle", "Boxer", "Bulldog", "Chihuahua", "Dachshund", "German Shepherd", "Golden Retriever", "Labrador Retriever", "Poodle", "Rottweiler", "Shih Tzu", "Siberian Husky", "Yorkshire Terrier", "Mixed / Other"], + Cat: ["Abyssinian", "Bengal", "British Shorthair", "Maine Coon", "Persian", "Ragdoll", "Scottish Fold", "Siamese", "Sphynx", "Mixed / Other"], + Bird: ["Canary", "Cockatiel", "Cockatoo", "Finch", "Lovebird", "Macaw", "Parakeet", "Parrot", "Other"], + Rabbit: ["Dutch", "Flemish Giant", "Holland Lop", "Lionhead", "Mini Rex", "Other"], + Hamster: ["Dwarf", "Roborovski", "Syrian", "Other"], + "Guinea Pig": ["Abyssinian", "American", "Peruvian", "Teddy", "Other"], + Reptile: ["Ball Python", "Bearded Dragon", "Blue-tongued Skink", "Corn Snake", "Leopard Gecko", "Other"], + Fish: ["Angelfish", "Betta", "Cichlid", "Clownfish", "Goldfish", "Guppy", "Tetra", "Other"], + Other: ["Other"], +}; + export default function ProfilePage() { const {user, token, loading, logout, refreshUser} = useAuth(); const router = useRouter(); @@ -53,7 +65,7 @@ export default function ProfilePage() { setLoadingPets(true); try { - const response = await fetch(`${API_BASE}/api/v1/my-pets`, { + const response = await fetch(`${API_BASE}/api/v1/my-pets?status=Owned`, { headers: { Authorization: `Bearer ${token}` }, }); @@ -64,25 +76,21 @@ export default function ProfilePage() { const petData = await response.json(); clearPetImageObjectUrls(); - const petsWithResolvedImages = await Promise.all( - (Array.isArray(petData) ? petData : []).map(async (pet) => { - if (!pet.imageUrl) { - return pet; - } + const ownedPets = Array.isArray(petData) ? petData : []; + const petsWithResolvedImages = await Promise.all( + ownedPets.map(async (pet) => { + if (!pet.imageUrl) return pet; try { const imageResponse = await fetch(`${API_BASE}${pet.imageUrl}`, { headers: { Authorization: `Bearer ${token}` }, }); - if (!imageResponse.ok) { - return { ...pet, imageUrl: null }; - } + if (!imageResponse.ok) return { ...pet, imageUrl: null }; const blob = await imageResponse.blob(); const objectUrl = URL.createObjectURL(blob); petImageObjectUrlsRef.current.push(objectUrl); - return { ...pet, imageUrl: objectUrl }; } catch { return { ...pet, imageUrl: null }; @@ -284,14 +292,19 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { } try { - await fetch(`${API_BASE}/api/v1/my-pets/${id}`, { + const res = await fetch(`${API_BASE}/api/v1/my-pets/${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }); + if (!res.ok) { + const data = await res.json().catch(() => null); + throw new Error(data?.message || `Failed to remove pet (${res.status})`); + } loadPets(); } - - catch { + + catch (err) { + alert(err.message); } } @@ -445,26 +458,32 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
- - -
- - ))} + + + + + ))} )} From 2d27f95f7d97525d05c34c9041aa716af4e9e604 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Tue, 14 Apr 2026 13:31:56 -0600 Subject: [PATCH 04/14] Removed addresses, adjusted contact page --- .../backend/controller/StoreController.java | 2 - .../backend/security/SecurityConfig.java | 1 + web/app/contact/page.js | 97 ++++++++++--------- web/app/globals.css | 35 ++++++- web/components/Footer.js | 1 - 5 files changed, 86 insertions(+), 50 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/StoreController.java b/backend/src/main/java/com/petshop/backend/controller/StoreController.java index 0c4a70c0..7dfb2b01 100644 --- a/backend/src/main/java/com/petshop/backend/controller/StoreController.java +++ b/backend/src/main/java/com/petshop/backend/controller/StoreController.java @@ -23,7 +23,6 @@ public class StoreController { } @GetMapping - @PreAuthorize("isAuthenticated()") public ResponseEntity> getAllStores( @RequestParam(required = false) String q, Pageable pageable) { @@ -31,7 +30,6 @@ public class StoreController { } @GetMapping("/{id}") - @PreAuthorize("isAuthenticated()") public ResponseEntity getStoreById(@PathVariable Long id) { return ResponseEntity.ok(storeService.getStoreById(id)); } diff --git a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java index fae6deda..d5a38e47 100644 --- a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java +++ b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java @@ -63,6 +63,7 @@ public class SecurityConfig { .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/stores/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/pet-species").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/pet-breeds").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/stores").permitAll() diff --git a/web/app/contact/page.js b/web/app/contact/page.js index 85fba175..19295ebd 100644 --- a/web/app/contact/page.js +++ b/web/app/contact/page.js @@ -1,31 +1,24 @@ -const LOCATIONS = [ - { - name: "Downtown Branch", - address: "123 Main St", - phone: "(123) 456-7890", - email: "downtown@petshop.com", - }, - { - name: "North Branch", - address: "456 North Ave", - phone: "(987) 654-3210", - email: "north@petshop.com", - }, - { - name: "West Side Store", - address: "789 West Blvd", - phone: "(555) 123-4567", - email: "westside@petshop.com", - }, -]; +"use client"; -const PERSONNEL = [ - { name: "John Doe", role: "Store Manager" }, - { name: "Sara Smith", role: "Staff" }, - { name: "Michael Johnson", role: "Grooming Team" }, -]; +import { useState, useEffect } from "react"; export default function ContactPage() { + const [locations, setLocations] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const params = new URLSearchParams({ page: "0", size: "100", sort: "storeName,asc" }); + fetch(`/api/v1/stores?${params}`) + .then((res) => { + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); + }) + .then((data) => setLocations(data.content ?? [])) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)); + }, []); + return (
@@ -44,28 +37,40 @@ export default function ContactPage() {

Store Locations

-
- {LOCATIONS.map((location) => ( -
-

{location.name}

-

{location.address}

-

{location.phone}

-

{location.email}

-
- ))} -
-
-
-

Store Personnel

-
- {PERSONNEL.map((person) => ( -
-

{person.name}

-

{person.role}

-
- ))} -
+ {loading &&

Loading locations...

} + + {error &&

Failed to load locations: {error}

} + + {!loading && !error && locations.length === 0 && ( +

No store locations found.

+ )} + + {!loading && !error && locations.length > 0 && ( +
+ {locations.map((location) => ( +
+
+ {location.storeName} { + e.currentTarget.onerror = null; + e.currentTarget.src = "/images/pet-placeholder.png"; + }} + /> +
+
+

{location.storeName}

+

{location.address}

+

{location.phone}

+

{location.email}

+
+
+ ))} +
+ )}
diff --git a/web/app/globals.css b/web/app/globals.css index 4932c2b5..4f0ee3ee 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -38,7 +38,7 @@ body { align-items: center; flex-wrap: wrap; min-height: 70px; - border-radius: 0px 0px 10px 10px; + /* border-radius: 0px 0px 10px 10px; */ } /* Add padding to body to account for fixed header */ @@ -758,6 +758,39 @@ body { margin-bottom: 0.5rem; } +.location-card { + padding: 0; + overflow: hidden; +} + +.location-card-image-wrapper { + width: 100%; + aspect-ratio: 16 / 9; + overflow: hidden; + background: #eee; +} + +.location-card-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.location-card-body { + padding: 1rem; +} + +.location-card-body h3 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +.location-card-body p { + margin: 0.25rem 0; + font-size: 0.9rem; + color: #555; +} + .products-hero { text-align: center; padding: 4rem 2rem 3rem; diff --git a/web/components/Footer.js b/web/components/Footer.js index 0ef31e65..47976ce5 100644 --- a/web/components/Footer.js +++ b/web/components/Footer.js @@ -43,7 +43,6 @@ export default function Footer() {
  • (403) 123-4567
  • support@leonspetstore.com
  • -
  • 123 Street Street, Calgary, Alberta, Canada
From 5dbddfdc1f90efad2261e662dcf6ebcdb05ddca0 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Tue, 14 Apr 2026 13:44:13 -0600 Subject: [PATCH 05/14] Navbar fixed --- web/app/globals.css | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/web/app/globals.css b/web/app/globals.css index 4f0ee3ee..2dbbe14a 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -62,11 +62,9 @@ body { .nav-links { display: flex; align-items: center; - gap: 2rem; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); + gap: 1.25rem; + flex: 1; + justify-content: center; } /* Indivdual Link Styles */ @@ -2650,7 +2648,24 @@ body { /* Mobile / Responsive */ -@media (max-width: 768px) { +/* Compact nav at mid-range widths before collapsing to hamburger */ +@media (min-width: 1101px) and (max-width: 1350px) { + .nav-links { + gap: 0.25rem; + } + + .nav-link { + font-size: 0.9rem; + padding: 0.4rem 0.5rem; + } + + .nav-auth { + gap: 0.35rem; + padding-left: 0.5rem; + } +} + +@media (max-width: 1100px) { .navbar { padding: 0.5rem 1rem; } @@ -2706,7 +2721,7 @@ body { display: none; } -@media (max-width: 768px) { +@media (max-width: 1100px) { /* Show hamburger bar, hide desktop nav */ .nav-mobile-bar { display: flex; From c5448b95c96dcaabd95de469ac3173d19e7a703a Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Tue, 14 Apr 2026 13:56:21 -0600 Subject: [PATCH 06/14] Age input when editing/adding pet profile --- .../petshop/backend/dto/pet/MyPetRequest.java | 14 ++++++++++++++ .../backend/dto/pet/MyPetResponse.java | 12 +++++++++++- .../petshop/backend/service/PetService.java | 3 ++- web/app/profile/page.js | 19 ++++++++++++++++++- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java index 17d08e2f..37942a47 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java @@ -1,5 +1,7 @@ package com.petshop.backend.dto.pet; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @@ -16,6 +18,10 @@ public class MyPetRequest { @Size(max = 50, message = "Breed must not exceed 50 characters") private String breed; + @Min(value = 0, message = "Age must be 0 or greater") + @Max(value = 100, message = "Age must not exceed 100") + private Integer petAge; + public String getPetName() { return petName; } @@ -39,4 +45,12 @@ public class MyPetRequest { public void setBreed(String breed) { this.breed = breed; } + + public Integer getPetAge() { + return petAge; + } + + public void setPetAge(Integer petAge) { + this.petAge = petAge; + } } diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java index bea9c276..15117334 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java @@ -6,17 +6,19 @@ public class MyPetResponse { private String petName; private String species; private String breed; + private Integer petAge; private String imageUrl; private String petStatus; public MyPetResponse() { } - public MyPetResponse(Long customerPetId, String petName, String species, String breed, String imageUrl, String petStatus) { + public MyPetResponse(Long customerPetId, String petName, String species, String breed, Integer petAge, String imageUrl, String petStatus) { this.customerPetId = customerPetId; this.petName = petName; this.species = species; this.breed = breed; + this.petAge = petAge; this.imageUrl = imageUrl; this.petStatus = petStatus; } @@ -53,6 +55,14 @@ public class MyPetResponse { this.breed = breed; } + public Integer getPetAge() { + return petAge; + } + + public void setPetAge(Integer petAge) { + this.petAge = petAge; + } + public String getImageUrl() { return imageUrl; } diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index 1fe51b19..dd86eb4a 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -357,6 +357,7 @@ public class PetService { pet.getPetName(), pet.getPetSpecies(), pet.getPetBreed(), + pet.getPetAge(), pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null, pet.getPetStatus() ); @@ -366,7 +367,7 @@ public class PetService { pet.setPetName(request.getPetName().trim()); pet.setPetSpecies(request.getSpecies().trim()); pet.setPetBreed(normalizeOptional(request.getBreed())); - pet.setPetAge(null); + pet.setPetAge(request.getPetAge()); pet.setPetPrice(null); } diff --git a/web/app/profile/page.js b/web/app/profile/page.js index 6920ad6a..1b251593 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -30,6 +30,7 @@ export default function ProfilePage() { const [petName, setPetName] = useState(""); const [species, setSpecies] = useState(""); const [breed, setBreed] = useState(""); + const [petAge, setPetAge] = useState("1"); const [submitting, setSubmitting] = useState(false); const [petError, setPetError] = useState(null); const [profileForm, setProfileForm] = useState({ fullName: "", email: "", phone: "" }); @@ -230,6 +231,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { setPetName(""); setSpecies(""); setBreed(""); + setPetAge("1"); setPetError(null); setShowForm(true); } @@ -239,6 +241,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { setPetName(pet.petName); setSpecies(pet.species); setBreed(pet.breed || ""); + setPetAge(pet.petAge != null ? String(pet.petAge) : "1"); setPetError(null); setShowForm(true); } @@ -265,7 +268,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ petName, species, breed: breed || null }), + body: JSON.stringify({ petName, species, breed: breed || null, petAge: Number(petAge) }), }); if (!res.ok) { @@ -485,6 +488,19 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { ))} +
From 2282f0da6f83b57a5b01de3c23e6c4cc056780be Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Tue, 14 Apr 2026 15:02:57 -0600 Subject: [PATCH 07/14] Profile image works, editing profile works, now uses first/last name --- .../backend/controller/AuthController.java | 26 ++-- .../dto/auth/ProfileUpdateRequest.java | 31 +++-- .../backend/dto/auth/RegisterRequest.java | 34 ++++-- .../backend/dto/auth/UserInfoResponse.java | 22 +++- web/app/profile/page.js | 114 +++++++++++++++--- web/app/register/page.js | 28 ++++- web/context/AuthContext.js | 10 +- 7 files changed, 212 insertions(+), 53 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index 33281560..522c5ad8 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -70,7 +70,8 @@ public class AuthController { public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { String username = trimToNull(request.getUsername()); String email = trimToNull(request.getEmail()); - NameParts nameParts = splitFullName(request.getFullName()); + String firstName = trimToNull(request.getFirstName()); + String lastName = trimToNull(request.getLastName()); String phone = normalizePhone(request.getPhone()); if (userRepository.findByUsername(username).isPresent()) { @@ -98,9 +99,9 @@ public class AuthController { user.setUsername(username); user.setPassword(passwordEncoder.encode(request.getPassword())); user.setEmail(email); - user.setFirstName(nameParts.firstName()); - user.setLastName(nameParts.lastName()); - user.setFullName(nameParts.fullName()); + user.setFirstName(firstName); + user.setLastName(lastName); + user.setFullName(joinFullName(firstName, lastName)); user.setPhone(phone); user.setRole(User.Role.CUSTOMER); user.setActive(true); @@ -203,11 +204,16 @@ public class AuthController { user.setEmail(email); } - if (request.getFullName() != null) { - NameParts nameParts = splitFullName(request.getFullName()); - user.setFirstName(nameParts.firstName()); - user.setLastName(nameParts.lastName()); - user.setFullName(nameParts.fullName()); + String firstName = trimToNull(request.getFirstName()); + if (firstName != null) { + user.setFirstName(firstName); + } + String lastName = trimToNull(request.getLastName()); + if (lastName != null) { + user.setLastName(lastName); + } + if (firstName != null || lastName != null) { + user.setFullName(joinFullName(user.getFirstName(), user.getLastName())); } if (request.getPhone() != null) { @@ -247,6 +253,8 @@ public class AuthController { return new UserInfoResponse( user.getId(), user.getUsername(), + user.getFirstName(), + user.getLastName(), user.getEmail(), fullName, user.getPhone(), diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java b/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java index 58959678..dc1c98c0 100644 --- a/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java @@ -11,8 +11,11 @@ public class ProfileUpdateRequest { @Email(message = "Email must be valid") private String email; - @Size(max = 100, message = "Full name must not exceed 100 characters") - private String fullName; + @Size(max = 50, message = "First name must not exceed 50 characters") + private String firstName; + + @Size(max = 50, message = "Last name must not exceed 50 characters") + private String lastName; @Size(max = 20, message = "Phone must not exceed 20 characters") private String phone; @@ -36,12 +39,20 @@ public class ProfileUpdateRequest { this.email = email; } - public String getFullName() { - return fullName; + public String getFirstName() { + return firstName; } - public void setFullName(String fullName) { - this.fullName = fullName; + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; } public String getPhone() { @@ -67,14 +78,15 @@ public class ProfileUpdateRequest { ProfileUpdateRequest that = (ProfileUpdateRequest) o; return Objects.equals(username, that.username) && Objects.equals(email, that.email) && - Objects.equals(fullName, that.fullName) && + Objects.equals(firstName, that.firstName) && + Objects.equals(lastName, that.lastName) && Objects.equals(phone, that.phone) && Objects.equals(password, that.password); } @Override public int hashCode() { - return Objects.hash(username, email, fullName, phone, password); + return Objects.hash(username, email, firstName, lastName, phone, password); } @Override @@ -82,7 +94,8 @@ public class ProfileUpdateRequest { return "ProfileUpdateRequest{" + "username='" + username + '\'' + ", email='" + email + '\'' + - ", fullName='" + fullName + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + ", phone='" + phone + '\'' + ", password='" + password + '\'' + '}'; diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java b/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java index 2791746c..4758d923 100644 --- a/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java @@ -18,9 +18,13 @@ public class RegisterRequest { @Email(message = "Email must be valid") private String email; - @NotBlank(message = "Full name is required") - @Size(max = 100, message = "Full name must not exceed 100 characters") - private String fullName; + @NotBlank(message = "First name is required") + @Size(max = 50, message = "First name must not exceed 50 characters") + private String firstName; + + @NotBlank(message = "Last name is required") + @Size(max = 50, message = "Last name must not exceed 50 characters") + private String lastName; @NotBlank(message = "Phone is required") @Size(max = 20, message = "Phone must not exceed 20 characters") @@ -50,12 +54,20 @@ public class RegisterRequest { this.email = email; } - public String getFullName() { - return fullName; + public String getFirstName() { + return firstName; } - public void setFullName(String fullName) { - this.fullName = fullName; + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; } public String getPhone() { @@ -74,13 +86,14 @@ public class RegisterRequest { return Objects.equals(username, that.username) && Objects.equals(password, that.password) && Objects.equals(email, that.email) && - Objects.equals(fullName, that.fullName) && + Objects.equals(firstName, that.firstName) && + Objects.equals(lastName, that.lastName) && Objects.equals(phone, that.phone); } @Override public int hashCode() { - return Objects.hash(username, password, email, fullName, phone); + return Objects.hash(username, password, email, firstName, lastName, phone); } @Override @@ -89,7 +102,8 @@ public class RegisterRequest { "username='" + username + '\'' + ", password='" + password + '\'' + ", email='" + email + '\'' + - ", fullName='" + fullName + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + ", phone='" + phone + '\'' + '}'; } diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java b/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java index 84372638..e88c272d 100644 --- a/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java @@ -5,6 +5,8 @@ import java.util.Objects; public class UserInfoResponse { private Long id; private String username; + private String firstName; + private String lastName; private String email; private String fullName; private String phone; @@ -17,9 +19,11 @@ public class UserInfoResponse { public UserInfoResponse() { } - public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) { + public UserInfoResponse(Long id, String username, String firstName, String lastName, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) { this.id = id; this.username = username; + this.firstName = firstName; + this.lastName = lastName; this.email = email; this.fullName = fullName; this.phone = phone; @@ -46,6 +50,22 @@ public class UserInfoResponse { this.username = username; } + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getEmail() { return email; } diff --git a/web/app/profile/page.js b/web/app/profile/page.js index 1b251593..ddb23d62 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -33,7 +33,8 @@ export default function ProfilePage() { const [petAge, setPetAge] = useState("1"); const [submitting, setSubmitting] = useState(false); const [petError, setPetError] = useState(null); - const [profileForm, setProfileForm] = useState({ fullName: "", email: "", phone: "" }); + const [avatarObjectUrl, setAvatarObjectUrl] = useState(null); + const [profileForm, setProfileForm] = useState({ firstName: "", lastName: "", email: "", phone: "", password: "", confirmPassword: "" }); const [profileSubmitting, setProfileSubmitting] = useState(false); const [profileError, setProfileError] = useState(null); const [profileSuccess, setProfileSuccess] = useState(null); @@ -55,9 +56,12 @@ export default function ProfilePage() { useEffect(() => { setProfileForm({ - fullName: user?.fullName || "", + firstName: user?.firstName || "", + lastName: user?.lastName || "", email: user?.email || "", phone: user?.phone || "", + password: "", + confirmPassword: "", }); }, [user]); @@ -117,11 +121,37 @@ export default function ProfilePage() { }, [clearPetImageObjectUrls]); useEffect(() => { -if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { + if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { loadPets(); } }, [user, loadPets]); + useEffect(() => { + let objectUrl = null; + + if (user?.avatarUrl && token) { + fetch(`${API_BASE}${user.avatarUrl}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((res) => (res.ok ? res.blob() : null)) + .then((blob) => { + if (blob) { + objectUrl = URL.createObjectURL(blob); + setAvatarObjectUrl(objectUrl); + } else { + setAvatarObjectUrl(null); + } + }) + .catch(() => setAvatarObjectUrl(null)); + } else { + setAvatarObjectUrl(null); + } + + return () => { + if (objectUrl) URL.revokeObjectURL(objectUrl); + }; + }, [user?.avatarUrl, token]); + function handleLogout() { logout(); router.push("/"); @@ -129,10 +159,26 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { async function handleProfileSubmit(e) { e.preventDefault(); - setProfileSubmitting(true); setProfileError(null); + + if (profileForm.password && profileForm.password !== profileForm.confirmPassword) { + setProfileError("Passwords do not match."); + return; + } + + setProfileSubmitting(true); setProfileSuccess(null); + const payload = { + firstName: profileForm.firstName, + lastName: profileForm.lastName, + email: profileForm.email, + phone: profileForm.phone, + }; + if (profileForm.password) { + payload.password = profileForm.password; + } + try { const res = await fetch(`${API_BASE}/api/v1/auth/me`, { method: "PUT", @@ -140,7 +186,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify(profileForm), + body: JSON.stringify(payload), }); const data = await res.json().catch(() => null); @@ -149,6 +195,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { } await refreshUser(); + setProfileForm((prev) => ({ ...prev, password: "", confirmPassword: "" })); setProfileSuccess("Profile updated successfully."); } @@ -340,12 +387,14 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { return

Loading…

; } + const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || user.username; + const fields = [ - {label: "Full Name", value: user.fullName}, + {label: "First Name", value: user.firstName || "N/A"}, + {label: "Last Name", value: user.lastName || "N/A"}, {label: "Username", value: user.username}, {label: "Email", value: user.email}, - {label: "Phone", value: user.phone || "—"}, - {label: "Role", value: user.role}, + {label: "Phone", value: user.phone || "N/A"}, ...(user.storeName ? [{ label: "Store", value: user.storeName }] : []), ]; @@ -353,14 +402,14 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
- {user.avatarUrl ? ( - {user.fullName + {avatarObjectUrl ? ( + {displayName} ) : ( - (user.fullName || user.username).charAt(0).toUpperCase() + displayName.charAt(0).toUpperCase() )}
-

{user.fullName || user.username}

+

{displayName}

{user.role}
@@ -377,13 +426,23 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") { {profileError &&
{profileError}
} {profileSuccess &&
{profileSuccess}
} + + +