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/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..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 @@ -8,16 +8,22 @@ 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; 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); + } + + 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 +48,14 @@ public class AdoptionDTO { return customerName; } + public Long getEmployeeId() { + return employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + public String getAdoptionDate() { return adoptionDate; } @@ -65,4 +79,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..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; @@ -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; @@ -20,21 +22,25 @@ 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) { + 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; this.petIds = petIds; } - // Getters public Long getAppointmentId() { return appointmentId; } @@ -63,6 +69,14 @@ public class AppointmentDTO { return serviceName; } + public Long getEmployeeId() { + return employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + public String getAppointmentDate() { return appointmentDate; } @@ -91,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) : ""; } @@ -104,7 +117,6 @@ public class AppointmentDTO { return getPetID(); } - // Keep old name so adapter doesn't break public String getServiceType() { return serviceName; } @@ -113,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/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 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 c942eae8..d0a69036 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -1,10 +1,14 @@ 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; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -17,23 +21,31 @@ public class DropdownController { private final PetRepository petRepository; private final CustomerRepository customerRepository; + private final CustomerPetRepository customerPetRepository; private final ServiceRepository serviceRepository; private final ProductRepository productRepository; 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, - ServiceRepository serviceRepository, ProductRepository productRepository, - CategoryRepository categoryRepository, StoreRepository storeRepository, - SupplierRepository supplierRepository) { + CustomerPetRepository customerPetRepository, + ServiceRepository serviceRepository, ProductRepository productRepository, + CategoryRepository categoryRepository, StoreRepository storeRepository, + SupplierRepository supplierRepository, EmployeeStoreRepository employeeStoreRepository, + UserRepository userRepository) { this.petRepository = petRepository; this.customerRepository = customerRepository; + this.customerPetRepository = customerPetRepository; this.serviceRepository = serviceRepository; this.productRepository = productRepository; this.categoryRepository = categoryRepository; this.storeRepository = storeRepository; this.supplierRepository = supplierRepository; + this.employeeStoreRepository = employeeStoreRepository; + this.userRepository = userRepository; } @GetMapping("/pets") @@ -45,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() { @@ -55,6 +77,26 @@ 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) { + return ResponseEntity.ok( + customerPetRepository.findByCustomerCustomerIdOrderByPetNameAsc(customerId).stream() + .map(this::toCustomerPetOption) + .collect(Collectors.toList()) + ); + } + @GetMapping("/services") public ResponseEntity> getServices() { return ResponseEntity.ok( @@ -114,6 +156,24 @@ public class DropdownController { ); } + @GetMapping({"/stores/{storeId}/employees", "/employees"}) + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + 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( + employees.stream() + .filter(this::isAssignableEmployee) + .map(this::toEmployeeOption) + .distinct() + .collect(Collectors.toList()) + ); + } + @GetMapping("/suppliers") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> getSuppliers() { @@ -123,4 +183,26 @@ public class DropdownController { .collect(Collectors.toList()) ); } + + private DropdownOption toCustomerPetOption(CustomerPet pet) { + String species = pet.getSpecies() == null || pet.getSpecies().isBlank() ? "Pet" : pet.getSpecies(); + 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) + .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/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/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/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index 5c7b6ec0..f8649671 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 " + @@ -36,4 +36,10 @@ 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) 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/repository/CustomerPetRepository.java b/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java index 8d08f8b9..4fe0ef81 100644 --- a/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java @@ -12,5 +12,7 @@ public interface CustomerPetRepository extends JpaRepository List findByCustomerCustomerIdOrderByCreatedAtDesc(Long customerId); + List findByCustomerCustomerIdOrderByPetNameAsc(Long customerId); + Optional findByCustomerPetIdAndCustomerCustomerId(Long customerPetId, Long customerId); } 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/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..16a59cea 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,20 @@ 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); + + @Query("SELECT es FROM EmployeeStore es WHERE es.employee.isActive = true ORDER BY es.employee.employeeId ASC") + List findActiveAllOrderByEmployeeEmployeeIdAsc(); } 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 a474fa8b..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,12 +8,28 @@ 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 " + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))") Page searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable); + + @Query("SELECT p FROM Pet p WHERE LOWER(p.petStatus) = 'available' AND " + + "(: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))") + Page searchPublicPets(@Param("q") String query, @Param("species") String species, Pageable pageable); + + @Query("SELECT DISTINCT p FROM Pet p LEFT JOIN Adoption a ON a.pet = p AND LOWER(a.adoptionStatus) = 'completed' WHERE " + + "(LOWER(p.petStatus) = 'available' OR a.customer.userId = :userId) AND " + + "(: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 " + + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))") + Page searchCustomerVisiblePets(@Param("userId") Long userId, @Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable); } 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..c6ff1c60 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; @@ -18,14 +22,24 @@ 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; + 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,14 +80,19 @@ 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); } @@ -87,13 +106,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, 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); } @@ -117,6 +141,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 +150,72 @@ 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) + .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 67ce4f36..155b7524 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,19 +110,19 @@ 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()); - 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."); } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>(); + 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); @@ -131,6 +133,7 @@ public class AppointmentService { appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setPets(pets); appointment.setCustomerPets(customerPets); + appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); return mapToResponse(appointment); @@ -140,6 +143,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)); @@ -152,19 +157,19 @@ 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()); - 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."); } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>(); + 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); @@ -174,6 +179,7 @@ public class AppointmentService { appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setPets(pets); appointment.setCustomerPets(customerPets); + appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); return mapToResponse(appointment); @@ -193,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)); @@ -201,16 +206,20 @@ 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()); - // ------------------------------------------------------- + if (assignableEmployees.isEmpty()) { + return List.of(); + } + + List employeeIds = assignableEmployees.stream().map(Employee::getEmployeeId).collect(Collectors.toList()); + List allAppointments = appointmentRepository.findByEmployeeEmployeeIdInAndAppointmentDate(employeeIds, date); + + java.util.Map> appointmentsByEmployee = allAppointments.stream() + .collect(Collectors.groupingBy(a -> a.getEmployee().getEmployeeId())); List availableSlots = new ArrayList<>(); LocalTime startTime = LocalTime.of(9, 0); @@ -219,7 +228,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 = appointmentsByEmployee.getOrDefault(emp.getEmployeeId(), List.of()); + return isSlotAvailable(empAppointments, service, slotTime, null); + }); + + if (anyEmployeeAvailable) { availableSlots.add(currentTime.toString()); } currentTime = currentTime.plusMinutes(30); @@ -247,11 +262,14 @@ public class AppointmentService { return pets; } - private Set fetchCustomerPets(List customerPetIds) { + 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 (!customerPet.getCustomer().getCustomerId().equals(customerId)) { + throw new IllegalArgumentException("Selected pet does not belong to the selected customer"); + } customerPets.add(customerPet); } @@ -286,6 +304,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); @@ -296,20 +316,46 @@ public class AppointmentService { return response; } - //------------------------------------ - 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 - List existingAppointments = appointmentRepository - .findByStoreAndDate(store.getStoreId(), date) - .stream() - .filter(a -> a.getService().getServiceId().equals(service.getServiceId())) + private Employee resolveAppointmentEmployee(Long requestedEmployeeId, Long storeId) { + List assignableEmployees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() + .filter(es -> isAssignableEmployee(es.getEmployee())) .collect(Collectors.toList()); - if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { - throw new IllegalArgumentException("Appointment time is not available for the selected store and service"); + + 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) + .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> Boolean.TRUE.equals(user.getActive())) + .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); + if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { + throw new IllegalArgumentException("The selected employee is already booked for this time slot"); + } + } private boolean isSlotAvailable(List existingAppointments, com.petshop.backend.entity.Service requestedService, LocalTime requestedStart, Long appointmentIdToIgnore) { LocalTime requestedEnd = requestedStart.plusMinutes(requestedService.getServiceDuration()); @@ -326,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/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index 4672ee85..dc5fa61e 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -7,12 +7,16 @@ import com.petshop.backend.entity.Adoption; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.PetRepository; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -34,13 +38,38 @@ public class PetService { } public Page getAllPets(String query, String species, String status, Pageable pageable) { - return petRepository.searchPets(normalizeFilter(query), normalizeFilter(species), normalizeFilter(status), pageable) + String normalizedQuery = normalizeFilter(query); + String normalizedSpecies = normalizeFilter(species); + String normalizedStatus = normalizeFilter(status); + CurrentViewer viewer = getCurrentViewer(); + + Page pets; + if (viewer == null) { + if (!isAllowedPublicStatus(normalizedStatus)) { + return new PageImpl<>(java.util.List.of(), pageable, 0); + } + pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable); + } else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) { + pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, pageable); + } else if (viewer.role() == User.Role.CUSTOMER) { + if (!isAllowedCustomerStatus(normalizedStatus)) { + return new PageImpl<>(java.util.List.of(), pageable, 0); + } + pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedStatus, pageable); + } else { + pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable); + } + + return pets .map(this::mapToResponse); } public PetResponse getPetById(Long id) { Pet pet = petRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); + if (!canViewPet(pet, getCurrentViewer())) { + throw new ResourceNotFoundException("Pet not found with id: " + id); + } return mapToResponse(pet); } @@ -110,7 +139,7 @@ public class PetService { if (pet.getImageUrl() == null || pet.getImageUrl().isBlank()) { throw new ResourceNotFoundException("Pet image not found for id: " + id); } - if (!canViewPetImage(pet, requesterUserId, requesterRole)) { + if (!canViewPet(pet, new CurrentViewer(requesterUserId, requesterRole))) { throw new ForbiddenImageAccessException(); } Resource resource = catalogImageStorageService.loadPetImage(pet.getImageUrl()); @@ -122,14 +151,21 @@ public class PetService { return "available".equalsIgnoreCase(normalizeStatus(pet.getPetStatus())); } - private boolean canViewPetImage(Pet pet, Long requesterUserId, User.Role requesterRole) { + private boolean canViewPet(Pet pet, CurrentViewer viewer) { if (isPubliclyVisible(pet)) { return true; } - if (requesterRole == User.Role.STAFF || requesterRole == User.Role.ADMIN) { + if (viewer != null && (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN)) { return true; } - if (requesterUserId == null) { + if (viewer == null || viewer.userId() == null) { + return false; + } + return isAdoptedByUser(pet, viewer.userId()); + } + + private boolean isAdoptedByUser(Pet pet, Long userId) { + if (userId == null) { return false; } if (!"adopted".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) { @@ -137,10 +173,22 @@ public class PetService { } return adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(pet.getPetId(), "Completed") .map(Adoption::getCustomer) - .map(customer -> requesterUserId.equals(customer.getUserId())) + .map(customer -> userId.equals(customer.getUserId())) .orElse(false); } + private CurrentViewer getCurrentViewer() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof AppPrincipal appPrincipal) { + return new CurrentViewer(appPrincipal.getUserId(), appPrincipal.getRole()); + } + return null; + } + private Pet findPet(Long id) { return petRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); @@ -177,6 +225,14 @@ public class PetService { return status == null ? "" : status.trim(); } + private boolean isAllowedPublicStatus(String status) { + return status == null || "available".equalsIgnoreCase(status); + } + + private boolean isAllowedCustomerStatus(String status) { + return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status); + } + private String normalizeFilter(String value) { if (value == null) { return null; @@ -203,6 +259,9 @@ public class PetService { public record ImagePayload(Resource resource, MediaType mediaType) { } + private record CurrentViewer(Long userId, User.Role role) { + } + public static class ForbiddenImageAccessException extends RuntimeException { } } 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/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..314c86c8 --- /dev/null +++ b/backend/src/main/resources/db/migration/V16__activate_all_employees.sql @@ -0,0 +1,4 @@ +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); 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..90c2a407 --- /dev/null +++ b/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql @@ -0,0 +1,22 @@ +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 +); + +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 +); + +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 new file mode 100644 index 00000000..093c6ce3 --- /dev/null +++ b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql @@ -0,0 +1,40 @@ +UPDATE appointment +SET appointmentStatus = 'Missed' +WHERE LOWER(appointmentStatus) = 'booked' + AND ( + appointmentDate < CURRENT_DATE + OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME) + ); + +UPDATE appointment a1 +JOIN ( + SELECT a3.appointmentId + FROM appointment a3 + INNER JOIN appointment a4 + ON a4.employeeId = a3.employeeId + AND a4.appointmentDate = a3.appointmentDate + AND a4.appointmentTime = a3.appointmentTime + AND a4.appointmentId < a3.appointmentId + WHERE LOWER(a3.appointmentStatus) NOT IN ('cancelled', 'missed') +) conflicting ON conflicting.appointmentId = a1.appointmentId +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' + AND NOT EXISTS ( + 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 +); 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 new file mode 100644 index 00000000..e7caa86e --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -0,0 +1,201 @@ +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; +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 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DropdownControllerTest { + + 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; + + @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, + serviceRepository, + productRepository, + categoryRepository, + storeRepository, + supplierRepository, + 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 getStoreEmployeesReturnsOnlyStaffLinkedEmployees() { + 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); + 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))); + 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()); + } + + @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(); + 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 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); + + 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()); + } +} 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..a133c29f --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -0,0 +1,150 @@ +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"); + pet.setPetStatus("Available"); + + 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); + 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)); + } + + @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)); + } + + @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 2a1e6eed..3e6f2d89 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -2,23 +2,32 @@ 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; import com.petshop.backend.entity.User; import com.petshop.backend.repository.AppointmentRepository; +import com.petshop.backend.repository.CustomerPetRepository; import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeRepository; +import com.petshop.backend.repository.EmployeeStoreRepository; 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; 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; @@ -29,32 +38,26 @@ import java.util.List; import java.util.Optional; import java.util.Set; +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 PetRepository petRepository; - - @Mock - private ServiceRepository serviceRepository; - - @Mock - private StoreRepository storeRepository; - - @Mock - private UserRepository userRepository; + @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; @@ -64,10 +67,19 @@ class AppointmentServiceTest { private Service grooming; private Service nailTrim; private Pet pet; + private CustomerPet customerPet; + private Employee employee; private LocalDate date; @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"); @@ -91,8 +103,25 @@ class AppointmentServiceTest { pet.setPetId(1L); pet.setPetName("Milo"); - date = LocalDate.now().plusDays(1); + customerPet = new CustomerPet(); + customerPet.setCustomerPetId(11L); + 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); + staffUser.setActive(true); + when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); + + date = LocalDate.now().plusDays(1); } @AfterEach @@ -101,11 +130,28 @@ 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); @@ -113,53 +159,26 @@ class AppointmentServiceTest { } @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)); + 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))); - List slots = appointmentService.checkAvailability(1L, 1L, date); + Customer otherCustomer = new Customer(); + otherCustomer.setCustomerId(2L); - assertFalse(slots.contains("10:00")); - } + CustomerPet otherCustomerPet = new CustomerPet(); + otherCustomerPet.setCustomerPetId(22L); + otherCustomerPet.setCustomer(otherCustomer); + otherCustomerPet.setPetName("Not Yours"); - @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)); - - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken( - new com.petshop.backend.security.AppPrincipal(10L, "pat", User.Role.CUSTOMER, 0), - "n/a", - List.of(new SimpleGrantedAuthority("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(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); - when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); + 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(); request.setCustomerId(1L); @@ -168,12 +187,122 @@ class AppointmentServiceTest { request.setAppointmentDate(date); request.setAppointmentTime(LocalTime.of(10, 0)); request.setAppointmentStatus("Booked"); - request.setPetIds(List.of(1L)); + request.setCustomerPetIds(List.of(22L)); - var response = appointmentService.updateAppointment(1L, request); + assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); + } - assertEquals(1L, response.getAppointmentId()); - assertEquals("Booked", response.getAppointmentStatus()); + @Test + void createAppointmentRejectsCustomerPetOwnedByDifferentCustomer() { + 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.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); + when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); + + 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)); + + assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); + } + + @Test + 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); + adminEmployee.setUserId(8L); + adminEmployee.setFirstName("Admin"); + adminEmployee.setLastName("Helper"); + adminEmployee.setIsActive(true); + + 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)); + 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.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))); + + 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)); + } + + @Test + void createAppointmentRejectsInactiveStaffUserSelection() { + 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)); + + 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.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))); + + 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) { @@ -184,8 +313,20 @@ class AppointmentServiceTest { appointment.setAppointmentStatus("Booked"); appointment.setService(service); appointment.setStore(storeLocation); + 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 AppPrincipal(userId, "user", role, 0), + "n/a", + List.of(new SimpleGrantedAuthority("ROLE_" + role.name())) + ) + ); + } } diff --git a/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java b/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java new file mode 100644 index 00000000..9107ebd9 --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java @@ -0,0 +1,165 @@ +package com.petshop.backend.service; + +import com.petshop.backend.entity.Adoption; +import com.petshop.backend.entity.Customer; +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.PetRepository; +import com.petshop.backend.security.AppPrincipal; +import org.junit.jupiter.api.AfterEach; +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.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +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.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PetServiceTest { + + @Mock + private PetRepository petRepository; + + @Mock + private AdoptionRepository adoptionRepository; + + @Mock + private CatalogImageStorageService catalogImageStorageService; + + @InjectMocks + private PetService petService; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void getAllPetsAnonymousReturnsOnlyPublicPets() { + Pageable pageable = PageRequest.of(0, 10); + Pet availablePet = pet(1L, "Buddy", "Available"); + when(petRepository.searchPublicPets(null, null, pageable)).thenReturn(new PageImpl<>(List.of(availablePet), pageable, 1)); + + var result = petService.getAllPets(null, null, null, pageable); + + assertEquals(1, result.getTotalElements()); + assertEquals("Buddy", result.getContent().get(0).getPetName()); + verify(petRepository).searchPublicPets(null, null, pageable); + verify(petRepository, never()).searchPets(null, null, null, pageable); + } + + @Test + void getAllPetsAnonymousWithAdoptedStatusReturnsEmptyPage() { + Pageable pageable = PageRequest.of(0, 10); + + var result = petService.getAllPets(null, null, "Adopted", pageable); + + assertEquals(0, result.getTotalElements()); + verify(petRepository, never()).searchPublicPets(null, null, pageable); + } + + @Test + void getAllPetsCustomerReturnsVisiblePetsOnly() { + Pageable pageable = PageRequest.of(0, 10); + setAuthentication(25L, User.Role.CUSTOMER); + Pet availablePet = pet(1L, "Buddy", "Available"); + Pet adoptedPet = pet(2L, "Luna", "Adopted"); + when(petRepository.searchCustomerVisiblePets(25L, null, null, null, pageable)) + .thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); + + var result = petService.getAllPets(null, null, null, pageable); + + assertEquals(2, result.getTotalElements()); + verify(petRepository).searchCustomerVisiblePets(25L, null, null, null, pageable); + } + + @Test + void getAllPetsAdminReturnsAllPets() { + Pageable pageable = PageRequest.of(0, 10); + setAuthentication(99L, User.Role.ADMIN); + Pet availablePet = pet(1L, "Buddy", "Available"); + Pet adoptedPet = pet(2L, "Luna", "Adopted"); + when(petRepository.searchPets(null, null, null, pageable)) + .thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); + + var result = petService.getAllPets(null, null, null, pageable); + + assertEquals(2, result.getTotalElements()); + verify(petRepository).searchPets(null, null, null, pageable); + } + + @Test + void getPetByIdHidesAdoptedPetFromUnrelatedCustomer() { + setAuthentication(50L, User.Role.CUSTOMER); + Pet adoptedPet = pet(2L, "Luna", "Adopted"); + when(petRepository.findById(2L)).thenReturn(Optional.of(adoptedPet)); + when(adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(2L, "Completed")) + .thenReturn(Optional.of(adoption(2L, 25L))); + + assertThrows(ResourceNotFoundException.class, () -> petService.getPetById(2L)); + } + + @Test + void getPetByIdAllowsOwnerToSeeAdoptedPet() { + setAuthentication(25L, User.Role.CUSTOMER); + Pet adoptedPet = pet(2L, "Luna", "Adopted"); + when(petRepository.findById(2L)).thenReturn(Optional.of(adoptedPet)); + when(adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(2L, "Completed")) + .thenReturn(Optional.of(adoption(2L, 25L))); + + var result = petService.getPetById(2L); + + assertEquals(2L, result.getPetId()); + } + + private void setAuthentication(Long userId, User.Role role) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + new AppPrincipal(userId, "user", role, 0), + "n/a", + List.of(new SimpleGrantedAuthority("ROLE_" + role.name())) + ) + ); + } + + private Pet pet(Long id, String name, String status) { + Pet pet = new Pet(); + pet.setPetId(id); + pet.setPetName(name); + pet.setPetSpecies("Cat"); + pet.setPetBreed("Mixed"); + pet.setPetAge(2); + pet.setPetStatus(status); + pet.setPetPrice(java.math.BigDecimal.TEN); + return pet; + } + + private Adoption adoption(Long petId, Long userId) { + Adoption adoption = new Adoption(); + Pet pet = new Pet(); + pet.setPetId(petId); + adoption.setPet(pet); + Customer customer = new Customer(); + customer.setCustomerId(1L); + customer.setUserId(userId); + adoption.setCustomer(customer); + adoption.setAdoptionStatus("Completed"); + return adoption; + } +} 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..cab8e4cb 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java @@ -15,16 +15,19 @@ public class AppointmentDTO { private SimpleIntegerProperty serviceId; private SimpleStringProperty serviceName; + private SimpleIntegerProperty employeeId; + private SimpleStringProperty employeeName; private SimpleStringProperty appointmentDate; private SimpleStringProperty appointmentTime; private SimpleStringProperty appointmentStatus; - // Constructor public AppointmentDTO(int appointmentId, int customerId, String customerName, int petId, String petName, int serviceId, String serviceName, + int employeeId, + String employeeName, String appointmentDate, String appointmentTime, String appointmentStatus) { @@ -36,12 +39,13 @@ 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); } - // Getters public int getAppointmentId() { return appointmentId.get(); } public int getCustomerId() { return customerId.get(); } @@ -52,8 +56,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 a81faaff..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 @@ -6,9 +6,11 @@ import java.util.List; public class AppointmentRequest { private List petIds; + private List customerPetIds; private Long customerId; private Long storeId; private Long serviceId; + private Long employeeId; private LocalDate appointmentDate; private LocalTime appointmentTime; private String appointmentStatus; @@ -24,6 +26,14 @@ public class AppointmentRequest { this.petIds = petIds; } + public List getCustomerPetIds() { + return customerPetIds; + } + + public void setCustomerPetIds(List customerPetIds) { + this.customerPetIds = customerPetIds; + } + public Long getCustomerId() { return customerId; } @@ -48,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 1d904bd0..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 @@ -12,7 +12,11 @@ public class AppointmentResponse { private Long serviceId; private java.util.List petNames; private java.util.List petIds; + 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; @@ -84,6 +88,22 @@ public class AppointmentResponse { this.petIds = petIds; } + public java.util.List getCustomerPetNames() { + return customerPetNames; + } + + public void setCustomerPetNames(java.util.List customerPetNames) { + this.customerPetNames = customerPetNames; + } + + public java.util.List getCustomerPetIds() { + return customerPetIds; + } + + public void setCustomerPetIds(java.util.List customerPetIds) { + this.customerPetIds = customerPetIds; + } + public String getServiceName() { return serviceName; } @@ -92,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 30fcb0b8..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 @@ -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,22 @@ 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()) { + throw new IllegalStateException("Empty response from customer pets endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + public List getStores() throws Exception { String response = apiClient.getRawResponse("/api/v1/dropdowns/stores"); if (response == null || response.isEmpty()) { @@ -89,4 +113,20 @@ 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>() {}); + } + + 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/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java index 564e6205..4de4d212 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; @@ -65,12 +68,13 @@ 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")); 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")); @@ -87,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) { @@ -105,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 @@ -119,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()) @@ -142,7 +143,6 @@ public class AdoptionController { alert.showAndWait(); } - //refresh display and reset inputs displayAdoptions(); btnDelete.setDisable(true); btnEdit.setDisable(true); @@ -252,8 +252,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 bd5dc392..d183918a 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; @@ -46,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")); @@ -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); @@ -64,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) { @@ -146,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 @@ -160,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()) @@ -183,7 +182,6 @@ public class AppointmentController { alert.showAndWait(); } - //refresh display loadAppointments(); } } @@ -233,15 +231,22 @@ public class AppointmentController { } private AppointmentDTO mapToAppointmentDTO(AppointmentResponse response) { - Long petId = response.getPetIds() != null && !response.getPetIds().isEmpty() ? response.getPetIds().get(0) : null; + Long petId = response.getCustomerPetIds() != null && !response.getCustomerPetIds().isEmpty() + ? response.getCustomerPetIds().get(0) + : response.getPetIds() != null && !response.getPetIds().isEmpty() ? response.getPetIds().get(0) : null; + String petName = response.getCustomerPetNames() != null && !response.getCustomerPetNames().isEmpty() + ? String.join(", ", response.getCustomerPetNames()) + : String.join(", ", response.getPetNames()); return new AppointmentDTO( response.getAppointmentId().intValue(), response.getCustomerId() != null ? response.getCustomerId().intValue() : 0, response.getCustomerName(), petId != null ? petId.intValue() : 0, - String.join(", ", response.getPetNames()), + 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..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 @@ -11,21 +11,23 @@ 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; 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 { - //FXML elements @FXML private Button btnCancel; @@ -38,6 +40,9 @@ public class AdoptionDialogController { @FXML private ComboBox cbCustomer; + @FXML + private ComboBox cbEmployee; + @FXML private ComboBox cbPet; @@ -50,10 +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" ); @@ -62,14 +66,16 @@ public class AdoptionDialogController { void initialize() { cbAdoptionStatus.setItems(statusList); + cbEmployee.setPromptText("Select an employee"); 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) { @@ -83,6 +89,46 @@ public class AdoptionDialogController { } }).start(); + new Thread(() -> { + try { + Long storeId = UserSession.getInstance().getStoreId(); + 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(); + }); + } 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(); @@ -90,6 +136,7 @@ public class AdoptionDialogController { if (customers != null) { ObservableList customersObs = FXCollections.observableArrayList(customers); cbCustomer.setItems(customersObs); + applySelectedCustomer(); } }); } catch (Exception e) { @@ -129,6 +176,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 +193,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()); @@ -179,7 +231,6 @@ public class AdoptionDialogController { } } - private void closeStage(MouseEvent mouseEvent) { Node node = (Node) mouseEvent.getSource(); Stage stage = (Stage) node.getScene().getWindow(); @@ -188,21 +239,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; - } - } + applySelectedPet(); + applySelectedCustomer(); + applySelectedEmployee(); if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) { try { @@ -229,4 +270,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 932d10d5..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 @@ -19,21 +19,20 @@ import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; import java.time.LocalTime; -import java.util.Collections; +import java.time.LocalDate; import java.util.List; +import java.util.Collections; +import java.util.Objects; public class AppointmentDialogController { - // ============================ - // FXML - // ============================ - @FXML private Button btnCancel; @FXML private Button btnSave; @FXML private ComboBox cbService; @FXML private ComboBox cbCustomer; @FXML private ComboBox cbPet; + @FXML private ComboBox cbEmployee; @FXML private ComboBox cbHour; @FXML private ComboBox cbMinute; @@ -44,74 +43,38 @@ 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; private ObservableList statusList = FXCollections.observableArrayList( - "Booked", "Completed", "Cancelled" + "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() { - - new Thread(() -> { - try { - List services = DropdownApi.getInstance().getServices(); - List customers = DropdownApi.getInstance().getCustomers(); - List pets = DropdownApi.getInstance().getPets(); - - Platform.runLater(() -> { - if (services != null) { - cbService.setItems(FXCollections.observableArrayList(services)); - } - if (customers != null) { - cbCustomer.setItems(FXCollections.observableArrayList(customers)); - } - if (pets != null) { - cbPet.setItems(FXCollections.observableArrayList(pets)); - } - syncSelectedAppointment(); - }); - } catch (Exception e) { - Platform.runLater(() -> { - ActivityLogger.getInstance().logException( - "AppointmentDialogController.initialize", - e, - "Loading combo box data for services, customers, and pets"); - e.printStackTrace(); - }); - } - }).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"); + 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) { @@ -157,18 +120,48 @@ 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); + cbPet.setItems(FXCollections.observableArrayList()); + cbPet.setDisable(customerId == null); + if (customerId != null) { + cbPet.setPromptText("Loading customer pets..."); + loadCustomerPets(customerId); + } else { + cbPet.setPromptText("Select a customer first"); + pendingPetSelectionId = null; + } + }); + btnSave.setOnMouseClicked(this::buttonSaveClicked); btnCancel.setOnMouseClicked(this::closeStage); - } - // - // DISPLAY FOR EDIT - // + loadServices(); + loadAppointmentCustomers(); + loadEmployees(); + } public void displayAppointmentDetails(AppointmentDTO appt) { selectedAppointment = appt; lblAppointmentId.setText("ID: " + appt.getAppointmentId()); + pendingPetSelectionId = appt.getPetId() > 0 ? (long) appt.getPetId() : null; try { dpAppointmentDate.setValue( @@ -194,28 +187,17 @@ 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()) cbCustomer.setValue(c); - }); - - cbPet.getItems().forEach(p -> { - if (p.getId() != null && p.getId().longValue() == appt.getPetId()) cbPet.setValue(p); - }); + applySelectedService(); + applySelectedCustomer(); + applySelectedEmployee(); } - // - // SAVE - // - private void buttonSaveClicked(MouseEvent e) { if (cbService.getValue() == null || cbCustomer.getValue() == null || cbPet.getValue() == null || + cbEmployee.getValue() == null || dpAppointmentDate.getValue() == null || cbHour.getValue() == null || cbMinute.getValue() == null || @@ -233,10 +215,11 @@ public class AppointmentDialogController { } AppointmentRequest request = new AppointmentRequest(); - request.setPetIds(Collections.singletonList(cbPet.getValue().getId())); + request.setCustomerPetIds(Collections.singletonList(cbPet.getValue().getId())); 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()); @@ -267,10 +250,6 @@ public class AppointmentDialogController { }).start(); } - // - // UTIL - // - private void closeStage(MouseEvent e) { Stage stage = (Stage) ((Node) e.getSource()).getScene().getWindow(); stage.close(); @@ -288,4 +267,156 @@ public class AppointmentDialogController { displayAppointmentDetails(selectedAppointment); } } + + 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 { + List pets = DropdownApi.getInstance().getCustomerPets(customerId); + Platform.runLater(() -> { + cbPet.setItems(FXCollections.observableArrayList(pets)); + cbPet.setDisable(pets == null || pets.isEmpty()); + cbPet.setPromptText(pets == null || pets.isEmpty() ? "No pets for selected customer" : "Select a pet"); + if (pendingPetSelectionId != null) { + for (DropdownOption pet : cbPet.getItems()) { + if (pet.getId() != null && pet.getId().equals(pendingPetSelectionId)) { + cbPet.setValue(pet); + break; + } + } + pendingPetSelectionId = null; + } + }); + } catch (Exception ex) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.loadCustomerPets", + ex, + "Loading customer pets for appointment dialog"); + cbPet.setDisable(true); + cbPet.setPromptText("Unable to load pets"); + showError("Error loading pets for selected customer"); + }); + } + }).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; + if (storeId != null && storeId > 0) { + employees = DropdownApi.getInstance().getStoreEmployees(storeId); + } else { + employees = DropdownApi.getInstance().getEmployees(); + } + 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(); + } } 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}