Fix availability checks

This commit is contained in:
2026-04-06 01:51:58 -06:00
parent d8e2c8c95d
commit d51b1b0ab7
4 changed files with 52 additions and 24 deletions

View File

@@ -39,4 +39,7 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
@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')") @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<Appointment> findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); List<Appointment> 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<Appointment> findByEmployeeEmployeeIdInAndAppointmentDate(@Param("employeeIds") List<Long> employeeIds, @Param("date") LocalDate date);
} }

View File

@@ -211,6 +211,17 @@ public class AppointmentService {
.map(EmployeeStore::getEmployee) .map(EmployeeStore::getEmployee)
.collect(Collectors.toList()); .collect(Collectors.toList());
if (assignableEmployees.isEmpty()) {
return List.of();
}
List<Long> employeeIds = assignableEmployees.stream().map(Employee::getEmployeeId).collect(Collectors.toList());
List<Appointment> allAppointments = appointmentRepository.findByEmployeeEmployeeIdInAndAppointmentDate(employeeIds, date);
// Group by employee for faster lookup in the loop
java.util.Map<Long, List<Appointment>> appointmentsByEmployee = allAppointments.stream()
.collect(Collectors.groupingBy(a -> a.getEmployee().getEmployeeId()));
List<String> availableSlots = new ArrayList<>(); List<String> availableSlots = new ArrayList<>();
LocalTime startTime = LocalTime.of(9, 0); LocalTime startTime = LocalTime.of(9, 0);
LocalTime endTime = LocalTime.of(17, 0); LocalTime endTime = LocalTime.of(17, 0);
@@ -220,7 +231,7 @@ public class AppointmentService {
while (!currentTime.isAfter(latestStart)) { while (!currentTime.isAfter(latestStart)) {
final LocalTime slotTime = currentTime; final LocalTime slotTime = currentTime;
boolean anyEmployeeAvailable = assignableEmployees.stream().anyMatch(emp -> { boolean anyEmployeeAvailable = assignableEmployees.stream().anyMatch(emp -> {
List<Appointment> empAppointments = appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(emp.getEmployeeId(), date); List<Appointment> empAppointments = appointmentsByEmployee.getOrDefault(emp.getEmployeeId(), List.of());
return isSlotAvailable(empAppointments, service, slotTime, null); return isSlotAvailable(empAppointments, service, slotTime, null);
}); });

View File

@@ -1,6 +1,7 @@
-- V18: Normalize past appointments. -- V18: Normalize past appointments and resolve initial employee double-bookings
-- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'.
-- Part 1: Normalize past appointments.
-- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'.
UPDATE appointment UPDATE appointment
SET appointmentStatus = 'Missed' SET appointmentStatus = 'Missed'
WHERE LOWER(appointmentStatus) = 'booked' WHERE LOWER(appointmentStatus) = 'booked'
@@ -8,3 +9,37 @@ WHERE LOWER(appointmentStatus) = 'booked'
appointmentDate < CURRENT_DATE appointmentDate < CURRENT_DATE
OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME) 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');

View File

@@ -198,25 +198,4 @@ class DropdownControllerTest {
assertEquals(1, response.getBody().size()); assertEquals(1, response.getBody().size());
assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); 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());
}
} }