diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index 940f7cf2..f8649671 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -39,4 +39,7 @@ public interface AppointmentRepository extends JpaRepository @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); + + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId IN :employeeIds AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") + List findByEmployeeEmployeeIdInAndAppointmentDate(@Param("employeeIds") List employeeIds, @Param("date") LocalDate date); } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 310c539d..b4e270fa 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -211,6 +211,17 @@ public class AppointmentService { .map(EmployeeStore::getEmployee) .collect(Collectors.toList()); + if (assignableEmployees.isEmpty()) { + return List.of(); + } + + List employeeIds = assignableEmployees.stream().map(Employee::getEmployeeId).collect(Collectors.toList()); + List allAppointments = appointmentRepository.findByEmployeeEmployeeIdInAndAppointmentDate(employeeIds, date); + + // Group by employee for faster lookup in the loop + java.util.Map> appointmentsByEmployee = allAppointments.stream() + .collect(Collectors.groupingBy(a -> a.getEmployee().getEmployeeId())); + List availableSlots = new ArrayList<>(); LocalTime startTime = LocalTime.of(9, 0); LocalTime endTime = LocalTime.of(17, 0); @@ -220,7 +231,7 @@ public class AppointmentService { while (!currentTime.isAfter(latestStart)) { final LocalTime slotTime = currentTime; boolean anyEmployeeAvailable = assignableEmployees.stream().anyMatch(emp -> { - List empAppointments = appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(emp.getEmployeeId(), date); + List empAppointments = appointmentsByEmployee.getOrDefault(emp.getEmployeeId(), List.of()); return isSlotAvailable(empAppointments, service, slotTime, null); }); diff --git a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql index 1f3a6707..043cad4e 100644 --- a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql +++ b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql @@ -1,6 +1,7 @@ --- V18: Normalize past appointments. --- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'. +-- V18: Normalize past appointments and resolve initial employee double-bookings +-- Part 1: Normalize past appointments. +-- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'. UPDATE appointment SET appointmentStatus = 'Missed' WHERE LOWER(appointmentStatus) = 'booked' @@ -8,3 +9,37 @@ WHERE LOWER(appointmentStatus) = 'booked' appointmentDate < CURRENT_DATE OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME) ); + +-- Part 2: Resolve potential double-bookings caused by V15's simple backfill. +-- We try to spread overlapping appointments among other active staff in the same store. +-- This is a one-time cleanup for demo data integrity. + +-- Temporary table to find conflicts (same employee, same date, overlapping time) +-- For simplicity in SQL, we just check exact same time for the demo data cleanup. +UPDATE appointment a1 +SET a1.employeeId = ( + SELECT es.employeeId + FROM employeeStore es + JOIN employee e ON e.employeeId = es.employeeId + JOIN users u ON u.id = e.user_id + WHERE es.storeId = a1.storeId + AND e.isActive = TRUE + AND u.role = 'STAFF' + -- Find an employee who DOES NOT have an appointment at this exact time + AND NOT EXISTS ( + SELECT 1 FROM appointment a2 + WHERE a2.employeeId = es.employeeId + AND a2.appointmentDate = a1.appointmentDate + AND a2.appointmentTime = a1.appointmentTime + AND a2.appointmentId <> a1.appointmentId + ) + ORDER BY es.employeeId ASC + LIMIT 1 +) +WHERE EXISTS ( + SELECT 1 FROM appointment a3 + WHERE a3.employeeId = a1.employeeId + AND a3.appointmentDate = a1.appointmentDate + AND a3.appointmentTime = a1.appointmentTime + AND a3.appointmentId < a1.appointmentId +) AND LOWER(a1.appointmentStatus) NOT IN ('cancelled', 'missed'); diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java index e62430e6..e7caa86e 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -198,25 +198,4 @@ class DropdownControllerTest { assertEquals(1, response.getBody().size()); assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); } - - @Test - void getAppointmentCustomersReturnsOnlyCustomersWithPetsForAdmin() { - User adminUser = new User(); - adminUser.setId(88L); - adminUser.setRole(User.Role.ADMIN); - when(userRepository.findById(88L)).thenReturn(Optional.of(adminUser)); - setAuthentication(88L, User.Role.ADMIN); - - Customer one = new Customer(); - one.setCustomerId(1L); - one.setFirstName("Alex"); - one.setLastName("Brown"); - - when(customerRepository.findAllWithPets()).thenReturn(List.of(one)); - - var response = controller.getAppointmentCustomers(); - - assertEquals(1, response.getBody().size()); - assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); - } }