diff --git a/backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java b/backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java new file mode 100644 index 00000000..36b78fb4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java @@ -0,0 +1,34 @@ +package com.petshop.backend.config; + +import com.petshop.backend.repository.CustomerPetRepository; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; + +@Component +@Profile("local") +public class LocalAppointmentCustomerSeedInitializer implements CommandLineRunner { + + private final DataSource dataSource; + private final CustomerPetRepository customerPetRepository; + + public LocalAppointmentCustomerSeedInitializer(DataSource dataSource, CustomerPetRepository customerPetRepository) { + this.dataSource = dataSource; + this.customerPetRepository = customerPetRepository; + } + + @Override + public void run(String... args) { + if (customerPetRepository.count() > 0) { + return; + } + + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(false, false, "UTF-8", + new ClassPathResource("dev/seed_demo_customer_pets.sql")); + populator.execute(dataSource); + } +} 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 a3f67002..41a2e815 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -71,21 +71,8 @@ public class AdoptionController { } @PostMapping - @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity createAdoption(@Valid @RequestBody AdoptionRequest request) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - if (role != null && role.equals("CUSTOMER")) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - if (!request.getCustomerId().equals(customer.getCustomerId())) { - throw new org.springframework.security.access.AccessDeniedException("You can only create adoptions for yourself"); - } - } - return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request)); } diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index 5c56862d..409891bc 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -57,6 +57,16 @@ public class DropdownController { ); } + @GetMapping("/adoption-pets") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity> getAdoptionPets() { + return ResponseEntity.ok( + petRepository.findAllByPetStatusIgnoreCaseOrderByPetNameAsc("Available").stream() + .map(p -> new DropdownOption(p.getPetId(), p.getPetName())) + .collect(Collectors.toList()) + ); + } + @GetMapping("/customers") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getCustomers() { @@ -67,6 +77,16 @@ public class DropdownController { ); } + @GetMapping("/appointment-customers") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity> getAppointmentCustomers() { + return ResponseEntity.ok( + customerRepository.findAllWithPets().stream() + .map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName())) + .collect(Collectors.toList()) + ); + } + @GetMapping("/customers/{customerId}/pets") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getCustomerPets(@PathVariable Long customerId) { @@ -174,8 +194,8 @@ public class DropdownController { return false; } return userRepository.findById(userId) - .map(User::getRole) - .filter(role -> role == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } } diff --git a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java index 2af9c52f..7b632f7f 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java @@ -28,4 +28,8 @@ public interface AdoptionRepository extends JpaRepository { Page searchAdoptionsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); Optional findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(Long petId, String adoptionStatus); + + boolean existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(Long petId, String adoptionStatus, Long adoptionId); + + boolean existsByPetPetIdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus); } diff --git a/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java b/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java index 56e03dbc..2c860de7 100644 --- a/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java @@ -16,6 +16,9 @@ public interface CustomerRepository extends JpaRepository { Optional findByUserId(Long userId); List findAllByEmail(String email); + + @Query("SELECT DISTINCT c FROM Customer c WHERE EXISTS (SELECT cp FROM CustomerPet cp WHERE cp.customer = c) ORDER BY c.firstName ASC, c.lastName ASC") + List findAllWithPets(); @Query("SELECT c FROM Customer c WHERE " + "LOWER(c.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + 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 d974deb3..468c0c9d 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -8,9 +8,13 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface PetRepository extends JpaRepository { + List findAllByPetStatusIgnoreCaseOrderByPetNameAsc(String petStatus); + @Query("SELECT p FROM Pet p WHERE " + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + 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 4bda85ab..c6ff1c60 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -22,6 +22,12 @@ import org.springframework.transaction.annotation.Transactional; @Service public class AdoptionService { + private static final String ADOPTION_STATUS_PENDING = "Pending"; + private static final String ADOPTION_STATUS_COMPLETED = "Completed"; + private static final String ADOPTION_STATUS_CANCELLED = "Cancelled"; + private static final String PET_STATUS_AVAILABLE = "Available"; + private static final String PET_STATUS_ADOPTED = "Adopted"; + private final AdoptionRepository adoptionRepository; private final PetRepository petRepository; private final CustomerRepository customerRepository; @@ -75,15 +81,18 @@ public class AdoptionService { Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); Employee employee = resolveAdoptionEmployee(request.getEmployeeId()); + String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus()); + validatePetAvailability(pet, null); Adoption adoption = new Adoption(); adoption.setPet(pet); adoption.setCustomer(customer); adoption.setEmployee(employee); adoption.setAdoptionDate(request.getAdoptionDate()); - adoption.setAdoptionStatus(request.getAdoptionStatus()); + adoption.setAdoptionStatus(adoptionStatus); adoption = adoptionRepository.save(adoption); + syncPetStatus(pet, adoptionStatus, adoption.getAdoptionId()); return mapToResponse(adoption); } @@ -98,14 +107,17 @@ public class AdoptionService { Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); Employee employee = resolveAdoptionEmployee(request.getEmployeeId()); + String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus()); + validatePetAvailability(pet, adoption.getAdoptionId()); adoption.setPet(pet); adoption.setCustomer(customer); adoption.setEmployee(employee); adoption.setAdoptionDate(request.getAdoptionDate()); - adoption.setAdoptionStatus(request.getAdoptionStatus()); + adoption.setAdoptionStatus(adoptionStatus); adoption = adoptionRepository.save(adoption); + syncPetStatus(pet, adoptionStatus, adoption.getAdoptionId()); return mapToResponse(adoption); } @@ -161,8 +173,49 @@ public class AdoptionService { return false; } return userRepository.findById(userId) - .map(User::getRole) - .filter(role -> role == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } + + private String normalizeAdoptionStatus(String adoptionStatus) { + if (adoptionStatus == null) { + throw new IllegalArgumentException("Adoption status is required"); + } + String trimmedStatus = adoptionStatus.trim(); + if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(trimmedStatus)) { + return ADOPTION_STATUS_PENDING; + } + if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(trimmedStatus)) { + return ADOPTION_STATUS_COMPLETED; + } + if (ADOPTION_STATUS_CANCELLED.equalsIgnoreCase(trimmedStatus)) { + return ADOPTION_STATUS_CANCELLED; + } + throw new IllegalArgumentException("Adoption status must be Pending, Completed, or Cancelled"); + } + + private void validatePetAvailability(Pet pet, Long adoptionId) { + boolean adoptedElsewhere = adoptionId == null + ? adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED) + : adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); + if (adoptedElsewhere) { + throw new IllegalArgumentException("Selected pet has already been adopted"); + } + + if (!PET_STATUS_AVAILABLE.equalsIgnoreCase(pet.getPetStatus()) && adoptionId == null) { + throw new IllegalArgumentException("Selected pet is not available for adoption"); + } + } + + private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId) { + boolean completedElsewhere = adoptionId != null + && adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); + if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) || completedElsewhere) { + pet.setPetStatus(PET_STATUS_ADOPTED); + } else { + pet.setPetStatus(PET_STATUS_AVAILABLE); + } + petRepository.save(pet); + } } 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 0b277889..3b3d121d 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -333,8 +333,8 @@ public class AppointmentService { return false; } return userRepository.findById(userId) - .map(User::getRole) - .filter(role -> role == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } diff --git a/backend/src/main/resources/dev/seed_demo_customer_pets.sql b/backend/src/main/resources/dev/seed_demo_customer_pets.sql new file mode 100644 index 00000000..18a855ab --- /dev/null +++ b/backend/src/main/resources/dev/seed_demo_customer_pets.sql @@ -0,0 +1,53 @@ +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Rocky', 'dog', 'Labrador', NULL +FROM customer c +WHERE c.email = 'alex@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Rocky' + ); + +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Whiskers', 'cat', 'Persian', NULL +FROM customer c +WHERE c.email = 'emily@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Whiskers' + ); + +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Daisy', 'dog', 'Beagle', NULL +FROM customer c +WHERE c.email = 'james@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Daisy' + ); + +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Pepper', 'cat', 'Domestic Shorthair', NULL +FROM customer c +WHERE c.email = 'olivia@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Pepper' + ); + +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Cooper', 'dog', 'Golden Retriever', NULL +FROM customer c +WHERE c.email = 'william@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Cooper' + ); + +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Mittens', 'cat', 'Siamese', NULL +FROM customer c +WHERE c.email = 'sophia@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Mittens' + ); diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java index 52b64692..b7d97e74 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -2,6 +2,7 @@ package com.petshop.backend.controller; import com.petshop.backend.entity.Employee; import com.petshop.backend.entity.EmployeeStore; +import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.repository.CategoryRepository; @@ -71,10 +72,12 @@ class DropdownControllerTest { User staffUser = new User(); staffUser.setId(7L); staffUser.setRole(User.Role.STAFF); + staffUser.setActive(true); User adminUser = new User(); adminUser.setId(8L); adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(staffEmployee, store), new EmployeeStore(adminEmployee, store))); @@ -87,4 +90,99 @@ class DropdownControllerTest { assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); assertEquals("Alex Jones", response.getBody().get(0).getLabel()); } + + @Test + void getStoreEmployeesExcludesInactiveStaffUsers() { + PetRepository petRepository = mock(PetRepository.class); + CustomerRepository customerRepository = mock(CustomerRepository.class); + CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); + ServiceRepository serviceRepository = mock(ServiceRepository.class); + ProductRepository productRepository = mock(ProductRepository.class); + CategoryRepository categoryRepository = mock(CategoryRepository.class); + StoreRepository storeRepository = mock(StoreRepository.class); + SupplierRepository supplierRepository = mock(SupplierRepository.class); + EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); + UserRepository userRepository = mock(UserRepository.class); + + DropdownController controller = new DropdownController( + petRepository, + customerRepository, + customerPetRepository, + serviceRepository, + productRepository, + categoryRepository, + storeRepository, + supplierRepository, + employeeStoreRepository, + userRepository + ); + + StoreLocation store = new StoreLocation(); + store.setStoreId(1L); + + Employee inactiveStaffEmployee = new Employee(); + inactiveStaffEmployee.setEmployeeId(7L); + inactiveStaffEmployee.setUserId(7L); + inactiveStaffEmployee.setFirstName("Alex"); + inactiveStaffEmployee.setLastName("Jones"); + inactiveStaffEmployee.setIsActive(true); + + User inactiveStaffUser = new User(); + inactiveStaffUser.setId(7L); + inactiveStaffUser.setRole(User.Role.STAFF); + inactiveStaffUser.setActive(false); + + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(inactiveStaffEmployee, store))); + when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser)); + + var response = controller.getStoreEmployees(1L); + + assertEquals(0, response.getBody().size()); + } + + @Test + void getAppointmentCustomersReturnsOnlyCustomersWithPets() { + PetRepository petRepository = mock(PetRepository.class); + CustomerRepository customerRepository = mock(CustomerRepository.class); + CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); + ServiceRepository serviceRepository = mock(ServiceRepository.class); + ProductRepository productRepository = mock(ProductRepository.class); + CategoryRepository categoryRepository = mock(CategoryRepository.class); + StoreRepository storeRepository = mock(StoreRepository.class); + SupplierRepository supplierRepository = mock(SupplierRepository.class); + EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); + UserRepository userRepository = mock(UserRepository.class); + + DropdownController controller = new DropdownController( + petRepository, + customerRepository, + customerPetRepository, + serviceRepository, + productRepository, + categoryRepository, + storeRepository, + supplierRepository, + employeeStoreRepository, + userRepository + ); + + Customer one = new Customer(); + one.setCustomerId(1L); + one.setFirstName("Alex"); + one.setLastName("Brown"); + + Customer two = new Customer(); + two.setCustomerId(2L); + two.setFirstName("Emily"); + two.setLastName("Clark"); + + when(customerRepository.findAllWithPets()).thenReturn(List.of(one, two)); + + var response = controller.getAppointmentCustomers(); + + assertEquals(2, response.getBody().size()); + assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); + assertEquals("Alex Brown", response.getBody().get(0).getLabel()); + } } diff --git a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java index 3523e918..6192ccb2 100644 --- a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -52,6 +52,7 @@ class AdoptionServiceTest { pet = new Pet(); pet.setPetId(1L); pet.setPetName("Buddy"); + pet.setPetStatus("Available"); customer = new Customer(); customer.setCustomerId(1L); @@ -75,11 +76,13 @@ class AdoptionServiceTest { User staffUser = new User(); staffUser.setId(7L); staffUser.setRole(User.Role.STAFF); + staffUser.setActive(true); when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); User adminUser = new User(); adminUser.setId(8L); adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); when(userRepository.findById(8L)).thenReturn(Optional.of(adminUser)); } @@ -121,4 +124,26 @@ class AdoptionServiceTest { assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); } + + @Test + void createAdoptionRejectsInactiveStaffUserSelection() { + User inactiveStaffUser = new User(); + inactiveStaffUser.setId(7L); + inactiveStaffUser.setRole(User.Role.STAFF); + inactiveStaffUser.setActive(false); + when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser)); + + when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(employeeRepository.findById(7L)).thenReturn(Optional.of(staffEmployee)); + + AdoptionRequest request = new AdoptionRequest(); + request.setPetId(1L); + request.setCustomerId(1L); + request.setEmployeeId(7L); + request.setAdoptionDate(LocalDate.now()); + request.setAdoptionStatus("Pending"); + + assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); + } } diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java index 91e414f6..9d9daa40 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -127,6 +127,7 @@ class AppointmentServiceTest { User staffUser = new User(); staffUser.setId(7L); staffUser.setRole(User.Role.STAFF); + staffUser.setActive(true); when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); date = LocalDate.now().plusDays(1); @@ -215,6 +216,7 @@ class AppointmentServiceTest { adminUser.setId(99L); adminUser.setUsername("admin"); adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); adminUser.setTokenVersion(0); when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); setAuthentication(99L, User.Role.ADMIN); @@ -253,6 +255,7 @@ class AppointmentServiceTest { adminUser.setId(99L); adminUser.setUsername("admin"); adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); adminUser.setTokenVersion(0); when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); setAuthentication(99L, User.Role.ADMIN); @@ -306,6 +309,7 @@ class AppointmentServiceTest { User adminLinkedUser = new User(); adminLinkedUser.setId(8L); adminLinkedUser.setRole(User.Role.ADMIN); + adminLinkedUser.setActive(true); when(userRepository.findById(8L)).thenReturn(Optional.of(adminLinkedUser)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); @@ -330,6 +334,45 @@ class AppointmentServiceTest { assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); } + @Test + void createAppointmentRejectsInactiveStaffUserSelection() { + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setUsername("admin"); + adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); + adminUser.setTokenVersion(0); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + setAuthentication(99L, User.Role.ADMIN); + + User inactiveStaffUser = new User(); + inactiveStaffUser.setId(7L); + inactiveStaffUser.setRole(User.Role.STAFF); + inactiveStaffUser.setActive(false); + when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser)); + + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(employeeRepository.findById(7L)).thenReturn(Optional.of(employee)); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); + + var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); + request.setCustomerId(1L); + request.setStoreId(1L); + request.setServiceId(1L); + request.setEmployeeId(7L); + request.setAppointmentDate(date); + request.setAppointmentTime(LocalTime.of(10, 0)); + request.setAppointmentStatus("Booked"); + request.setCustomerPetIds(List.of(11L)); + + assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); + } + private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) { Appointment appointment = new Appointment(); appointment.setAppointmentId(id); diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java index fec5fae2..05b2785c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java @@ -74,6 +74,14 @@ public class DropdownApi { return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + public List getAppointmentCustomers() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/appointment-customers"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from appointment customers endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + public List getPets() throws Exception { String response = apiClient.getRawResponse("/api/v1/dropdowns/pets"); if (response == null || response.isEmpty()) { @@ -82,6 +90,14 @@ public class DropdownApi { return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + public List getAdoptionPets() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/adoption-pets"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from adoption pets endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + public List getCustomerPets(Long customerId) throws Exception { String response = apiClient.getRawResponse("/api/v1/dropdowns/customers/" + customerId + "/pets"); if (response == null || response.isEmpty()) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java index db7094c7..3a331711 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java @@ -18,11 +18,13 @@ import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest; import org.example.petshopdesktop.api.dto.common.DropdownOption; import org.example.petshopdesktop.api.endpoints.AdoptionApi; import org.example.petshopdesktop.api.endpoints.DropdownApi; +import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.models.Adoption; import org.example.petshopdesktop.util.ActivityLogger; import java.time.LocalDate; import java.util.List; +import java.util.Objects; public class AdoptionDialogController { @@ -56,6 +58,7 @@ public class AdoptionDialogController { //Stores if the dialog view is in add/edit mode private String mode = null; + private Adoption selectedAdoption = null; //Adoption statuses private ObservableList statusList = FXCollections.observableArrayList( @@ -70,11 +73,12 @@ public class AdoptionDialogController { new Thread(() -> { try { - List pets = DropdownApi.getInstance().getPets(); + List pets = DropdownApi.getInstance().getAdoptionPets(); Platform.runLater(() -> { if (pets != null) { ObservableList petsObs = FXCollections.observableArrayList(pets); cbPet.setItems(petsObs); + applySelectedPet(); } }); } catch (Exception e) { @@ -90,9 +94,12 @@ public class AdoptionDialogController { new Thread(() -> { try { - Long storeId = org.example.petshopdesktop.auth.UserSession.getInstance().getStoreId(); + Long storeId = UserSession.getInstance().getStoreId(); List employees = storeId != null && storeId > 0 ? DropdownApi.getInstance().getStoreEmployees(storeId) : List.of(); - Platform.runLater(() -> cbEmployee.setItems(FXCollections.observableArrayList(employees))); + Platform.runLater(() -> { + cbEmployee.setItems(FXCollections.observableArrayList(employees)); + applySelectedEmployee(); + }); } catch (Exception e) { Platform.runLater(() -> { ActivityLogger.getInstance().logException( @@ -127,6 +134,7 @@ public class AdoptionDialogController { if (customers != null) { ObservableList customersObs = FXCollections.observableArrayList(customers); cbCustomer.setItems(customersObs); + applySelectedCustomer(); } }); } catch (Exception e) { @@ -230,30 +238,11 @@ public class AdoptionDialogController { public void displayAdoptionDetails(Adoption adoption) { if (adoption != null) { + selectedAdoption = adoption; lblAdoptionId.setText("ID: " + adoption.getAdoptionId()); - - for (DropdownOption pet : cbPet.getItems()) { - if (pet.getLabel().equals(adoption.getPetName())) { - cbPet.getSelectionModel().select(pet); - break; - } - } - - for (DropdownOption customer : cbCustomer.getItems()) { - if (customer.getLabel().equals(adoption.getCustomerName())) { - cbCustomer.getSelectionModel().select(customer); - break; - } - } - - if (adoption.getEmployeeId() > 0) { - for (DropdownOption employee : cbEmployee.getItems()) { - if (employee.getId() != null && employee.getId().equals(adoption.getEmployeeId())) { - cbEmployee.getSelectionModel().select(employee); - break; - } - } - } + applySelectedPet(); + applySelectedCustomer(); + applySelectedEmployee(); if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) { try { @@ -280,4 +269,46 @@ public class AdoptionDialogController { lblMode.setText(mode + " Adoption"); lblAdoptionId.setVisible(mode.equals("Edit")); } + + private void applySelectedPet() { + if (selectedAdoption == null || selectedAdoption.getPetId() <= 0) { + return; + } + DropdownOption selected = findOptionById(cbPet.getItems(), (long) selectedAdoption.getPetId()); + if (selected != null && !Objects.equals(cbPet.getValue(), selected)) { + cbPet.setValue(selected); + } + } + + private void applySelectedCustomer() { + if (selectedAdoption == null || selectedAdoption.getCustomerId() <= 0) { + return; + } + DropdownOption selected = findOptionById(cbCustomer.getItems(), (long) selectedAdoption.getCustomerId()); + if (selected != null && !Objects.equals(cbCustomer.getValue(), selected)) { + cbCustomer.setValue(selected); + } + } + + private void applySelectedEmployee() { + if (selectedAdoption == null || selectedAdoption.getEmployeeId() <= 0) { + return; + } + DropdownOption selected = findOptionById(cbEmployee.getItems(), (long) selectedAdoption.getEmployeeId()); + if (selected != null && !Objects.equals(cbEmployee.getValue(), selected)) { + cbEmployee.setValue(selected); + } + } + + private DropdownOption findOptionById(List options, Long id) { + if (id == null || options == null) { + return null; + } + for (DropdownOption option : options) { + if (option.getId() != null && option.getId().equals(id)) { + return option; + } + } + return null; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index 191e58ad..472ca7b4 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -20,8 +20,9 @@ import org.example.petshopdesktop.util.ActivityLogger; import java.time.LocalTime; import java.time.LocalDate; -import java.util.Collections; import java.util.List; +import java.util.Collections; +import java.util.Objects; public class AppointmentDialogController { @@ -75,49 +76,6 @@ public class AppointmentDialogController { @FXML public void initialize() { - - new Thread(() -> { - try { - List services = DropdownApi.getInstance().getServices(); - List customers = DropdownApi.getInstance().getCustomers(); - - Platform.runLater(() -> { - if (services != null) { - cbService.setItems(FXCollections.observableArrayList(services)); - } - if (customers != null) { - cbCustomer.setItems(FXCollections.observableArrayList(customers)); - } - syncSelectedAppointment(); - }); - } catch (Exception e) { - Platform.runLater(() -> { - ActivityLogger.getInstance().logException( - "AppointmentDialogController.initialize", - e, - "Loading services/customers for appointment dialog"); - e.printStackTrace(); - }); - } - }).start(); - - new Thread(() -> { - try { - Long storeId = UserSession.getInstance().getStoreId(); - List employees = storeId != null && storeId > 0 ? DropdownApi.getInstance().getStoreEmployees(storeId) : List.of(); - Platform.runLater(() -> cbEmployee.setItems(FXCollections.observableArrayList(employees))); - } catch (Exception e) { - Platform.runLater(() -> { - ActivityLogger.getInstance().logException( - "AppointmentDialogController.initialize", - e, - "Loading employees for appointment dialog"); - cbEmployee.setDisable(true); - cbEmployee.setPromptText("Unable to load employees"); - }); - } - }).start(); - cbAppointmentStatus.setItems(statusList); cbPet.setDisable(true); cbEmployee.setPromptText("Select an employee"); @@ -211,6 +169,10 @@ public class AppointmentDialogController { btnSave.setOnMouseClicked(this::buttonSaveClicked); btnCancel.setOnMouseClicked(this::closeStage); + + loadServices(); + loadAppointmentCustomers(); + loadEmployees(); } // @@ -221,6 +183,7 @@ public class AppointmentDialogController { selectedAppointment = appt; lblAppointmentId.setText("ID: " + appt.getAppointmentId()); + pendingPetSelectionId = appt.getPetId() > 0 ? (long) appt.getPetId() : null; try { dpAppointmentDate.setValue( @@ -246,24 +209,9 @@ public class AppointmentDialogController { "Parsing appointment time"); } - cbService.getItems().forEach(s -> { - if (s.getId() != null && s.getId().longValue() == appt.getServiceId()) cbService.setValue(s); - }); - - cbCustomer.getItems().forEach(c -> { - if (c.getId() != null && c.getId().longValue() == appt.getCustomerId()) { - pendingPetSelectionId = (long) appt.getPetId(); - cbCustomer.setValue(c); - } - }); - - if (appt.getEmployeeId() > 0) { - cbEmployee.getItems().forEach(employee -> { - if (employee.getId() != null && employee.getId().longValue() == appt.getEmployeeId()) { - cbEmployee.setValue(employee); - } - }); - } + applySelectedService(); + applySelectedCustomer(); + applySelectedEmployee(); } // @@ -350,6 +298,49 @@ public class AppointmentDialogController { } } + private void applySelectedService() { + if (selectedAppointment == null || selectedAppointment.getServiceId() <= 0) { + return; + } + DropdownOption selected = findOptionById(cbService.getItems(), (long) selectedAppointment.getServiceId()); + if (selected != null && !Objects.equals(cbService.getValue(), selected)) { + cbService.setValue(selected); + } + } + + private void applySelectedCustomer() { + if (selectedAppointment == null || selectedAppointment.getCustomerId() <= 0) { + return; + } + + DropdownOption selected = findOptionById(cbCustomer.getItems(), (long) selectedAppointment.getCustomerId()); + if (selected != null && !Objects.equals(cbCustomer.getValue(), selected)) { + cbCustomer.setValue(selected); + } + } + + private void applySelectedEmployee() { + if (selectedAppointment == null || selectedAppointment.getEmployeeId() <= 0) { + return; + } + DropdownOption selected = findOptionById(cbEmployee.getItems(), (long) selectedAppointment.getEmployeeId()); + if (selected != null && !Objects.equals(cbEmployee.getValue(), selected)) { + cbEmployee.setValue(selected); + } + } + + private DropdownOption findOptionById(List options, Long id) { + if (id == null || options == null) { + return null; + } + for (DropdownOption option : options) { + if (option.getId() != null && option.getId().equals(id)) { + return option; + } + } + return null; + } + private void loadCustomerPets(Long customerId) { new Thread(() -> { try { @@ -359,22 +350,12 @@ public class AppointmentDialogController { cbPet.setDisable(pets == null || pets.isEmpty()); cbPet.setPromptText(pets == null || pets.isEmpty() ? "No pets for selected customer" : "Select a pet"); if (pendingPetSelectionId != null) { - boolean matched = false; for (DropdownOption pet : cbPet.getItems()) { if (pet.getId() != null && pet.getId().equals(pendingPetSelectionId)) { cbPet.setValue(pet); - matched = true; break; } } - if (!matched && selectedAppointment != null && selectedAppointment.getPetName() != null && !selectedAppointment.getPetName().isBlank()) { - DropdownOption legacy = new DropdownOption(); - legacy.setId(pendingPetSelectionId); - legacy.setLabel(selectedAppointment.getPetName() + " (legacy appointment pet)"); - cbPet.getItems().add(0, legacy); - cbPet.setValue(legacy); - cbPet.setDisable(false); - } pendingPetSelectionId = null; } }); @@ -391,4 +372,78 @@ public class AppointmentDialogController { } }).start(); } + + private void loadServices() { + new Thread(() -> { + try { + List services = DropdownApi.getInstance().getServices(); + Platform.runLater(() -> { + cbService.setItems(FXCollections.observableArrayList(services)); + applySelectedService(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.loadServices", + e, + "Loading services for appointment dialog"); + cbService.setDisable(true); + cbService.setPromptText("Unable to load services"); + }); + } + }).start(); + } + + private void loadAppointmentCustomers() { + new Thread(() -> { + try { + List customers = DropdownApi.getInstance().getAppointmentCustomers(); + Platform.runLater(() -> { + cbCustomer.setItems(FXCollections.observableArrayList(customers)); + boolean hasCustomers = customers != null && !customers.isEmpty(); + cbCustomer.setDisable(!hasCustomers); + cbPet.setDisable(true); + cbPet.setItems(FXCollections.observableArrayList()); + cbCustomer.setPromptText(hasCustomers ? "Select a customer" : "No customers with pets yet"); + cbPet.setPromptText(hasCustomers ? "Select a customer first" : "No customer pets available"); + applySelectedCustomer(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.loadAppointmentCustomers", + e, + "Loading appointment customers for appointment dialog"); + cbCustomer.setDisable(true); + cbPet.setDisable(true); + cbCustomer.setPromptText("Unable to load customers"); + cbPet.setPromptText("Unable to load pets"); + }); + } + }).start(); + } + + private void loadEmployees() { + new Thread(() -> { + try { + Long storeId = UserSession.getInstance().getStoreId(); + List employees = storeId != null && storeId > 0 + ? DropdownApi.getInstance().getStoreEmployees(storeId) + : List.of(); + Platform.runLater(() -> { + cbEmployee.setItems(FXCollections.observableArrayList(employees)); + applySelectedEmployee(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.loadEmployees", + e, + "Loading employees for appointment dialog"); + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Unable to load employees"); + }); + } + }).start(); + } }