feat: edit request time blockers, today closing time, UI polish, and test fixes

- Add time blocker management for booking edit requests
- Add closing_time field to admin today/current-next endpoint
- Update UserBookingModal and CurrentAppointment UI components
- Fix fmt import in bookings_test.go (was missing)
- Fix created_by FK in TestAdminApproveEditRequest_TimeBlockerOverlap
- Update test coverage for edit request time blocker overlap
- Update gap backlog documentation
This commit is contained in:
2026-05-10 16:53:17 +01:00
parent f3fb44f401
commit 83c62ffb97
10 changed files with 950 additions and 37 deletions
@@ -351,6 +351,70 @@
async function submitReschedule() {
if (!selectedBooking || !rescheduleDate || !rescheduleTime) return;
// Re-fetch available hours to confirm slot is still open
try {
const dateStr = rescheduleDate.toString();
const monthKey = `${rescheduleDate.year}-${String(rescheduleDate.month).padStart(2, '0')}`;
const startOfMonth = new CalendarDate(rescheduleDate.year, rescheduleDate.month, 1);
const endOfMonth = new CalendarDate(rescheduleDate.year, rescheduleDate.month, rescheduleDate.calendar.getDaysInMonth(rescheduleDate));
const [whRes, ahRes] = await Promise.all([
fetch(`/api/scheduling/working-hours?start=${startOfMonth}&end=${endOfMonth}`),
fetch(`/api/scheduling/available-hours?start=${startOfMonth}&end=${endOfMonth}`)
]);
if (whRes.ok && ahRes.ok) {
const whData: WorkingHoursDay[] = await whRes.json();
const ahData: AvailableHoursDay[] = await ahRes.json();
const freshWH: Record<string, { isOpen: boolean; startTime: string; endTime: string }> = {};
whData.forEach((d) => { freshWH[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime }; });
const freshAH: Record<string, { isOpen: boolean; slots: Array<{ startTime: string; endTime: string }> }> = {};
ahData.forEach((d) => { freshAH[d.date] = { isOpen: d.isOpen, slots: d.slots }; });
const dayWH = freshWH[dateStr];
const dayAH = freshAH[dateStr];
if (!dayWH?.isOpen || !dayAH?.slots) {
toast.error('This date is no longer available. Please select a different date.');
rescheduleDate = undefined;
rescheduleTime = '';
return;
}
// Check if the selected time is still available
const freshSlots: string[] = [];
for (const slot of dayAH.slots) {
const [sh, sm] = slot.startTime.split(':').map(Number);
const [eh, em] = slot.endTime.split(':').map(Number);
for (let m = sh * 60 + sm; m < eh * 60 + em; m += 15) {
if (m + totalDuration <= eh * 60 + em) {
freshSlots.push(`${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`);
}
}
}
if (!freshSlots.includes(rescheduleTime)) {
toast.error('This time slot is no longer available. Please choose a different time.');
rescheduleTime = '';
return;
}
// Check lunch protection
const existingBookings = extractBookedSlots(dayWH.startTime, dayWH.endTime, dayAH.slots);
const freshLunch = getLunchProtectionForSlots(dayWH.startTime, dayWH.endTime, existingBookings, totalDuration, 15, false);
if (freshLunch.get(rescheduleTime)?.isBlocked) {
toast.error('This time slot is no longer available. Please choose a different time.');
rescheduleTime = '';
return;
}
}
} catch {
toast.error('Could not verify slot availability. Please try again.');
return;
}
rescheduleSubmitting = true;
try {
const [hours, minutes] = rescheduleTime.split(':').map(Number);