From 5d956137860201c94c2a3cef21ca102b5c90e9f7 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 12:17:37 -0600 Subject: [PATCH] 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}