From 5d956137860201c94c2a3cef21ca102b5c90e9f7 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 12:17:37 -0600 Subject: [PATCH 01/12] Harden staff assignment --- .../petstoremobile/dtos/AdoptionDTO.java | 17 ++- .../petstoremobile/dtos/AppointmentDTO.java | 17 +++ .../controller/DropdownController.java | 36 ++++- .../backend/dto/adoption/AdoptionRequest.java | 16 ++- .../dto/adoption/AdoptionResponse.java | 22 +++- .../dto/appointment/AppointmentRequest.java | 16 ++- .../dto/appointment/AppointmentResponse.java | 18 +++ .../com/petshop/backend/entity/Adoption.java | 16 ++- .../petshop/backend/entity/Appointment.java | 16 ++- .../repository/EmployeeRepository.java | 2 + .../repository/EmployeeStoreRepository.java | 6 + .../backend/service/AdoptionService.java | 43 +++++- .../backend/service/AppointmentService.java | 39 ++++++ ...appointment_adoption_employee_required.sql | 61 +++++++++ .../controller/DropdownControllerTest.java | 90 +++++++++++++ .../backend/service/AdoptionServiceTest.java | 124 ++++++++++++++++++ .../service/AppointmentServiceTest.java | 70 ++++++++++ .../petshopdesktop/DTOs/AppointmentDTO.java | 10 +- .../api/dto/adoption/AdoptionRequest.java | 9 ++ .../api/dto/adoption/AdoptionResponse.java | 18 +++ .../dto/appointment/AppointmentRequest.java | 9 ++ .../dto/appointment/AppointmentResponse.java | 18 +++ .../api/endpoints/DropdownApi.java | 8 ++ .../controllers/AdoptionController.java | 6 + .../controllers/AppointmentController.java | 4 + .../AdoptionDialogController.java | 51 +++++++ .../AppointmentDialogController.java | 46 ++++++- .../petshopdesktop/models/Adoption.java | 18 ++- .../dialogviews/adoption-dialog-view.fxml | 15 +++ .../dialogviews/appointment-dialog-view.fxml | 27 +++- .../modelviews/adoption-view.fxml | 11 +- .../modelviews/appointment-view.fxml | 13 +- web/app/appointments/page.js | 45 +++++++ 33 files changed, 887 insertions(+), 30 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql create mode 100644 backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java create mode 100644 backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java index 03758473..6866f6b0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java @@ -8,6 +8,8 @@ public class AdoptionDTO { private String petName; private Long customerId; private String customerName; + private Long employeeId; + private String employeeName; private String adoptionDate; private String adoptionStatus; private BigDecimal adoptionFee; @@ -16,8 +18,13 @@ public class AdoptionDTO { // Constructor for create/update requests public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus) { + this(petId, customerId, null, adoptionDate, adoptionStatus); + } + + public AdoptionDTO(Long petId, Long customerId, Long employeeId, String adoptionDate, String adoptionStatus) { this.petId = petId; this.customerId = customerId; + this.employeeId = employeeId; this.adoptionDate = adoptionDate; this.adoptionStatus = adoptionStatus; } @@ -42,6 +49,14 @@ public class AdoptionDTO { return customerName; } + public Long getEmployeeId() { + return employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + public String getAdoptionDate() { return adoptionDate; } @@ -65,4 +80,4 @@ public class AdoptionDTO { public String getUpdatedAt() { return updatedAt; } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java index 4c7a91b7..05f9ea21 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java @@ -12,6 +12,8 @@ public class AppointmentDTO { private String storeName; private Long serviceId; private String serviceName; + private Long employeeId; + private String employeeName; private String appointmentDate; private String appointmentTime; private String appointmentStatus; @@ -25,9 +27,16 @@ public class AppointmentDTO { public AppointmentDTO(Long customerId, Long storeId, Long serviceId, String appointmentDate, String appointmentTime, String appointmentStatus, List petIds) { + this(customerId, storeId, serviceId, null, appointmentDate, appointmentTime, appointmentStatus, petIds); + } + + public AppointmentDTO(Long customerId, Long storeId, Long serviceId, Long employeeId, + String appointmentDate, String appointmentTime, + String appointmentStatus, List petIds) { this.customerId = customerId; this.storeId = storeId; this.serviceId = serviceId; + this.employeeId = employeeId; this.appointmentDate = appointmentDate; this.appointmentTime = appointmentTime; this.appointmentStatus = appointmentStatus; @@ -63,6 +72,14 @@ public class AppointmentDTO { return serviceName; } + public Long getEmployeeId() { + return employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + public String getAppointmentDate() { return appointmentDate; } 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 56e53a56..5c56862d 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -2,6 +2,8 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.common.DropdownOption; import com.petshop.backend.entity.CustomerPet; +import com.petshop.backend.entity.EmployeeStore; +import com.petshop.backend.entity.User; import com.petshop.backend.repository.*; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -25,12 +27,15 @@ public class DropdownController { private final CategoryRepository categoryRepository; private final StoreRepository storeRepository; private final SupplierRepository supplierRepository; + private final EmployeeStoreRepository employeeStoreRepository; + private final UserRepository userRepository; public DropdownController(PetRepository petRepository, CustomerRepository customerRepository, CustomerPetRepository customerPetRepository, ServiceRepository serviceRepository, ProductRepository productRepository, CategoryRepository categoryRepository, StoreRepository storeRepository, - SupplierRepository supplierRepository) { + SupplierRepository supplierRepository, EmployeeStoreRepository employeeStoreRepository, + UserRepository userRepository) { this.petRepository = petRepository; this.customerRepository = customerRepository; this.customerPetRepository = customerPetRepository; @@ -39,6 +44,8 @@ public class DropdownController { this.categoryRepository = categoryRepository; this.storeRepository = storeRepository; this.supplierRepository = supplierRepository; + this.employeeStoreRepository = employeeStoreRepository; + this.userRepository = userRepository; } @GetMapping("/pets") @@ -129,6 +136,17 @@ public class DropdownController { ); } + @GetMapping("/stores/{storeId}/employees") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity> getStoreEmployees(@PathVariable Long storeId) { + return ResponseEntity.ok( + employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() + .filter(this::isAssignableEmployee) + .map(this::toEmployeeOption) + .collect(Collectors.toList()) + ); + } + @GetMapping("/suppliers") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> getSuppliers() { @@ -144,4 +162,20 @@ public class DropdownController { String breed = pet.getBreed() == null || pet.getBreed().isBlank() ? "" : " ยท " + pet.getBreed(); return new DropdownOption(pet.getCustomerPetId(), pet.getPetName() + " (" + species + breed + ")"); } + + private DropdownOption toEmployeeOption(EmployeeStore employeeStore) { + var employee = employeeStore.getEmployee(); + return new DropdownOption(employee.getEmployeeId(), employee.getFirstName() + " " + employee.getLastName()); + } + + private boolean isAssignableEmployee(EmployeeStore employeeStore) { + Long userId = employeeStore.getEmployee().getUserId(); + if (userId == null) { + return false; + } + return userRepository.findById(userId) + .map(User::getRole) + .filter(role -> role == User.Role.STAFF) + .isPresent(); + } } diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java index 1807f5b4..9a34dff8 100644 --- a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java @@ -18,6 +18,8 @@ public class AdoptionRequest { @NotBlank(message = "Adoption status is required") private String adoptionStatus; + private Long employeeId; + public Long getPetId() { return petId; } @@ -50,6 +52,14 @@ public class AdoptionRequest { this.adoptionStatus = adoptionStatus; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -58,12 +68,13 @@ public class AdoptionRequest { return Objects.equals(petId, that.petId) && Objects.equals(customerId, that.customerId) && Objects.equals(adoptionDate, that.adoptionDate) && - Objects.equals(adoptionStatus, that.adoptionStatus); + Objects.equals(adoptionStatus, that.adoptionStatus) && + Objects.equals(employeeId, that.employeeId); } @Override public int hashCode() { - return Objects.hash(petId, customerId, adoptionDate, adoptionStatus); + return Objects.hash(petId, customerId, adoptionDate, adoptionStatus, employeeId); } @Override @@ -73,6 +84,7 @@ public class AdoptionRequest { ", customerId=" + customerId + ", adoptionDate=" + adoptionDate + ", adoptionStatus='" + adoptionStatus + '\'' + + ", employeeId=" + employeeId + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java index 6f2d0556..43128d23 100644 --- a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java @@ -11,6 +11,8 @@ public class AdoptionResponse { private String petName; private Long customerId; private String customerName; + private Long employeeId; + private String employeeName; private LocalDate adoptionDate; private String adoptionStatus; private BigDecimal adoptionFee; @@ -20,12 +22,14 @@ public class AdoptionResponse { public AdoptionResponse() { } - public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, LocalDate adoptionDate, String adoptionStatus, BigDecimal adoptionFee, LocalDateTime createdAt, LocalDateTime updatedAt) { + public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, Long employeeId, String employeeName, LocalDate adoptionDate, String adoptionStatus, BigDecimal adoptionFee, LocalDateTime createdAt, LocalDateTime updatedAt) { this.adoptionId = adoptionId; this.petId = petId; this.petName = petName; this.customerId = customerId; this.customerName = customerName; + this.employeeId = employeeId; + this.employeeName = employeeName; this.adoptionDate = adoptionDate; this.adoptionStatus = adoptionStatus; this.adoptionFee = adoptionFee; @@ -73,6 +77,22 @@ public class AdoptionResponse { this.customerName = customerName; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + public LocalDate getAdoptionDate() { return adoptionDate; } diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java index 8423d090..3d127c19 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java @@ -29,6 +29,8 @@ public class AppointmentRequest { private List customerPetIds; + private Long employeeId; + public Long getCustomerId() { return customerId; } @@ -93,6 +95,14 @@ public class AppointmentRequest { this.customerPetIds = customerPetIds; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -105,12 +115,13 @@ public class AppointmentRequest { Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentStatus, that.appointmentStatus) && Objects.equals(petIds, that.petIds) && - Objects.equals(customerPetIds, that.customerPetIds); + Objects.equals(customerPetIds, that.customerPetIds) && + Objects.equals(employeeId, that.employeeId); } @Override public int hashCode() { - return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, customerPetIds); + return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, customerPetIds, employeeId); } @Override @@ -124,6 +135,7 @@ public class AppointmentRequest { ", appointmentStatus='" + appointmentStatus + '\'' + ", petIds=" + petIds + ", customerPetIds=" + customerPetIds + + ", employeeId=" + employeeId + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java index f6398248..efc1c300 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java @@ -17,6 +17,8 @@ public class AppointmentResponse { private LocalDate appointmentDate; private LocalTime appointmentTime; private String appointmentStatus; + private Long employeeId; + private String employeeName; private List petNames; private List petIds; private List customerPetNames; @@ -124,6 +126,22 @@ public class AppointmentResponse { this.appointmentStatus = appointmentStatus; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + public List getPetNames() { return petNames; } diff --git a/backend/src/main/java/com/petshop/backend/entity/Adoption.java b/backend/src/main/java/com/petshop/backend/entity/Adoption.java index 84912ba9..78360e2e 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Adoption.java +++ b/backend/src/main/java/com/petshop/backend/entity/Adoption.java @@ -25,6 +25,10 @@ public class Adoption { @JoinColumn(name = "customerId", nullable = false) private Customer customer; + @ManyToOne + @JoinColumn(name = "employeeId", nullable = false) + private Employee employee; + @Column(nullable = false) private LocalDate adoptionDate; @@ -42,10 +46,11 @@ public class Adoption { public Adoption() { } - public Adoption(Long adoptionId, Pet pet, Customer customer, LocalDate adoptionDate, String adoptionStatus, LocalDateTime createdAt, LocalDateTime updatedAt) { + public Adoption(Long adoptionId, Pet pet, Customer customer, Employee employee, LocalDate adoptionDate, String adoptionStatus, LocalDateTime createdAt, LocalDateTime updatedAt) { this.adoptionId = adoptionId; this.pet = pet; this.customer = customer; + this.employee = employee; this.adoptionDate = adoptionDate; this.adoptionStatus = adoptionStatus; this.createdAt = createdAt; @@ -76,6 +81,14 @@ public class Adoption { this.customer = customer; } + public Employee getEmployee() { + return employee; + } + + public void setEmployee(Employee employee) { + this.employee = employee; + } + public LocalDate getAdoptionDate() { return adoptionDate; } @@ -127,6 +140,7 @@ public class Adoption { "adoptionId=" + adoptionId + ", pet=" + pet + ", customer=" + customer + + ", employee=" + employee + ", adoptionDate=" + adoptionDate + ", adoptionStatus='" + adoptionStatus + '\'' + ", createdAt=" + createdAt + diff --git a/backend/src/main/java/com/petshop/backend/entity/Appointment.java b/backend/src/main/java/com/petshop/backend/entity/Appointment.java index cec10224..d4ebc199 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Appointment.java +++ b/backend/src/main/java/com/petshop/backend/entity/Appointment.java @@ -31,6 +31,10 @@ public class Appointment { @JoinColumn(name = "serviceId", nullable = false) private Service service; + @ManyToOne + @JoinColumn(name = "employeeId", nullable = false) + private Employee employee; + @Column(nullable = false) private LocalDate appointmentDate; @@ -67,11 +71,12 @@ public class Appointment { public Appointment() { } - public Appointment(Long appointmentId, Customer customer, StoreLocation store, Service service, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, Set pets, LocalDateTime createdAt, LocalDateTime updatedAt) { + public Appointment(Long appointmentId, Customer customer, StoreLocation store, Service service, Employee employee, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, Set pets, LocalDateTime createdAt, LocalDateTime updatedAt) { this.appointmentId = appointmentId; this.customer = customer; this.store = store; this.service = service; + this.employee = employee; this.appointmentDate = appointmentDate; this.appointmentTime = appointmentTime; this.appointmentStatus = appointmentStatus; @@ -112,6 +117,14 @@ public class Appointment { this.service = service; } + public Employee getEmployee() { + return employee; + } + + public void setEmployee(Employee employee) { + this.employee = employee; + } + public LocalDate getAppointmentDate() { return appointmentDate; } @@ -189,6 +202,7 @@ public class Appointment { ", customer=" + customer + ", store=" + store + ", service=" + service + + ", employee=" + employee + ", appointmentDate=" + appointmentDate + ", appointmentTime=" + appointmentTime + ", appointmentStatus='" + appointmentStatus + '\'' + diff --git a/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java b/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java index cfbf715f..e320fc00 100644 --- a/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java @@ -15,6 +15,8 @@ import java.util.Optional; public interface EmployeeRepository extends JpaRepository { Optional findByUserId(Long userId); List findAllByEmail(String email); + Optional findFirstByIsActiveTrueOrderByEmployeeIdAsc(); + List findAllByIsActiveTrueOrderByEmployeeIdAsc(); @Query("SELECT e FROM Employee e WHERE " + "LOWER(e.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + diff --git a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java index 1c847817..0cc3f771 100644 --- a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java @@ -2,11 +2,17 @@ package com.petshop.backend.repository; import com.petshop.backend.entity.EmployeeStore; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface EmployeeStoreRepository extends JpaRepository { Optional findByEmployeeEmployeeId(Long employeeId); + + @Query("SELECT es FROM EmployeeStore es WHERE es.store.storeId = :storeId AND es.employee.isActive = true ORDER BY es.employee.employeeId ASC") + List findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(@Param("storeId") Long storeId); } 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 a8f7f476..4bda85ab 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -5,11 +5,15 @@ import com.petshop.backend.dto.adoption.AdoptionResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.Adoption; import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Employee; import com.petshop.backend.entity.Pet; +import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeRepository; import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -21,11 +25,15 @@ public class AdoptionService { private final AdoptionRepository adoptionRepository; private final PetRepository petRepository; private final CustomerRepository customerRepository; + private final EmployeeRepository employeeRepository; + private final UserRepository userRepository; - public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, CustomerRepository customerRepository) { + public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, CustomerRepository customerRepository, EmployeeRepository employeeRepository, UserRepository userRepository) { this.adoptionRepository = adoptionRepository; this.petRepository = petRepository; this.customerRepository = customerRepository; + this.employeeRepository = employeeRepository; + this.userRepository = userRepository; } public Page getAllAdoptions(String query, Pageable pageable, Long customerId) { @@ -66,10 +74,12 @@ public class AdoptionService { Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + Employee employee = resolveAdoptionEmployee(request.getEmployeeId()); Adoption adoption = new Adoption(); adoption.setPet(pet); adoption.setCustomer(customer); + adoption.setEmployee(employee); adoption.setAdoptionDate(request.getAdoptionDate()); adoption.setAdoptionStatus(request.getAdoptionStatus()); @@ -87,9 +97,11 @@ public class AdoptionService { Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + Employee employee = resolveAdoptionEmployee(request.getEmployeeId()); adoption.setPet(pet); adoption.setCustomer(customer); + adoption.setEmployee(employee); adoption.setAdoptionDate(request.getAdoptionDate()); adoption.setAdoptionStatus(request.getAdoptionStatus()); @@ -117,6 +129,8 @@ public class AdoptionService { adoption.getPet().getPetName(), adoption.getCustomer().getCustomerId(), adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(), + adoption.getEmployee().getEmployeeId(), + adoption.getEmployee().getFirstName() + " " + adoption.getEmployee().getLastName(), adoption.getAdoptionDate(), adoption.getAdoptionStatus(), adoption.getPet().getPetPrice(), @@ -124,4 +138,31 @@ public class AdoptionService { adoption.getUpdatedAt() ); } + + private Employee resolveAdoptionEmployee(Long requestedEmployeeId) { + if (requestedEmployeeId != null) { + Employee employee = employeeRepository.findById(requestedEmployeeId) + .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId)); + if (!isAssignableEmployee(employee)) { + throw new IllegalArgumentException("Selected employee is not assignable for adoption work"); + } + return employee; + } + + return employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc().stream() + .filter(this::isAssignableEmployee) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No assignable staff member is available for adoption assignment")); + } + + private boolean isAssignableEmployee(Employee employee) { + Long userId = employee.getUserId(); + if (userId == null || !Boolean.TRUE.equals(employee.getIsActive())) { + return false; + } + return userRepository.findById(userId) + .map(User::getRole) + .filter(role -> role == User.Role.STAFF) + .isPresent(); + } } 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 c5b615d3..0b277889 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -121,6 +121,7 @@ public class AppointmentService { Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); + Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); Appointment appointment = new Appointment(); appointment.setCustomer(customer); @@ -131,6 +132,7 @@ public class AppointmentService { appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setPets(pets); appointment.setCustomerPets(customerPets); + appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); return mapToResponse(appointment); @@ -165,6 +167,7 @@ public class AppointmentService { Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); + Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); appointment.setCustomer(customer); appointment.setStore(store); @@ -174,6 +177,7 @@ public class AppointmentService { appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setPets(pets); appointment.setCustomerPets(customerPets); + appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); return mapToResponse(appointment); @@ -289,6 +293,8 @@ public class AppointmentService { response.setAppointmentDate(appointment.getAppointmentDate()); response.setAppointmentTime(appointment.getAppointmentTime()); response.setAppointmentStatus(appointment.getAppointmentStatus()); + response.setEmployeeId(appointment.getEmployee().getEmployeeId()); + response.setEmployeeName(appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName()); response.setPetNames(petNames); response.setPetIds(petIds); response.setCustomerPetNames(customerPetNames); @@ -299,6 +305,39 @@ public class AppointmentService { return response; } + private Employee resolveAppointmentEmployee(Long requestedEmployeeId, Long storeId) { + List assignableEmployees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() + .filter(es -> isAssignableEmployee(es.getEmployee())) + .collect(Collectors.toList()); + + if (requestedEmployeeId != null) { + Employee employee = employeeRepository.findById(requestedEmployeeId) + .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId)); + boolean assignedToStore = assignableEmployees.stream() + .anyMatch(es -> es.getEmployee().getEmployeeId().equals(requestedEmployeeId)); + if (!assignedToStore) { + throw new IllegalArgumentException("Selected employee is not assignable for the selected store"); + } + return employee; + } + + return assignableEmployees.stream() + .map(EmployeeStore::getEmployee) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No assignable staff member is assigned to the selected store")); + } + + private boolean isAssignableEmployee(Employee employee) { + Long userId = employee.getUserId(); + if (userId == null || !Boolean.TRUE.equals(employee.getIsActive())) { + return false; + } + return userRepository.findById(userId) + .map(User::getRole) + .filter(role -> role == User.Role.STAFF) + .isPresent(); + } + //------------------------------------ private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { // Filter by same service only - different services can run at same time diff --git a/backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql b/backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql new file mode 100644 index 00000000..e931e32a --- /dev/null +++ b/backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql @@ -0,0 +1,61 @@ +ALTER TABLE appointment + ADD COLUMN employeeId BIGINT NULL; + +UPDATE appointment a +SET a.employeeId = ( + SELECT es.employeeId + FROM employeeStore es + JOIN employee e ON e.employeeId = es.employeeId + JOIN users u ON u.id = e.user_id + WHERE es.storeId = a.storeId + AND e.isActive = TRUE + AND u.role = 'STAFF' + ORDER BY es.employeeId ASC + LIMIT 1 +) +WHERE a.employeeId IS NULL; + +UPDATE appointment a +SET a.employeeId = ( + SELECT e.employeeId + FROM employee e + JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE + AND u.role = 'STAFF' + ORDER BY e.employeeId ASC + LIMIT 1 +) +WHERE a.employeeId IS NULL; + +ALTER TABLE appointment + ADD CONSTRAINT fk_appointment_employee + FOREIGN KEY (employeeId) REFERENCES employee(employeeId); + +CREATE INDEX idx_appointment_employeeId ON appointment(employeeId); + +ALTER TABLE appointment + MODIFY employeeId BIGINT NOT NULL; + +ALTER TABLE adoption + ADD COLUMN employeeId BIGINT NULL; + +UPDATE adoption a +SET a.employeeId = ( + SELECT e.employeeId + FROM employee e + JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE + AND u.role = 'STAFF' + ORDER BY e.employeeId ASC + LIMIT 1 +) +WHERE a.employeeId IS NULL; + +ALTER TABLE adoption + ADD CONSTRAINT fk_adoption_employee + FOREIGN KEY (employeeId) REFERENCES employee(employeeId); + +CREATE INDEX idx_adoption_employeeId ON adoption(employeeId); + +ALTER TABLE adoption + MODIFY employeeId BIGINT NOT NULL; diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java new file mode 100644 index 00000000..52b64692 --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -0,0 +1,90 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.EmployeeStore; +import com.petshop.backend.entity.StoreLocation; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.CategoryRepository; +import com.petshop.backend.repository.CustomerPetRepository; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeStoreRepository; +import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.ProductRepository; +import com.petshop.backend.repository.ServiceRepository; +import com.petshop.backend.repository.StoreRepository; +import com.petshop.backend.repository.SupplierRepository; +import com.petshop.backend.repository.UserRepository; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DropdownControllerTest { + + @Test + void getStoreEmployeesReturnsOnlyStaffLinkedEmployees() { + 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 staffEmployee = new Employee(); + staffEmployee.setEmployeeId(7L); + staffEmployee.setUserId(7L); + staffEmployee.setFirstName("Alex"); + staffEmployee.setLastName("Jones"); + staffEmployee.setIsActive(true); + + Employee adminEmployee = new Employee(); + adminEmployee.setEmployeeId(8L); + adminEmployee.setUserId(8L); + adminEmployee.setFirstName("Admin"); + adminEmployee.setLastName("Helper"); + adminEmployee.setIsActive(true); + + User staffUser = new User(); + staffUser.setId(7L); + staffUser.setRole(User.Role.STAFF); + + User adminUser = new User(); + adminUser.setId(8L); + adminUser.setRole(User.Role.ADMIN); + + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(staffEmployee, store), new EmployeeStore(adminEmployee, store))); + when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); + when(userRepository.findById(8L)).thenReturn(Optional.of(adminUser)); + + var response = controller.getStoreEmployees(1L); + + assertEquals(1, response.getBody().size()); + assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); + assertEquals("Alex Jones", 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 new file mode 100644 index 00000000..3523e918 --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -0,0 +1,124 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.adoption.AdoptionRequest; +import com.petshop.backend.entity.Adoption; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.Pet; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.AdoptionRepository; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeRepository; +import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.quality.Strictness; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class AdoptionServiceTest { + + @Mock private AdoptionRepository adoptionRepository; + @Mock private PetRepository petRepository; + @Mock private CustomerRepository customerRepository; + @Mock private EmployeeRepository employeeRepository; + @Mock private UserRepository userRepository; + + @InjectMocks + private AdoptionService adoptionService; + + private Pet pet; + private Customer customer; + private Employee staffEmployee; + private Employee adminEmployee; + + @BeforeEach + void setUp() { + pet = new Pet(); + pet.setPetId(1L); + pet.setPetName("Buddy"); + + customer = new Customer(); + customer.setCustomerId(1L); + customer.setFirstName("Pat"); + customer.setLastName("Owner"); + + staffEmployee = new Employee(); + staffEmployee.setEmployeeId(7L); + staffEmployee.setUserId(7L); + staffEmployee.setFirstName("Alex"); + staffEmployee.setLastName("Jones"); + staffEmployee.setIsActive(true); + + adminEmployee = new Employee(); + adminEmployee.setEmployeeId(8L); + adminEmployee.setUserId(8L); + adminEmployee.setFirstName("Admin"); + adminEmployee.setLastName("Helper"); + adminEmployee.setIsActive(true); + + User staffUser = new User(); + staffUser.setId(7L); + staffUser.setRole(User.Role.STAFF); + when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); + + User adminUser = new User(); + adminUser.setId(8L); + adminUser.setRole(User.Role.ADMIN); + when(userRepository.findById(8L)).thenReturn(Optional.of(adminUser)); + } + + @Test + void createAdoptionAutoAssignsFirstStaffEmployee() { + when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee)); + when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { + Adoption adoption = invocation.getArgument(0); + adoption.setAdoptionId(10L); + return adoption; + }); + + AdoptionRequest request = new AdoptionRequest(); + request.setPetId(1L); + request.setCustomerId(1L); + request.setAdoptionDate(LocalDate.now()); + request.setAdoptionStatus("Pending"); + + var response = adoptionService.createAdoption(request); + + assertEquals(7L, response.getEmployeeId()); + assertEquals("Alex Jones", response.getEmployeeName()); + } + + @Test + void createAdoptionRejectsAdminEmployeeSelection() { + when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); + + AdoptionRequest request = new AdoptionRequest(); + request.setPetId(1L); + request.setCustomerId(1L); + request.setEmployeeId(8L); + 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 e978fcde..91e414f6 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -3,6 +3,8 @@ package com.petshop.backend.service; import com.petshop.backend.entity.Appointment; import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.CustomerPet; +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.EmployeeStore; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.Service; import com.petshop.backend.entity.StoreLocation; @@ -22,7 +24,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.quality.Strictness; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -41,6 +45,7 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) class AppointmentServiceTest { @Mock @@ -79,6 +84,7 @@ class AppointmentServiceTest { private Service nailTrim; private Pet pet; private CustomerPet customerPet; + private Employee employee; private LocalDate date; @BeforeEach @@ -111,6 +117,18 @@ class AppointmentServiceTest { customerPet.setPetName("Milo Jr"); customerPet.setCustomer(customer); + employee = new Employee(); + employee.setEmployeeId(7L); + employee.setUserId(7L); + employee.setFirstName("Alex"); + employee.setLastName("Jones"); + employee.setIsActive(true); + + User staffUser = new User(); + staffUser.setId(7L); + staffUser.setRole(User.Role.STAFF); + when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); + date = LocalDate.now().plusDays(1); } @@ -171,6 +189,8 @@ class AppointmentServiceTest { when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> invocation.getArgument(0)); @@ -210,6 +230,8 @@ class AppointmentServiceTest { 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(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); @@ -238,6 +260,8 @@ class AppointmentServiceTest { 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(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { @@ -259,6 +283,51 @@ class AppointmentServiceTest { assertEquals(99L, response.getAppointmentId()); assertEquals(1L, response.getCustomerId()); + assertEquals(7L, response.getEmployeeId()); + } + + @Test + void createAppointmentRejectsAdminEmployeeSelection() { + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setUsername("admin"); + adminUser.setRole(User.Role.ADMIN); + adminUser.setTokenVersion(0); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + setAuthentication(99L, User.Role.ADMIN); + + Employee adminEmployee = new Employee(); + adminEmployee.setEmployeeId(8L); + adminEmployee.setUserId(8L); + adminEmployee.setFirstName("Admin"); + adminEmployee.setLastName("Helper"); + adminEmployee.setIsActive(true); + + User adminLinkedUser = new User(); + adminLinkedUser.setId(8L); + adminLinkedUser.setRole(User.Role.ADMIN); + + when(userRepository.findById(8L)).thenReturn(Optional.of(adminLinkedUser)); + 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(8L)).thenReturn(Optional.of(adminEmployee)); + 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(adminEmployee, store), new EmployeeStore(employee, store))); + + var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); + request.setCustomerId(1L); + request.setStoreId(1L); + request.setServiceId(1L); + request.setEmployeeId(8L); + 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) { @@ -269,6 +338,7 @@ class AppointmentServiceTest { appointment.setAppointmentStatus("Booked"); appointment.setService(service); appointment.setStore(storeLocation); + appointment.setEmployee(employee); appointment.setCustomer(customer); appointment.setPets(Set.of()); return appointment; diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java index f749b5b5..71b7005c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java @@ -15,6 +15,8 @@ public class AppointmentDTO { private SimpleIntegerProperty serviceId; private SimpleStringProperty serviceName; + private SimpleIntegerProperty employeeId; + private SimpleStringProperty employeeName; private SimpleStringProperty appointmentDate; private SimpleStringProperty appointmentTime; @@ -25,6 +27,8 @@ public class AppointmentDTO { int customerId, String customerName, int petId, String petName, int serviceId, String serviceName, + int employeeId, + String employeeName, String appointmentDate, String appointmentTime, String appointmentStatus) { @@ -36,6 +40,8 @@ public class AppointmentDTO { this.petName = new SimpleStringProperty(petName); this.serviceId = new SimpleIntegerProperty(serviceId); this.serviceName = new SimpleStringProperty(serviceName); + this.employeeId = new SimpleIntegerProperty(employeeId); + this.employeeName = new SimpleStringProperty(employeeName); this.appointmentDate = new SimpleStringProperty(appointmentDate); this.appointmentTime = new SimpleStringProperty(appointmentTime); this.appointmentStatus = new SimpleStringProperty(appointmentStatus); @@ -52,8 +58,10 @@ public class AppointmentDTO { public int getServiceId() { return serviceId.get(); } public String getServiceName() { return serviceName.get(); } + public int getEmployeeId() { return employeeId.get(); } + public String getEmployeeName() { return employeeName.get(); } public String getAppointmentDate() { return appointmentDate.get(); } public String getAppointmentTime() { return appointmentTime.get(); } public String getAppointmentStatus() { return appointmentStatus.get(); } -} \ No newline at end of file +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java index 5bea7090..830488f1 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java @@ -5,6 +5,7 @@ import java.time.LocalDate; public class AdoptionRequest { private Long petId; private Long customerId; + private Long employeeId; private LocalDate adoptionDate; private String adoptionStatus; @@ -27,6 +28,14 @@ public class AdoptionRequest { this.customerId = customerId; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + public LocalDate getAdoptionDate() { return adoptionDate; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java index 60667217..8d0d1093 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java @@ -6,8 +6,10 @@ public class AdoptionResponse { private Long adoptionId; private Long petId; private Long customerId; + private Long employeeId; private String petName; private String customerName; + private String employeeName; private LocalDate adoptionDate; private java.math.BigDecimal adoptionFee; private String adoptionStatus; @@ -39,6 +41,14 @@ public class AdoptionResponse { this.customerId = customerId; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + public String getPetName() { return petName; } @@ -55,6 +65,14 @@ public class AdoptionResponse { this.customerName = customerName; } + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + public LocalDate getAdoptionDate() { return adoptionDate; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java index 299fcae9..c87b2e87 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java @@ -10,6 +10,7 @@ public class AppointmentRequest { private Long customerId; private Long storeId; private Long serviceId; + private Long employeeId; private LocalDate appointmentDate; private LocalTime appointmentTime; private String appointmentStatus; @@ -57,6 +58,14 @@ public class AppointmentRequest { this.serviceId = serviceId; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + public LocalDate getAppointmentDate() { return appointmentDate; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java index c71dc3f3..d1768a94 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java @@ -15,6 +15,8 @@ public class AppointmentResponse { private java.util.List customerPetNames; private java.util.List customerPetIds; private String serviceName; + private Long employeeId; + private String employeeName; private LocalDate appointmentDate; private LocalTime appointmentTime; private String appointmentStatus; @@ -110,6 +112,22 @@ public class AppointmentResponse { this.serviceName = serviceName; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + public LocalDate getAppointmentDate() { return appointmentDate; } 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 ec0fe4d0..fec5fae2 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 @@ -97,4 +97,12 @@ public class DropdownApi { } return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + + public List getStoreEmployees(Long storeId) throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/stores/" + storeId + "/employees"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from store employees endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java index 564e6205..0b8fe447 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java @@ -43,6 +43,9 @@ public class AdoptionController { @FXML private TableColumn colCustomerName; + @FXML + private TableColumn colEmployeeName; + @FXML private TableColumn colAdoptionDate; @@ -71,6 +74,7 @@ public class AdoptionController { colAdoptionId.setCellValueFactory(new PropertyValueFactory<>("adoptionId")); colPetId.setCellValueFactory(new PropertyValueFactory<>("petName")); colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName")); + colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName")); colAdoptionDate.setCellValueFactory(new PropertyValueFactory<>("adoptionDate")); colAdoptionFee.setCellValueFactory(new PropertyValueFactory<>("adoptionFee")); colAdoptionStatus.setCellValueFactory(new PropertyValueFactory<>("adoptionStatus")); @@ -252,8 +256,10 @@ public class AdoptionController { response.getAdoptionId().intValue(), response.getPetId() != null ? response.getPetId().intValue() : 0, response.getCustomerId() != null ? response.getCustomerId().intValue() : 0, + response.getEmployeeId() != null ? response.getEmployeeId().intValue() : 0, response.getPetName(), response.getCustomerName(), + response.getEmployeeName(), response.getAdoptionDate() != null ? response.getAdoptionDate().toString() : "", response.getAdoptionFee() != null ? response.getAdoptionFee().doubleValue() : 0.0, response.getAdoptionStatus() diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index e310c039..ba4c05aa 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -33,6 +33,7 @@ public class AppointmentController { @FXML private TableColumn colAppointmentDate; @FXML private TableColumn colAppointmentTime; @FXML private TableColumn colCustomerName; + @FXML private TableColumn colEmployeeName; @FXML private TableColumn colAppointmentStatus; @FXML private Button btnAdd; @@ -55,6 +56,7 @@ public class AppointmentController { colAppointmentDate.setCellValueFactory(new PropertyValueFactory<>("appointmentDate")); colAppointmentTime.setCellValueFactory(new PropertyValueFactory<>("appointmentTime")); colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName")); + colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName")); colAppointmentStatus.setCellValueFactory(new PropertyValueFactory<>("appointmentStatus")); filtered = new FilteredList<>(appointments, a -> true); @@ -247,6 +249,8 @@ public class AppointmentController { petName, response.getServiceId() != null ? response.getServiceId().intValue() : 0, response.getServiceName(), + response.getEmployeeId() != null ? response.getEmployeeId().intValue() : 0, + response.getEmployeeName(), response.getAppointmentDate().toString(), response.getAppointmentTime().toString(), response.getAppointmentStatus() 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 a1ed25fb..db7094c7 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 @@ -11,6 +11,7 @@ import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.DatePicker; import javafx.scene.control.Label; +import javafx.scene.control.ListCell; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest; @@ -38,6 +39,9 @@ public class AdoptionDialogController { @FXML private ComboBox cbCustomer; + @FXML + private ComboBox cbEmployee; + @FXML private ComboBox cbPet; @@ -62,6 +66,7 @@ public class AdoptionDialogController { void initialize() { cbAdoptionStatus.setItems(statusList); + cbEmployee.setPromptText("Select an employee"); new Thread(() -> { try { @@ -83,6 +88,38 @@ public class AdoptionDialogController { } }).start(); + new Thread(() -> { + try { + Long storeId = org.example.petshopdesktop.auth.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( + "AdoptionDialogController.initialize", + e, + "Loading employees for combo box"); + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Unable to load employees"); + }); + } + }).start(); + + cbEmployee.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + cbEmployee.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + new Thread(() -> { try { List customers = DropdownApi.getInstance().getCustomers(); @@ -129,6 +166,10 @@ public class AdoptionDialogController { errorMsg += "Customer is required.\n"; } + if (cbEmployee.getSelectionModel().getSelectedItem() == null) { + errorMsg += "Employee is required.\n"; + } + if (dpAdoptionDate.getValue() == null) { errorMsg += "Adoption Date is required.\n"; } @@ -142,6 +183,7 @@ public class AdoptionDialogController { AdoptionRequest request = new AdoptionRequest(); request.setPetId(cbPet.getSelectionModel().getSelectedItem().getId()); request.setCustomerId(cbCustomer.getSelectionModel().getSelectedItem().getId()); + request.setEmployeeId(cbEmployee.getSelectionModel().getSelectedItem().getId()); request.setAdoptionDate(dpAdoptionDate.getValue()); request.setAdoptionStatus(cbAdoptionStatus.getValue()); @@ -204,6 +246,15 @@ public class AdoptionDialogController { } } + if (adoption.getEmployeeId() > 0) { + for (DropdownOption employee : cbEmployee.getItems()) { + if (employee.getId() != null && employee.getId().equals(adoption.getEmployeeId())) { + cbEmployee.getSelectionModel().select(employee); + break; + } + } + } + if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) { try { dpAdoptionDate.setValue(LocalDate.parse(adoption.getAdoptionDate())); 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 81b54cb9..191e58ad 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 @@ -35,6 +35,7 @@ public class AppointmentDialogController { @FXML private ComboBox cbService; @FXML private ComboBox cbCustomer; @FXML private ComboBox cbPet; + @FXML private ComboBox cbEmployee; @FXML private ComboBox cbHour; @FXML private ComboBox cbMinute; @@ -94,14 +95,32 @@ public class AppointmentDialogController { ActivityLogger.getInstance().logException( "AppointmentDialogController.initialize", e, - "Loading combo box data for services, customers, and pets"); + "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"); cbPet.setPromptText("Select a customer first"); cbCustomer.setPromptText("Select a customer"); cbService.setPromptText("Select a service"); @@ -161,6 +180,21 @@ public class AppointmentDialogController { } }); + cbEmployee.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + cbEmployee.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + cbCustomer.valueProperty().addListener((obs, oldValue, newValue) -> { Long customerId = newValue != null ? newValue.getId() : null; cbPet.setValue(null); @@ -222,6 +256,14 @@ public class AppointmentDialogController { cbCustomer.setValue(c); } }); + + if (appt.getEmployeeId() > 0) { + cbEmployee.getItems().forEach(employee -> { + if (employee.getId() != null && employee.getId().longValue() == appt.getEmployeeId()) { + cbEmployee.setValue(employee); + } + }); + } } // @@ -233,6 +275,7 @@ public class AppointmentDialogController { if (cbService.getValue() == null || cbCustomer.getValue() == null || cbPet.getValue() == null || + cbEmployee.getValue() == null || dpAppointmentDate.getValue() == null || cbHour.getValue() == null || cbMinute.getValue() == null || @@ -254,6 +297,7 @@ public class AppointmentDialogController { request.setCustomerId(cbCustomer.getValue().getId()); request.setStoreId(storeId); request.setServiceId(cbService.getValue().getId()); + request.setEmployeeId(cbEmployee.getValue().getId()); request.setAppointmentDate(dpAppointmentDate.getValue()); request.setAppointmentTime(appointmentTime); request.setAppointmentStatus(cbAppointmentStatus.getValue()); diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java b/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java index c5c02f63..98d1f4a6 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java @@ -8,18 +8,22 @@ public class Adoption { private SimpleIntegerProperty adoptionId; private SimpleIntegerProperty petId; private SimpleIntegerProperty customerId; + private SimpleIntegerProperty employeeId; private SimpleStringProperty petName; private SimpleStringProperty customerName; + private SimpleStringProperty employeeName; private SimpleStringProperty adoptionDate; private SimpleDoubleProperty adoptionFee; private SimpleStringProperty adoptionStatus; - public Adoption(int adoptionId, int petId, int customerId, String petName, String customerName, String adoptionDate, double adoptionFee, String adoptionStatus) { + public Adoption(int adoptionId, int petId, int customerId, int employeeId, String petName, String customerName, String employeeName, String adoptionDate, double adoptionFee, String adoptionStatus) { this.adoptionId = new SimpleIntegerProperty(adoptionId); this.petId = new SimpleIntegerProperty(petId); this.customerId = new SimpleIntegerProperty(customerId); + this.employeeId = new SimpleIntegerProperty(employeeId); this.petName = new SimpleStringProperty(petName); this.customerName = new SimpleStringProperty(customerName); + this.employeeName = new SimpleStringProperty(employeeName); this.adoptionDate = new SimpleStringProperty(adoptionDate); this.adoptionFee = new SimpleDoubleProperty(adoptionFee); this.adoptionStatus = new SimpleStringProperty(adoptionStatus); @@ -43,6 +47,12 @@ public class Adoption { public SimpleIntegerProperty customerIdProperty() { return customerId; } + public int getEmployeeId() { return employeeId.get(); } + + public void setEmployeeId(int employeeId) { this.employeeId.set(employeeId); } + + public SimpleIntegerProperty employeeIdProperty() { return employeeId; } + public String getPetName() { return petName.get(); } public void setPetName(String petName) { this.petName.set(petName); } @@ -55,6 +65,12 @@ public class Adoption { public SimpleStringProperty customerNameProperty() { return customerName; } + public String getEmployeeName() { return employeeName.get(); } + + public void setEmployeeName(String employeeName) { this.employeeName.set(employeeName); } + + public SimpleStringProperty employeeNameProperty() { return employeeName; } + public String getAdoptionDate() { return adoptionDate.get(); } public void setAdoptionDate(String adoptionDate) { this.adoptionDate.set(adoptionDate); } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml index c3a14b62..b1140dad 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml @@ -73,6 +73,7 @@ + @@ -131,6 +132,20 @@ + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml index 99cf0833..f3cccdac 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml @@ -83,6 +83,7 @@ + @@ -190,9 +191,9 @@ - - - + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml index 9443ef14..896ed71e 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml @@ -68,11 +68,12 @@ - - - - - + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml index f698c5c1..8e920be0 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml @@ -68,12 +68,13 @@ - - - - - - + + + + + + + diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index 4643bf0e..f8e4ec2d 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -203,6 +203,7 @@ function AppointmentsPage() { const didPreselectRef = useRef(false); const [stores, setStores] = useState([]); + const [employees, setEmployees] = useState([]); const [services, setServices] = useState([]); const [allPets, setAllPets] = useState([]); const [customerPets, setCustomerPets] = useState([]); @@ -210,6 +211,7 @@ function AppointmentsPage() { const [storeId, setStoreId] = useState(""); const [serviceId, setServiceId] = useState(""); + const [employeeId, setEmployeeId] = useState(""); const [appointmentDate, setAppointmentDate] = useState(""); const [appointmentTime, setAppointmentTime] = useState(""); const [selectedPetIds, setSelectedPetIds] = useState([]); @@ -302,6 +304,33 @@ function AppointmentsPage() { loadAppointments(); }, [loadAppointments]); + useEffect(() => { + if (!token || !storeId) { + setEmployees([]); + setEmployeeId(""); + return; + } + + fetch(`${API_BASE}/api/v1/dropdowns/stores/${storeId}/employees`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((r) => r.json()) + .then((data) => setEmployees(Array.isArray(data) ? data : [])) + .catch(() => setEmployees([])); + }, [token, storeId]); + + useEffect(() => { + if (!employees.length) { + setEmployeeId(""); + return; + } + + const currentExists = employees.some((employee) => String(employee.id) === String(employeeId)); + if (!currentExists) { + setEmployeeId(String(employees[0].id)); + } + }, [employees, employeeId]); + useEffect(() => { if (!storeId || !serviceId || !appointmentDate) { setAvailableSlots([]); @@ -401,6 +430,7 @@ function AppointmentsPage() { customerId: user.customerId, storeId: Number(storeId), serviceId: Number(serviceId), + employeeId: employeeId ? Number(employeeId) : undefined, appointmentDate, appointmentTime: appointmentTime + ":00", appointmentStatus: "Booked", @@ -513,6 +543,21 @@ function AppointmentsPage() { + {employees.length > 0 && ( + + )} + {selectedService && (

{selectedService.serviceDesc}

-- 2.49.1 From 1f343f413298086cbddccb54f8fd37b51cbf3f52 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 15:51:11 -0600 Subject: [PATCH 02/12] Harden assignment rules --- ...calAppointmentCustomerSeedInitializer.java | 34 +++ .../controller/AdoptionController.java | 15 +- .../controller/DropdownController.java | 24 ++- .../repository/AdoptionRepository.java | 4 + .../repository/CustomerRepository.java | 3 + .../backend/repository/PetRepository.java | 4 + .../backend/service/AdoptionService.java | 61 +++++- .../backend/service/AppointmentService.java | 4 +- .../resources/dev/seed_demo_customer_pets.sql | 53 +++++ .../controller/DropdownControllerTest.java | 98 +++++++++ .../backend/service/AdoptionServiceTest.java | 25 +++ .../service/AppointmentServiceTest.java | 43 ++++ .../api/endpoints/DropdownApi.java | 16 ++ .../AdoptionDialogController.java | 83 +++++--- .../AppointmentDialogController.java | 199 +++++++++++------- 15 files changed, 546 insertions(+), 120 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java create mode 100644 backend/src/main/resources/dev/seed_demo_customer_pets.sql 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(); + } } -- 2.49.1 From 890391f9828c9bf93f2f9aa18c13e8852395eaa6 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 16:01:46 -0600 Subject: [PATCH 03/12] Allow admin ownership bypass --- .../controller/DropdownController.java | 11 +- .../backend/service/AdoptionService.java | 2 +- .../backend/service/AppointmentService.java | 21 +- .../controller/DropdownControllerTest.java | 142 +++++++------ .../backend/service/AdoptionServiceTest.java | 16 +- .../service/AppointmentServiceTest.java | 197 ++++++++---------- 6 files changed, 199 insertions(+), 190 deletions(-) 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 409891bc..b307174c 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -80,8 +80,15 @@ public class DropdownController { @GetMapping("/appointment-customers") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getAppointmentCustomers() { + User user = com.petshop.backend.util.AuthenticationHelper.getAuthenticatedUser(userRepository); + List customers; + if (user.getRole() == User.Role.ADMIN) { + customers = customerRepository.findAll(); + } else { + customers = customerRepository.findAllWithPets(); + } return ResponseEntity.ok( - customerRepository.findAllWithPets().stream() + customers.stream() .map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName())) .collect(Collectors.toList()) ); @@ -194,7 +201,7 @@ public class DropdownController { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } 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 c6ff1c60..9519b8c5 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -173,7 +173,7 @@ public class AdoptionService { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } 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 3b3d121d..867a8c98 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -99,6 +99,8 @@ public class AppointmentService { public AppointmentResponse createAppointment(AppointmentRequest request) { validateAppointmentRequest(request); + User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository); + Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); @@ -108,7 +110,7 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - validateStoreAccess(store.getStoreId()); + validateStoreAccess(store.getStoreId(), authenticatedUser); validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null); boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); @@ -120,7 +122,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId(), authenticatedUser.getRole()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); Appointment appointment = new Appointment(); @@ -142,6 +144,8 @@ public class AppointmentService { public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) { validateAppointmentRequest(request); + User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository); + Appointment appointment = appointmentRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id)); @@ -154,7 +158,7 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - validateStoreAccess(store.getStoreId()); + validateStoreAccess(store.getStoreId(), authenticatedUser); validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id); boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); @@ -166,7 +170,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId(), authenticatedUser.getRole()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); appointment.setCustomer(customer); @@ -251,12 +255,12 @@ public class AppointmentService { return pets; } - private Set fetchCustomerPets(List customerPetIds, Long customerId) { + private Set fetchCustomerPets(List customerPetIds, Long customerId, User.Role authenticatedRole) { Set customerPets = new HashSet<>(); for (Long customerPetId : customerPetIds) { CustomerPet customerPet = customerPetRepository.findById(customerPetId) .orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId)); - if (!customerPet.getCustomer().getCustomerId().equals(customerId)) { + if (authenticatedRole != User.Role.ADMIN && !customerPet.getCustomer().getCustomerId().equals(customerId)) { throw new IllegalArgumentException("Selected pet does not belong to the selected customer"); } customerPets.add(customerPet); @@ -333,7 +337,7 @@ public class AppointmentService { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } @@ -368,8 +372,7 @@ public class AppointmentService { return true; } - private void validateStoreAccess(Long requestedStoreId) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + private void validateStoreAccess(Long requestedStoreId, User user) { if (user.getRole() != User.Role.STAFF) { return; } 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 b7d97e74..e0adc2b6 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -15,7 +15,12 @@ import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.SupplierRepository; import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.security.AppPrincipal; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; import java.util.List; import java.util.Optional; @@ -26,20 +31,32 @@ import static org.mockito.Mockito.when; class DropdownControllerTest { - @Test - void getStoreEmployeesReturnsOnlyStaffLinkedEmployees() { - 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); + private PetRepository petRepository; + private CustomerRepository customerRepository; + private CustomerPetRepository customerPetRepository; + private ServiceRepository serviceRepository; + private ProductRepository productRepository; + private CategoryRepository categoryRepository; + private StoreRepository storeRepository; + private SupplierRepository supplierRepository; + private EmployeeStoreRepository employeeStoreRepository; + private UserRepository userRepository; + private DropdownController controller; - DropdownController controller = new DropdownController( + @BeforeEach + void setUp() { + petRepository = mock(PetRepository.class); + customerRepository = mock(CustomerRepository.class); + customerPetRepository = mock(CustomerPetRepository.class); + serviceRepository = mock(ServiceRepository.class); + productRepository = mock(ProductRepository.class); + categoryRepository = mock(CategoryRepository.class); + storeRepository = mock(StoreRepository.class); + supplierRepository = mock(SupplierRepository.class); + employeeStoreRepository = mock(EmployeeStoreRepository.class); + userRepository = mock(UserRepository.class); + + controller = new DropdownController( petRepository, customerRepository, customerPetRepository, @@ -51,7 +68,25 @@ class DropdownControllerTest { employeeStoreRepository, userRepository ); + } + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + private void setAuthentication(Long userId, User.Role role) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + new AppPrincipal(userId, "user", role, 0), + null, + List.of() + ) + ); + } + + @Test + void getStoreEmployeesReturnsBothStaffAndAdminLinkedEmployees() { StoreLocation store = new StoreLocation(); store.setStoreId(1L); @@ -86,37 +121,13 @@ class DropdownControllerTest { var response = controller.getStoreEmployees(1L); - assertEquals(1, response.getBody().size()); + assertEquals(2, response.getBody().size()); assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); - assertEquals("Alex Jones", response.getBody().get(0).getLabel()); + assertEquals(Long.valueOf(8L), response.getBody().get(1).getId()); } @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); @@ -142,30 +153,33 @@ class DropdownControllerTest { } @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); + void getAppointmentCustomersReturnsOnlyCustomersWithPetsForStaff() { + User staffUser = new User(); + staffUser.setId(99L); + staffUser.setRole(User.Role.STAFF); + when(userRepository.findById(99L)).thenReturn(Optional.of(staffUser)); + setAuthentication(99L, User.Role.STAFF); - 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"); + + when(customerRepository.findAllWithPets()).thenReturn(List.of(one)); + + var response = controller.getAppointmentCustomers(); + + assertEquals(1, response.getBody().size()); + assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); + } + + @Test + void getAppointmentCustomersReturnsAllCustomersForAdmin() { + User adminUser = new User(); + adminUser.setId(88L); + adminUser.setRole(User.Role.ADMIN); + when(userRepository.findById(88L)).thenReturn(Optional.of(adminUser)); + setAuthentication(88L, User.Role.ADMIN); Customer one = new Customer(); one.setCustomerId(1L); @@ -177,12 +191,12 @@ class DropdownControllerTest { two.setFirstName("Emily"); two.setLastName("Clark"); - when(customerRepository.findAllWithPets()).thenReturn(List.of(one, two)); + when(customerRepository.findAll()).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()); + assertEquals(Long.valueOf(2L), response.getBody().get(1).getId()); } } 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 6192ccb2..0f3a47a1 100644 --- a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -87,10 +87,11 @@ class AdoptionServiceTest { } @Test - void createAdoptionAutoAssignsFirstStaffEmployee() { + void createAdoptionAutoAssignsFirstAssignableEmployee() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee)); + // resolveAdoptionEmployee uses the first one from the list returned by repo + when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(staffEmployee, adminEmployee)); when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { Adoption adoption = invocation.getArgument(0); adoption.setAdoptionId(10L); @@ -110,10 +111,15 @@ class AdoptionServiceTest { } @Test - void createAdoptionRejectsAdminEmployeeSelection() { + void createAdoptionAllowsAdminEmployeeSelection() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); + when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { + Adoption adoption = invocation.getArgument(0); + adoption.setAdoptionId(10L); + return adoption; + }); AdoptionRequest request = new AdoptionRequest(); request.setPetId(1L); @@ -122,7 +128,9 @@ class AdoptionServiceTest { request.setAdoptionDate(LocalDate.now()); request.setAdoptionStatus("Pending"); - assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); + var response = adoptionService.createAdoption(request); + + assertEquals(8L, response.getEmployeeId()); } @Test 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 9d9daa40..7146248c 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -18,6 +18,7 @@ import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.security.AppPrincipal; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -41,39 +42,22 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class AppointmentServiceTest { - @Mock - private AppointmentRepository appointmentRepository; - - @Mock - private CustomerRepository customerRepository; - - @Mock - private CustomerPetRepository customerPetRepository; - - @Mock - private PetRepository petRepository; - - @Mock - private ServiceRepository serviceRepository; - - @Mock - private StoreRepository storeRepository; - - @Mock - private UserRepository userRepository; - - @Mock - private EmployeeRepository employeeRepository; - - @Mock - private EmployeeStoreRepository employeeStoreRepository; + @Mock private AppointmentRepository appointmentRepository; + @Mock private CustomerRepository customerRepository; + @Mock private CustomerPetRepository customerPetRepository; + @Mock private ServiceRepository serviceRepository; + @Mock private PetRepository petRepository; + @Mock private StoreRepository storeRepository; + @Mock private UserRepository userRepository; + @Mock private EmployeeRepository employeeRepository; + @Mock private EmployeeStoreRepository employeeStoreRepository; @InjectMocks private AppointmentService appointmentService; @@ -89,6 +73,13 @@ class AppointmentServiceTest { @BeforeEach void setUp() { + setAuthentication(99L, User.Role.ADMIN); + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + customer = new Customer(); customer.setCustomerId(1L); customer.setFirstName("Pat"); @@ -131,7 +122,6 @@ class AppointmentServiceTest { when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); date = LocalDate.now().plusDays(1); - } @AfterEach @@ -164,62 +154,10 @@ class AppointmentServiceTest { } @Test - void cancelledAppointmentsDoNotBlockAvailability() { - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); - - List slots = appointmentService.checkAvailability(1L, 1L, date); - - assertTrue(slots.contains("10:00")); - } - - @Test - void updateAppointmentDoesNotConflictWithItself() { - Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); - User user = new User(); - user.setId(10L); - user.setUsername("pat"); - user.setRole(User.Role.CUSTOMER); - user.setTokenVersion(0); - when(userRepository.findById(10L)).thenReturn(Optional.of(user)); - setAuthentication(10L, User.Role.CUSTOMER); - - when(appointmentRepository.findById(1L)).thenReturn(Optional.of(existing)); - 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(petRepository.findById(1L)).thenReturn(Optional.of(pet)); - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); - when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); - request.setCustomerId(1L); - request.setStoreId(1L); - request.setServiceId(1L); - request.setAppointmentDate(date); - request.setAppointmentTime(LocalTime.of(10, 0)); - request.setAppointmentStatus("Booked"); - request.setPetIds(List.of(1L)); - - var response = appointmentService.updateAppointment(1L, request); - - assertEquals(1L, response.getAppointmentId()); - assertEquals("Booked", response.getAppointmentStatus()); - } - - @Test - void createAppointmentRejectsCustomerPetOwnedByDifferentCustomer() { - 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); + void createAppointmentRejectsCustomerPetOwnedByDifferentCustomerForStaff() { + setAuthentication(7L, User.Role.STAFF); + when(employeeRepository.findByUserId(7L)).thenReturn(Optional.of(employee)); + when(employeeStoreRepository.findByEmployeeEmployeeId(7L)).thenReturn(Optional.of(new EmployeeStore(employee, store))); Customer otherCustomer = new Customer(); otherCustomer.setCustomerId(2L); @@ -250,26 +188,63 @@ class AppointmentServiceTest { } @Test - void createAppointmentAllowsCustomerOwnedCustomerPet() { - 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)); + void createAppointmentAllowsCustomerPetOwnedByDifferentCustomerForAdmin() { setAuthentication(99L, User.Role.ADMIN); + Customer otherCustomer = new Customer(); + otherCustomer.setCustomerId(2L); + + CustomerPet otherCustomerPet = new CustomerPet(); + otherCustomerPet.setCustomerPetId(22L); + otherCustomerPet.setCustomer(otherCustomer); + otherCustomerPet.setPetName("Not Yours"); + 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(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); - when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); + when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); + when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { + Appointment appt = invocation.getArgument(0); + appt.setAppointmentId(101L); + return appt; + }); + + var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); + request.setCustomerId(1L); + request.setStoreId(1L); + request.setServiceId(1L); + request.setAppointmentDate(date); + request.setAppointmentTime(LocalTime.of(10, 0)); + request.setAppointmentStatus("Booked"); + request.setCustomerPetIds(List.of(22L)); + + var response = appointmentService.createAppointment(request); + assertEquals(101L, response.getAppointmentId()); + } + + @Test + void createAppointmentAllowsAnyPetForAdmin() { + setAuthentication(99L, User.Role.ADMIN); + + Customer otherCustomer = new Customer(); + otherCustomer.setCustomerId(22L); + CustomerPet otherCustomerPet = new CustomerPet(); + otherCustomerPet.setCustomerPetId(22L); + otherCustomerPet.setCustomer(otherCustomer); + + 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(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { Appointment appointment = invocation.getArgument(0); - appointment.setAppointmentId(99L); + appointment.setAppointmentId(101L); return appointment; }); @@ -280,24 +255,19 @@ class AppointmentServiceTest { request.setAppointmentDate(date); request.setAppointmentTime(LocalTime.of(10, 0)); request.setAppointmentStatus("Booked"); - request.setCustomerPetIds(List.of(11L)); + request.setCustomerPetIds(List.of(22L)); var response = appointmentService.createAppointment(request); - assertEquals(99L, response.getAppointmentId()); + assertEquals(101L, response.getAppointmentId()); assertEquals(1L, response.getCustomerId()); - assertEquals(7L, response.getEmployeeId()); } @Test - void createAppointmentRejectsAdminEmployeeSelection() { - User adminUser = new User(); - adminUser.setId(99L); - adminUser.setUsername("admin"); - adminUser.setRole(User.Role.ADMIN); - adminUser.setTokenVersion(0); - when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); - setAuthentication(99L, User.Role.ADMIN); + void createAppointmentAllowsAdminEmployeeSelection() { + setAuthentication(7L, User.Role.STAFF); + when(employeeRepository.findByUserId(7L)).thenReturn(Optional.of(employee)); + when(employeeStoreRepository.findByEmployeeEmployeeId(7L)).thenReturn(Optional.of(new EmployeeStore(employee, store))); Employee adminEmployee = new Employee(); adminEmployee.setEmployeeId(8L); @@ -320,6 +290,11 @@ class AppointmentServiceTest { when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store))); + when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { + Appointment appointment = invocation.getArgument(0); + appointment.setAppointmentId(102L); + return appointment; + }); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); request.setCustomerId(1L); @@ -331,19 +306,20 @@ class AppointmentServiceTest { request.setAppointmentStatus("Booked"); request.setCustomerPetIds(List.of(11L)); - assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); + var response = appointmentService.createAppointment(request); + + assertEquals(102L, response.getAppointmentId()); + assertEquals(8L, response.getEmployeeId()); } @Test void createAppointmentRejectsInactiveStaffUserSelection() { + setAuthentication(99L, User.Role.ADMIN); 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); @@ -384,13 +360,14 @@ class AppointmentServiceTest { appointment.setEmployee(employee); appointment.setCustomer(customer); appointment.setPets(Set.of()); + appointment.setCustomerPets(Set.of()); return appointment; } private void setAuthentication(Long userId, User.Role role) { SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken( - new com.petshop.backend.security.AppPrincipal(userId, "user", role, 0), + new AppPrincipal(userId, "user", role, 0), "n/a", List.of(new SimpleGrantedAuthority("ROLE_" + role.name())) ) -- 2.49.1 From 153ec836cf1efa40a8c1be354aae6aacbc28bcd7 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 16:03:29 -0600 Subject: [PATCH 04/12] Restrict assignments to staff --- .../controller/DropdownController.java | 2 +- .../backend/service/AdoptionService.java | 2 +- .../backend/service/AppointmentService.java | 2 +- .../controller/DropdownControllerTest.java | 5 ++--- .../backend/service/AdoptionServiceTest.java | 17 +++++---------- .../service/AppointmentServiceTest.java | 21 +++++++------------ 6 files changed, 18 insertions(+), 31 deletions(-) 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 b307174c..2217b4e7 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -201,7 +201,7 @@ public class DropdownController { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) + .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/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 9519b8c5..c6ff1c60 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -173,7 +173,7 @@ public class AdoptionService { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) + .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/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 867a8c98..363cb3a0 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -337,7 +337,7 @@ public class AppointmentService { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) + .filter(user -> user.getRole() == User.Role.STAFF) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } 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 e0adc2b6..563e5d2f 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -86,7 +86,7 @@ class DropdownControllerTest { } @Test - void getStoreEmployeesReturnsBothStaffAndAdminLinkedEmployees() { + void getStoreEmployeesReturnsOnlyStaffLinkedEmployees() { StoreLocation store = new StoreLocation(); store.setStoreId(1L); @@ -121,9 +121,8 @@ class DropdownControllerTest { var response = controller.getStoreEmployees(1L); - assertEquals(2, response.getBody().size()); + assertEquals(1, response.getBody().size()); assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); - assertEquals(Long.valueOf(8L), response.getBody().get(1).getId()); } @Test 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 0f3a47a1..1fef0e42 100644 --- a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -87,11 +87,11 @@ class AdoptionServiceTest { } @Test - void createAdoptionAutoAssignsFirstAssignableEmployee() { + void createAdoptionAutoAssignsFirstStaffEmployee() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - // resolveAdoptionEmployee uses the first one from the list returned by repo - when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(staffEmployee, adminEmployee)); + // resolveAdoptionEmployee filters for staff + when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee)); when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { Adoption adoption = invocation.getArgument(0); adoption.setAdoptionId(10L); @@ -111,15 +111,10 @@ class AdoptionServiceTest { } @Test - void createAdoptionAllowsAdminEmployeeSelection() { + void createAdoptionRejectsAdminEmployeeSelection() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); - when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { - Adoption adoption = invocation.getArgument(0); - adoption.setAdoptionId(10L); - return adoption; - }); AdoptionRequest request = new AdoptionRequest(); request.setPetId(1L); @@ -128,9 +123,7 @@ class AdoptionServiceTest { request.setAdoptionDate(LocalDate.now()); request.setAdoptionStatus("Pending"); - var response = adoptionService.createAdoption(request); - - assertEquals(8L, response.getEmployeeId()); + assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); } @Test 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 7146248c..d4892126 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -264,10 +264,13 @@ class AppointmentServiceTest { } @Test - void createAppointmentAllowsAdminEmployeeSelection() { - setAuthentication(7L, User.Role.STAFF); - when(employeeRepository.findByUserId(7L)).thenReturn(Optional.of(employee)); - when(employeeStoreRepository.findByEmployeeEmployeeId(7L)).thenReturn(Optional.of(new EmployeeStore(employee, store))); + void createAppointmentRejectsAdminEmployeeSelection() { + setAuthentication(99L, User.Role.ADMIN); + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); Employee adminEmployee = new Employee(); adminEmployee.setEmployeeId(8L); @@ -290,11 +293,6 @@ class AppointmentServiceTest { when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store))); - when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { - Appointment appointment = invocation.getArgument(0); - appointment.setAppointmentId(102L); - return appointment; - }); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); request.setCustomerId(1L); @@ -306,10 +304,7 @@ class AppointmentServiceTest { request.setAppointmentStatus("Booked"); request.setCustomerPetIds(List.of(11L)); - var response = appointmentService.createAppointment(request); - - assertEquals(102L, response.getAppointmentId()); - assertEquals(8L, response.getEmployeeId()); + assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); } @Test -- 2.49.1 From 521537dc8f8642e97fe2e92544dff61f2b4b3600 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 16:17:58 -0600 Subject: [PATCH 05/12] Enforce staff-only assignments --- .../resources/db/migration/V16__activate_all_employees.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V16__activate_all_employees.sql diff --git a/backend/src/main/resources/db/migration/V16__activate_all_employees.sql b/backend/src/main/resources/db/migration/V16__activate_all_employees.sql new file mode 100644 index 00000000..cbabc11d --- /dev/null +++ b/backend/src/main/resources/db/migration/V16__activate_all_employees.sql @@ -0,0 +1,5 @@ +-- Activate all employees in the users table so they appear in dropdowns +UPDATE users u +SET u.active = TRUE +WHERE u.role IN ('STAFF', 'ADMIN') + AND EXISTS (SELECT 1 FROM employee e WHERE e.user_id = u.id); -- 2.49.1 From a3d454e1191852622385123bc73b870c8f815bb4 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 23:35:05 -0600 Subject: [PATCH 06/12] Enforce pet ownership rules --- .../controller/DropdownController.java | 9 +--- .../backend/service/AppointmentService.java | 8 ++-- .../controller/DropdownControllerTest.java | 12 ++--- .../service/AppointmentServiceTest.java | 48 +------------------ 4 files changed, 10 insertions(+), 67 deletions(-) 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 2217b4e7..409891bc 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -80,15 +80,8 @@ public class DropdownController { @GetMapping("/appointment-customers") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getAppointmentCustomers() { - User user = com.petshop.backend.util.AuthenticationHelper.getAuthenticatedUser(userRepository); - List customers; - if (user.getRole() == User.Role.ADMIN) { - customers = customerRepository.findAll(); - } else { - customers = customerRepository.findAllWithPets(); - } return ResponseEntity.ok( - customers.stream() + customerRepository.findAllWithPets().stream() .map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName())) .collect(Collectors.toList()) ); 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 363cb3a0..8c0e05d8 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -122,7 +122,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId(), authenticatedUser.getRole()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); Appointment appointment = new Appointment(); @@ -170,7 +170,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId(), authenticatedUser.getRole()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); appointment.setCustomer(customer); @@ -255,12 +255,12 @@ public class AppointmentService { return pets; } - private Set fetchCustomerPets(List customerPetIds, Long customerId, User.Role authenticatedRole) { + private Set fetchCustomerPets(List customerPetIds, Long customerId) { Set customerPets = new HashSet<>(); for (Long customerPetId : customerPetIds) { CustomerPet customerPet = customerPetRepository.findById(customerPetId) .orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId)); - if (authenticatedRole != User.Role.ADMIN && !customerPet.getCustomer().getCustomerId().equals(customerId)) { + if (!customerPet.getCustomer().getCustomerId().equals(customerId)) { throw new IllegalArgumentException("Selected pet does not belong to the selected customer"); } customerPets.add(customerPet); 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 563e5d2f..fa0a0ebe 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -173,7 +173,7 @@ class DropdownControllerTest { } @Test - void getAppointmentCustomersReturnsAllCustomersForAdmin() { + void getAppointmentCustomersReturnsOnlyCustomersWithPetsForAdmin() { User adminUser = new User(); adminUser.setId(88L); adminUser.setRole(User.Role.ADMIN); @@ -185,17 +185,11 @@ class DropdownControllerTest { one.setFirstName("Alex"); one.setLastName("Brown"); - Customer two = new Customer(); - two.setCustomerId(2L); - two.setFirstName("Emily"); - two.setLastName("Clark"); - - when(customerRepository.findAll()).thenReturn(List.of(one, two)); + when(customerRepository.findAllWithPets()).thenReturn(List.of(one)); var response = controller.getAppointmentCustomers(); - assertEquals(2, response.getBody().size()); + assertEquals(1, response.getBody().size()); assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); - assertEquals(Long.valueOf(2L), response.getBody().get(1).getId()); } } 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 d4892126..aecb5644 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -188,7 +188,7 @@ class AppointmentServiceTest { } @Test - void createAppointmentAllowsCustomerPetOwnedByDifferentCustomerForAdmin() { + void createAppointmentRejectsCustomerPetOwnedByDifferentCustomer() { setAuthentication(99L, User.Role.ADMIN); Customer otherCustomer = new Customer(); @@ -206,11 +206,6 @@ class AppointmentServiceTest { .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); - when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { - Appointment appt = invocation.getArgument(0); - appt.setAppointmentId(101L); - return appt; - }); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); request.setCustomerId(1L); @@ -221,46 +216,7 @@ class AppointmentServiceTest { request.setAppointmentStatus("Booked"); request.setCustomerPetIds(List.of(22L)); - var response = appointmentService.createAppointment(request); - assertEquals(101L, response.getAppointmentId()); - } - - @Test - void createAppointmentAllowsAnyPetForAdmin() { - setAuthentication(99L, User.Role.ADMIN); - - Customer otherCustomer = new Customer(); - otherCustomer.setCustomerId(22L); - CustomerPet otherCustomerPet = new CustomerPet(); - otherCustomerPet.setCustomerPetId(22L); - otherCustomerPet.setCustomer(otherCustomer); - - 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(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); - when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); - when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { - Appointment appointment = invocation.getArgument(0); - appointment.setAppointmentId(101L); - return appointment; - }); - - var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); - request.setCustomerId(1L); - request.setStoreId(1L); - request.setServiceId(1L); - request.setAppointmentDate(date); - request.setAppointmentTime(LocalTime.of(10, 0)); - request.setAppointmentStatus("Booked"); - request.setCustomerPetIds(List.of(22L)); - - var response = appointmentService.createAppointment(request); - - assertEquals(101L, response.getAppointmentId()); - assertEquals(1L, response.getCustomerId()); + assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); } @Test -- 2.49.1 From b70afd66aa424170a81acbebeadd01dd3c1b642c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 23:58:21 -0600 Subject: [PATCH 07/12] Allow cross-store staff selection --- .../controller/DropdownController.java | 13 ++++++--- .../repository/EmployeeStoreRepository.java | 3 +++ .../controller/DropdownControllerTest.java | 27 +++++++++++++++++++ .../api/endpoints/DropdownApi.java | 8 ++++++ .../AdoptionDialogController.java | 7 ++++- .../AppointmentDialogController.java | 9 ++++--- 6 files changed, 60 insertions(+), 7 deletions(-) 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 409891bc..d0a69036 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -156,13 +156,20 @@ public class DropdownController { ); } - @GetMapping("/stores/{storeId}/employees") + @GetMapping({"/stores/{storeId}/employees", "/employees"}) @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") - public ResponseEntity> getStoreEmployees(@PathVariable Long storeId) { + public ResponseEntity> getStoreEmployees(@PathVariable(required = false) Long storeId) { + List employees; + if (storeId == null || storeId == 0) { + employees = employeeStoreRepository.findActiveAllOrderByEmployeeEmployeeIdAsc(); + } else { + employees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId); + } return ResponseEntity.ok( - employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() + employees.stream() .filter(this::isAssignableEmployee) .map(this::toEmployeeOption) + .distinct() .collect(Collectors.toList()) ); } diff --git a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java index 0cc3f771..16a59cea 100644 --- a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java @@ -15,4 +15,7 @@ public interface EmployeeStoreRepository extends JpaRepository findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(@Param("storeId") Long storeId); + + @Query("SELECT es FROM EmployeeStore es WHERE es.employee.isActive = true ORDER BY es.employee.employeeId ASC") + List findActiveAllOrderByEmployeeEmployeeIdAsc(); } 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 fa0a0ebe..e62430e6 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -125,6 +125,33 @@ class DropdownControllerTest { assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); } + @Test + void getStoreEmployeesReturnsAllStaffWhenStoreIdIsNull() { + StoreLocation store = new StoreLocation(); + store.setStoreId(1L); + + Employee staffEmployee = new Employee(); + staffEmployee.setEmployeeId(7L); + staffEmployee.setUserId(7L); + staffEmployee.setFirstName("Alex"); + staffEmployee.setLastName("Jones"); + staffEmployee.setIsActive(true); + + User staffUser = new User(); + staffUser.setId(7L); + staffUser.setRole(User.Role.STAFF); + staffUser.setActive(true); + + when(employeeStoreRepository.findActiveAllOrderByEmployeeEmployeeIdAsc()) + .thenReturn(List.of(new EmployeeStore(staffEmployee, store))); + when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); + + var response = controller.getStoreEmployees(null); + + assertEquals(1, response.getBody().size()); + assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); + } + @Test void getStoreEmployeesExcludesInactiveStaffUsers() { StoreLocation store = new StoreLocation(); 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 05b2785c..127d75db 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 @@ -121,4 +121,12 @@ public class DropdownApi { } return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + + public List getEmployees() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/employees"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from all employees endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } } 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 3a331711..56f682c6 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 @@ -95,7 +95,12 @@ public class AdoptionDialogController { new Thread(() -> { try { Long storeId = UserSession.getInstance().getStoreId(); - List employees = storeId != null && storeId > 0 ? DropdownApi.getInstance().getStoreEmployees(storeId) : List.of(); + List employees; + if (storeId != null && storeId > 0) { + employees = DropdownApi.getInstance().getStoreEmployees(storeId); + } else { + employees = DropdownApi.getInstance().getEmployees(); + } Platform.runLater(() -> { cbEmployee.setItems(FXCollections.observableArrayList(employees)); applySelectedEmployee(); 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 472ca7b4..69cfd412 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 @@ -427,9 +427,12 @@ public class AppointmentDialogController { new Thread(() -> { try { Long storeId = UserSession.getInstance().getStoreId(); - List employees = storeId != null && storeId > 0 - ? DropdownApi.getInstance().getStoreEmployees(storeId) - : List.of(); + List employees; + if (storeId != null && storeId > 0) { + employees = DropdownApi.getInstance().getStoreEmployees(storeId); + } else { + employees = DropdownApi.getInstance().getEmployees(); + } Platform.runLater(() -> { cbEmployee.setItems(FXCollections.observableArrayList(employees)); applySelectedEmployee(); -- 2.49.1 From 9ea5efe44ebea2fda7e2a524e04beb64063bbf66 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 00:18:49 -0600 Subject: [PATCH 08/12] Fix employee time conflicts --- .../repository/AppointmentRepository.java | 3 ++ .../backend/service/AppointmentService.java | 45 ++++++++----------- .../V17__normalize_appointment_pets.sql | 28 ++++++++++++ .../service/AppointmentServiceTest.java | 41 +++++++++-------- 4 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql 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 5c7b6ec0..2a78244b 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -36,4 +36,7 @@ public interface AppointmentRepository extends JpaRepository "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))") Page searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); + + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) <> 'cancelled'") + List findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); } 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 8c0e05d8..310c539d 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -110,14 +110,10 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - validateStoreAccess(store.getStoreId(), authenticatedUser); - validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null); - boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty(); if (!hasPetIds && !hasCustomerPetIds) { - throw new IllegalArgumentException("Please specify at least one pet."); } @@ -125,6 +121,9 @@ public class AppointmentService { Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); + validateStoreAccess(store.getStoreId(), authenticatedUser); + validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null); + Appointment appointment = new Appointment(); appointment.setCustomer(customer); appointment.setStore(store); @@ -158,14 +157,10 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - validateStoreAccess(store.getStoreId(), authenticatedUser); - validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id); - boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty(); if (!hasPetIds && !hasCustomerPetIds) { - throw new IllegalArgumentException("Please specify at least one pet."); } @@ -173,6 +168,9 @@ public class AppointmentService { Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); + validateStoreAccess(store.getStoreId(), authenticatedUser); + validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id); + appointment.setCustomer(customer); appointment.setStore(store); appointment.setService(service); @@ -201,7 +199,6 @@ public class AppointmentService { } @Transactional(readOnly = true) - public List checkAvailability(Long storeId, Long serviceId, LocalDate date) { storeRepository.findById(storeId) .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId)); @@ -209,16 +206,10 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId)); - - //-------------------------------------------------------------------------- - // CHANGED: filter by serviceId too - List existingAppointments = appointmentRepository - .findByStoreAndDate(storeId, date) - .stream() - .filter(a -> a.getService().getServiceId().equals(serviceId)) + List assignableEmployees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() + .filter(es -> isAssignableEmployee(es.getEmployee())) + .map(EmployeeStore::getEmployee) .collect(Collectors.toList()); - // ------------------------------------------------------- - List availableSlots = new ArrayList<>(); LocalTime startTime = LocalTime.of(9, 0); @@ -227,7 +218,13 @@ public class AppointmentService { LocalTime currentTime = startTime; while (!currentTime.isAfter(latestStart)) { - if (isSlotAvailable(existingAppointments, service, currentTime, null)) { + final LocalTime slotTime = currentTime; + boolean anyEmployeeAvailable = assignableEmployees.stream().anyMatch(emp -> { + List empAppointments = appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(emp.getEmployeeId(), date); + return isSlotAvailable(empAppointments, service, slotTime, null); + }); + + if (anyEmployeeAvailable) { availableSlots.add(currentTime.toString()); } currentTime = currentTime.plusMinutes(30); @@ -343,15 +340,11 @@ public class AppointmentService { } //------------------------------------ - private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { - // Filter by same service only - different services can run at same time + private void validateAvailability(Employee employee, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { List existingAppointments = appointmentRepository - .findByStoreAndDate(store.getStoreId(), date) - .stream() - .filter(a -> a.getService().getServiceId().equals(service.getServiceId())) - .collect(Collectors.toList()); + .findByEmployeeEmployeeIdAndAppointmentDate(employee.getEmployeeId(), date); if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { - throw new IllegalArgumentException("Appointment time is not available for the selected store and service"); + throw new IllegalArgumentException("The selected employee is already booked for this time slot"); } } diff --git a/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql b/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql new file mode 100644 index 00000000..00d34751 --- /dev/null +++ b/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql @@ -0,0 +1,28 @@ +-- V17: Normalize legacy appointmentPet data into customer_pet and appointment_customer_pet + +-- Step 1: Ensure a customer_pet exists for every pet linked in appointmentPet +-- Note: pet species and breed might be null in pet table, but we copy them over if present +INSERT INTO customer_pet (customer_id, pet_name, species, breed) +SELECT DISTINCT a.customerId, p.petName, p.petSpecies, p.petBreed +FROM appointmentPet ap +JOIN appointment a ON a.appointmentId = ap.appointmentId +JOIN pet p ON p.petId = ap.petId +WHERE NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = a.customerId AND cp.pet_name = p.petName +); + +-- Step 2: Link the appointment to the customer_pet +INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id) +SELECT ap.appointmentId, cp.customer_pet_id +FROM appointmentPet ap +JOIN appointment a ON a.appointmentId = ap.appointmentId +JOIN pet p ON p.petId = ap.petId +JOIN customer_pet cp ON cp.customer_id = a.customerId AND cp.pet_name = p.petName +WHERE NOT EXISTS ( + SELECT 1 FROM appointment_customer_pet acp + WHERE acp.appointment_id = ap.appointmentId AND acp.customer_pet_id = cp.customer_pet_id +); + +-- Step 3: Remove the old legacy relationships so it strictly uses the new ones +DELETE FROM appointmentPet; 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 aecb5644..3e6f2d89 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -130,29 +130,34 @@ class AppointmentServiceTest { } @Test - void checkAvailabilityAllowsDifferentServicesAtSameTime() { + void checkAvailabilityAllowsConcurrentAppointmentsIfAnotherEmployeeFree() { + Employee employee2 = new Employee(); + employee2.setEmployeeId(8L); + employee2.setUserId(8L); + employee2.setFirstName("Bob"); + employee2.setIsActive(true); + + User staffUser2 = new User(); + staffUser2.setId(8L); + staffUser2.setRole(User.Role.STAFF); + staffUser2.setActive(true); + when(userRepository.findById(8L)).thenReturn(Optional.of(staffUser2)); + Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(2L)).thenReturn(Optional.of(nailTrim)); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); + + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store), new EmployeeStore(employee2, store))); + + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of(existing)); + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(8L, date)).thenReturn(List.of()); List slots = appointmentService.checkAvailability(1L, 2L, date); assertTrue(slots.contains("10:00")); } - @Test - void checkAvailabilityBlocksSameServiceAtSameTime() { - Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); - - List slots = appointmentService.checkAvailability(1L, 1L, date); - - assertFalse(slots.contains("10:00")); - } - @Test void createAppointmentRejectsCustomerPetOwnedByDifferentCustomerForStaff() { setAuthentication(7L, User.Role.STAFF); @@ -172,7 +177,7 @@ class AppointmentServiceTest { when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); @@ -204,7 +209,7 @@ class AppointmentServiceTest { when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); @@ -245,7 +250,7 @@ class AppointmentServiceTest { when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store))); @@ -282,7 +287,7 @@ class AppointmentServiceTest { 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(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(employee, store))); -- 2.49.1 From 661c9b006a2e984e89faadfba57e653360e2cc1a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 00:39:37 -0600 Subject: [PATCH 09/12] Add Missed status --- .../backend/repository/AppointmentRepository.java | 4 ++-- .../db/migration/V18__past_appointments_missed.sql | 10 ++++++++++ .../dialogcontrollers/AppointmentDialogController.java | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V18__past_appointments_missed.sql 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 2a78244b..940f7cf2 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -18,7 +18,7 @@ public interface AppointmentRepository extends JpaRepository @Query("SELECT a FROM Appointment a WHERE a.appointmentDate = :date AND a.appointmentTime = :time") List findByDateAndTime(@Param("date") LocalDate date, @Param("time") LocalTime time); - @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.store.storeId = :storeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) <> 'cancelled'") + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.store.storeId = :storeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByStoreAndDate(@Param("storeId") Long storeId, @Param("date") LocalDate date); @Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE " + @@ -37,6 +37,6 @@ public interface AppointmentRepository extends JpaRepository "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))") Page searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); - @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) <> 'cancelled'") + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); } diff --git a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql new file mode 100644 index 00000000..1f3a6707 --- /dev/null +++ b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql @@ -0,0 +1,10 @@ +-- V18: Normalize past appointments. +-- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'. + +UPDATE appointment +SET appointmentStatus = 'Missed' +WHERE LOWER(appointmentStatus) = 'booked' + AND ( + appointmentDate < CURRENT_DATE + OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME) + ); 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 69cfd412..cac4f47e 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 @@ -57,7 +57,7 @@ public class AppointmentDialogController { private ObservableList statusList = FXCollections.observableArrayList( - "Booked", "Completed", "Cancelled" + "Booked", "Completed", "Cancelled", "Missed" ); // -- 2.49.1 From 419e5302f690d0094692f0b7da022064959e19af Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 01:51:58 -0600 Subject: [PATCH 10/12] Fix availability checks --- .../repository/AppointmentRepository.java | 3 ++ .../backend/service/AppointmentService.java | 13 ++++++- .../V18__past_appointments_missed.sql | 39 ++++++++++++++++++- .../controller/DropdownControllerTest.java | 21 ---------- 4 files changed, 52 insertions(+), 24 deletions(-) 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 940f7cf2..f8649671 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -39,4 +39,7 @@ public interface AppointmentRepository extends JpaRepository @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); + + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId IN :employeeIds AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") + List findByEmployeeEmployeeIdInAndAppointmentDate(@Param("employeeIds") List employeeIds, @Param("date") LocalDate date); } 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 310c539d..b4e270fa 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -211,6 +211,17 @@ public class AppointmentService { .map(EmployeeStore::getEmployee) .collect(Collectors.toList()); + if (assignableEmployees.isEmpty()) { + return List.of(); + } + + List employeeIds = assignableEmployees.stream().map(Employee::getEmployeeId).collect(Collectors.toList()); + List allAppointments = appointmentRepository.findByEmployeeEmployeeIdInAndAppointmentDate(employeeIds, date); + + // Group by employee for faster lookup in the loop + java.util.Map> appointmentsByEmployee = allAppointments.stream() + .collect(Collectors.groupingBy(a -> a.getEmployee().getEmployeeId())); + List availableSlots = new ArrayList<>(); LocalTime startTime = LocalTime.of(9, 0); LocalTime endTime = LocalTime.of(17, 0); @@ -220,7 +231,7 @@ public class AppointmentService { while (!currentTime.isAfter(latestStart)) { final LocalTime slotTime = currentTime; boolean anyEmployeeAvailable = assignableEmployees.stream().anyMatch(emp -> { - List empAppointments = appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(emp.getEmployeeId(), date); + List empAppointments = appointmentsByEmployee.getOrDefault(emp.getEmployeeId(), List.of()); return isSlotAvailable(empAppointments, service, slotTime, null); }); diff --git a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql index 1f3a6707..043cad4e 100644 --- a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql +++ b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql @@ -1,6 +1,7 @@ --- V18: Normalize past appointments. --- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'. +-- V18: Normalize past appointments and resolve initial employee double-bookings +-- Part 1: Normalize past appointments. +-- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'. UPDATE appointment SET appointmentStatus = 'Missed' WHERE LOWER(appointmentStatus) = 'booked' @@ -8,3 +9,37 @@ WHERE LOWER(appointmentStatus) = 'booked' appointmentDate < CURRENT_DATE OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME) ); + +-- Part 2: Resolve potential double-bookings caused by V15's simple backfill. +-- We try to spread overlapping appointments among other active staff in the same store. +-- This is a one-time cleanup for demo data integrity. + +-- Temporary table to find conflicts (same employee, same date, overlapping time) +-- For simplicity in SQL, we just check exact same time for the demo data cleanup. +UPDATE appointment a1 +SET a1.employeeId = ( + SELECT es.employeeId + FROM employeeStore es + JOIN employee e ON e.employeeId = es.employeeId + JOIN users u ON u.id = e.user_id + WHERE es.storeId = a1.storeId + AND e.isActive = TRUE + AND u.role = 'STAFF' + -- Find an employee who DOES NOT have an appointment at this exact time + AND NOT EXISTS ( + SELECT 1 FROM appointment a2 + WHERE a2.employeeId = es.employeeId + AND a2.appointmentDate = a1.appointmentDate + AND a2.appointmentTime = a1.appointmentTime + AND a2.appointmentId <> a1.appointmentId + ) + ORDER BY es.employeeId ASC + LIMIT 1 +) +WHERE EXISTS ( + SELECT 1 FROM appointment a3 + WHERE a3.employeeId = a1.employeeId + AND a3.appointmentDate = a1.appointmentDate + AND a3.appointmentTime = a1.appointmentTime + AND a3.appointmentId < a1.appointmentId +) AND LOWER(a1.appointmentStatus) NOT IN ('cancelled', 'missed'); 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 e62430e6..e7caa86e 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -198,25 +198,4 @@ class DropdownControllerTest { assertEquals(1, response.getBody().size()); assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); } - - @Test - void getAppointmentCustomersReturnsOnlyCustomersWithPetsForAdmin() { - User adminUser = new User(); - adminUser.setId(88L); - adminUser.setRole(User.Role.ADMIN); - when(userRepository.findById(88L)).thenReturn(Optional.of(adminUser)); - setAuthentication(88L, User.Role.ADMIN); - - Customer one = new Customer(); - one.setCustomerId(1L); - one.setFirstName("Alex"); - one.setLastName("Brown"); - - when(customerRepository.findAllWithPets()).thenReturn(List.of(one)); - - var response = controller.getAppointmentCustomers(); - - assertEquals(1, response.getBody().size()); - assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); - } } -- 2.49.1 From b5b829013106737e38f87868239b55c0d493f3af Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 13:35:01 -0600 Subject: [PATCH 11/12] Fix Flyway migration --- .gitignore | 3 ++ .../config/FlywayContextInitializer.java | 7 ++-- .../V18__past_appointments_missed.sql | 39 ++++++++++--------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 733370d6..7a998c43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *.zip .local/ +commit-patches/ +temp_photos/ +uploads/ diff --git a/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java b/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java index 000ebe86..746d4682 100644 --- a/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java +++ b/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java @@ -35,13 +35,14 @@ public class FlywayContextInitializer implements ApplicationContextInitializer a1.appointmentId + SELECT 1 FROM ( + SELECT employeeId, appointmentDate, appointmentTime, appointmentId + FROM appointment + ) snap + WHERE snap.employeeId = es.employeeId + AND snap.appointmentDate = a1.appointmentDate + AND snap.appointmentTime = a1.appointmentTime + AND snap.appointmentId <> a1.appointmentId ) ORDER BY es.employeeId ASC LIMIT 1 -) -WHERE EXISTS ( - SELECT 1 FROM appointment a3 - WHERE a3.employeeId = a1.employeeId - AND a3.appointmentDate = a1.appointmentDate - AND a3.appointmentTime = a1.appointmentTime - AND a3.appointmentId < a1.appointmentId -) AND LOWER(a1.appointmentStatus) NOT IN ('cancelled', 'missed'); +); -- 2.49.1 From cd5dd32c731cf0b2b12e3e975da93176c6d2e574 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 13:26:55 -0600 Subject: [PATCH 12/12] Update Postman collection --- .../petstoremobile/dtos/AdoptionDTO.java | 1 - .../petstoremobile/dtos/AppointmentDTO.java | 8 +- backend/petshop-api.postman_collection.json | 132 +++++++++++++++++- .../backend/service/AppointmentService.java | 4 - .../migration/V16__activate_all_employees.sql | 1 - .../V17__normalize_appointment_pets.sql | 6 - .../V18__past_appointments_missed.sql | 6 - .../backend/service/AdoptionServiceTest.java | 2 +- .../petshopdesktop/DTOs/AppointmentDTO.java | 2 - .../controllers/AdoptionController.java | 8 +- .../controllers/AppointmentController.java | 8 +- .../AdoptionDialogController.java | 4 - .../AppointmentDialogController.java | 32 +---- 13 files changed, 135 insertions(+), 79 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java index 6866f6b0..daf7d768 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java @@ -16,7 +16,6 @@ public class AdoptionDTO { private String createdAt; private String updatedAt; - // Constructor for create/update requests public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus) { this(petId, customerId, null, adoptionDate, adoptionStatus); } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java index 05f9ea21..01f8ef5d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java @@ -4,7 +4,7 @@ import java.math.BigDecimal; import java.util.List; public class AppointmentDTO { - // Response fields (from server) + private Long appointmentId; private Long customerId; private String customerName; @@ -22,8 +22,6 @@ public class AppointmentDTO { private String createdAt; private String updatedAt; - // Constructor for CREATE/UPDATE request body - // Matches AppointmentRequest exactly public AppointmentDTO(Long customerId, Long storeId, Long serviceId, String appointmentDate, String appointmentTime, String appointmentStatus, List petIds) { @@ -43,7 +41,6 @@ public class AppointmentDTO { this.petIds = petIds; } - // Getters public Long getAppointmentId() { return appointmentId; } @@ -108,7 +105,6 @@ public class AppointmentDTO { return updatedAt; } - // Convenience getters for adapter/list display public String getPetName() { return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : ""; } @@ -121,7 +117,6 @@ public class AppointmentDTO { return getPetID(); } - // Keep old name so adapter doesn't break public String getServiceType() { return serviceName; } @@ -130,7 +125,6 @@ public class AppointmentDTO { return serviceId; } - // Status alias public String getStatus() { return appointmentStatus; } diff --git a/backend/petshop-api.postman_collection.json b/backend/petshop-api.postman_collection.json index 7db7d61f..73fed551 100644 --- a/backend/petshop-api.postman_collection.json +++ b/backend/petshop-api.postman_collection.json @@ -2069,6 +2069,37 @@ { "name": "Appointments", "item": [ + { + "name": "Get Appointment Customers Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/appointment-customers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, { "name": "Check Appointment Availability", "request": { @@ -2180,7 +2211,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1]\n}", + "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}", "options": { "raw": { "language": "json" @@ -2222,7 +2253,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1]\n}", + "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}", "options": { "raw": { "language": "json" @@ -2315,6 +2346,37 @@ { "name": "Adoptions", "item": [ + { + "name": "Get Adoption Pets Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/adoption-pets", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, { "name": "List Adoptions", "request": { @@ -2395,7 +2457,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\"\n}", + "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\",\n \"employeeId\": 1\n}", "options": { "raw": { "language": "json" @@ -2437,7 +2499,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\"\n}", + "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\",\n \"employeeId\": 1\n}", "options": { "raw": { "language": "json" @@ -3719,6 +3781,68 @@ } ] }, + { + "name": "Get Store Employees Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/stores/{{storeId}}/employees", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Get All Employees Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/employees", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, { "name": "List Stores", "request": { 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 b4e270fa..155b7524 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -218,7 +218,6 @@ public class AppointmentService { List employeeIds = assignableEmployees.stream().map(Employee::getEmployeeId).collect(Collectors.toList()); List allAppointments = appointmentRepository.findByEmployeeEmployeeIdInAndAppointmentDate(employeeIds, date); - // Group by employee for faster lookup in the loop java.util.Map> appointmentsByEmployee = allAppointments.stream() .collect(Collectors.groupingBy(a -> a.getEmployee().getEmployeeId())); @@ -350,7 +349,6 @@ public class AppointmentService { .isPresent(); } - //------------------------------------ private void validateAvailability(Employee employee, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { List existingAppointments = appointmentRepository .findByEmployeeEmployeeIdAndAppointmentDate(employee.getEmployeeId(), date); @@ -359,8 +357,6 @@ public class AppointmentService { } } - //------------------------------------------------ - private boolean isSlotAvailable(List existingAppointments, com.petshop.backend.entity.Service requestedService, LocalTime requestedStart, Long appointmentIdToIgnore) { LocalTime requestedEnd = requestedStart.plusMinutes(requestedService.getServiceDuration()); for (Appointment existingAppointment : existingAppointments) { diff --git a/backend/src/main/resources/db/migration/V16__activate_all_employees.sql b/backend/src/main/resources/db/migration/V16__activate_all_employees.sql index cbabc11d..314c86c8 100644 --- a/backend/src/main/resources/db/migration/V16__activate_all_employees.sql +++ b/backend/src/main/resources/db/migration/V16__activate_all_employees.sql @@ -1,4 +1,3 @@ --- Activate all employees in the users table so they appear in dropdowns UPDATE users u SET u.active = TRUE WHERE u.role IN ('STAFF', 'ADMIN') diff --git a/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql b/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql index 00d34751..90c2a407 100644 --- a/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql +++ b/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql @@ -1,7 +1,3 @@ --- V17: Normalize legacy appointmentPet data into customer_pet and appointment_customer_pet - --- Step 1: Ensure a customer_pet exists for every pet linked in appointmentPet --- Note: pet species and breed might be null in pet table, but we copy them over if present INSERT INTO customer_pet (customer_id, pet_name, species, breed) SELECT DISTINCT a.customerId, p.petName, p.petSpecies, p.petBreed FROM appointmentPet ap @@ -12,7 +8,6 @@ WHERE NOT EXISTS ( WHERE cp.customer_id = a.customerId AND cp.pet_name = p.petName ); --- Step 2: Link the appointment to the customer_pet INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id) SELECT ap.appointmentId, cp.customer_pet_id FROM appointmentPet ap @@ -24,5 +19,4 @@ WHERE NOT EXISTS ( WHERE acp.appointment_id = ap.appointmentId AND acp.customer_pet_id = cp.customer_pet_id ); --- Step 3: Remove the old legacy relationships so it strictly uses the new ones DELETE FROM appointmentPet; diff --git a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql index 19ba0fa0..093c6ce3 100644 --- a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql +++ b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql @@ -1,7 +1,3 @@ --- V18: Normalize past appointments and resolve initial employee double-bookings - --- Part 1: Normalize past appointments. --- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'. UPDATE appointment SET appointmentStatus = 'Missed' WHERE LOWER(appointmentStatus) = 'booked' @@ -10,8 +6,6 @@ WHERE LOWER(appointmentStatus) = 'booked' OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME) ); --- Part 2: Resolve potential double-bookings caused by V15's simple backfill. --- MySQL Error 1093 workaround: wrap same-table subqueries in derived tables. UPDATE appointment a1 JOIN ( SELECT a3.appointmentId 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 1fef0e42..a133c29f 100644 --- a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -90,7 +90,7 @@ class AdoptionServiceTest { void createAdoptionAutoAssignsFirstStaffEmployee() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - // resolveAdoptionEmployee filters for staff + when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee)); when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { Adoption adoption = invocation.getArgument(0); diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java index 71b7005c..cab8e4cb 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java @@ -22,7 +22,6 @@ public class AppointmentDTO { private SimpleStringProperty appointmentTime; private SimpleStringProperty appointmentStatus; - // Constructor public AppointmentDTO(int appointmentId, int customerId, String customerName, int petId, String petName, @@ -47,7 +46,6 @@ public class AppointmentDTO { this.appointmentStatus = new SimpleStringProperty(appointmentStatus); } - // Getters public int getAppointmentId() { return appointmentId.get(); } public int getCustomerId() { return customerId.get(); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java index 0b8fe447..4de4d212 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java @@ -68,7 +68,7 @@ public class AdoptionController { void initialize() { btnEdit.setDisable(true); btnDelete.setDisable(true); - //Enable multiple selection + tvAdoptions.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); colAdoptionId.setCellValueFactory(new PropertyValueFactory<>("adoptionId")); @@ -91,7 +91,6 @@ public class AdoptionController { displayFilteredAdoptions(newValue); }); - //EventListener for DELETE key tvAdoptions.setOnKeyPressed(event -> { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { if (tvAdoptions.getSelectionModel().getSelectedItem() != null) { @@ -109,11 +108,10 @@ public class AdoptionController { @FXML void btnDeleteClicked(ActionEvent event) { - //get selected adoptions + var selectedAdoptions = tvAdoptions.getSelectionModel().getSelectedItems(); if (selectedAdoptions.isEmpty()) return; - //ask user to confirm Alert question = new Alert(Alert.AlertType.CONFIRMATION); question.setHeaderText("Please confirm delete"); String message = selectedAdoptions.size() == 1 @@ -123,7 +121,6 @@ public class AdoptionController { question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); Optional result = question.showAndWait(); - //if confirmed, start deletion if (result.isPresent() && result.get() == ButtonType.OK) { List ids = selectedAdoptions.stream() .map(a -> (long) a.getAdoptionId()) @@ -146,7 +143,6 @@ public class AdoptionController { alert.showAndWait(); } - //refresh display and reset inputs displayAdoptions(); btnDelete.setDisable(true); btnEdit.setDisable(true); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index ba4c05aa..d183918a 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -47,7 +47,7 @@ public class AppointmentController { @FXML public void initialize(){ - //Enable multiple selection + tvAppointments.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); colAppointmentId.setCellValueFactory(new PropertyValueFactory<>("appointmentId")); @@ -66,7 +66,6 @@ public class AppointmentController { txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); } - //EventListener for DELETE key tvAppointments.setOnKeyPressed(event -> { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { if (tvAppointments.getSelectionModel().getSelectedItem() != null) { @@ -148,11 +147,10 @@ public class AppointmentController { @FXML void btnDeleteClicked(ActionEvent event){ - //get selected appointments + var selectedAppointments = tvAppointments.getSelectionModel().getSelectedItems(); if (selectedAppointments.isEmpty()) return; - //ask user to confirm Alert question = new Alert(Alert.AlertType.CONFIRMATION); question.setHeaderText("Please confirm delete"); String message = selectedAppointments.size() == 1 @@ -162,7 +160,6 @@ public class AppointmentController { question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); java.util.Optional result = question.showAndWait(); - //if confirmed, start deletion if (result.isPresent() && result.get() == ButtonType.OK) { List ids = selectedAppointments.stream() .map(a -> (long) a.getAppointmentId()) @@ -185,7 +182,6 @@ public class AppointmentController { alert.showAndWait(); } - //refresh display loadAppointments(); } } 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 56f682c6..67e1073f 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 @@ -28,7 +28,6 @@ import java.util.Objects; public class AdoptionDialogController { - //FXML elements @FXML private Button btnCancel; @@ -56,11 +55,9 @@ public class AdoptionDialogController { @FXML private Label lblMode; - //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( "Pending", "Completed", "Cancelled" ); @@ -234,7 +231,6 @@ public class AdoptionDialogController { } } - private void closeStage(MouseEvent mouseEvent) { Node node = (Node) mouseEvent.getSource(); Stage stage = (Stage) node.getScene().getWindow(); 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 cac4f47e..d21ad756 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 @@ -26,10 +26,6 @@ import java.util.Objects; public class AppointmentDialogController { - // ============================ - // FXML - // ============================ - @FXML private Button btnCancel; @FXML private Button btnSave; @@ -47,11 +43,7 @@ public class AppointmentDialogController { @FXML private Label lblAppointmentId; @FXML private Label lblMode; - // ============================ - // DATA - // ============================ - - private String mode = null; // Add | Edit + private String mode = null; private AppointmentDTO selectedAppointment = null; private Long pendingPetSelectionId = null; @@ -60,20 +52,12 @@ public class AppointmentDialogController { "Booked", "Completed", "Cancelled", "Missed" ); - // - // MODE - // - public void setMode(String mode) { this.mode = mode; lblMode.setText(mode + " Appointment"); lblAppointmentId.setVisible(!mode.equals("Add")); } - // - // INITIALIZE - // - @FXML public void initialize() { cbAppointmentStatus.setItems(statusList); @@ -85,14 +69,12 @@ public class AppointmentDialogController { dpAppointmentDate.setValue(LocalDate.now().plusDays(1)); cbAppointmentStatus.setValue("Booked"); - // Hours 9 AM - 5 PM for (int i = 9; i <= 17; i++) { cbHour.getItems().add(i); } cbMinute.getItems().addAll(0, 15, 30, 45); - // Show dropdown labels cbService.setCellFactory(param -> new ListCell<>() { @Override protected void updateItem(DropdownOption option, boolean empty) { @@ -175,10 +157,6 @@ public class AppointmentDialogController { loadEmployees(); } - // - // DISPLAY FOR EDIT - // - public void displayAppointmentDetails(AppointmentDTO appt) { selectedAppointment = appt; @@ -214,10 +192,6 @@ public class AppointmentDialogController { applySelectedEmployee(); } - // - // SAVE - // - private void buttonSaveClicked(MouseEvent e) { if (cbService.getValue() == null || @@ -276,10 +250,6 @@ public class AppointmentDialogController { }).start(); } - // - // UTIL - // - private void closeStage(MouseEvent e) { Stage stage = (Stage) ((Node) e.getSource()).getScene().getWindow(); stage.close(); -- 2.49.1