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") {
- - -
- - ))} + + + + + ))} )}