diff --git a/frontend/src/lib/lunchProtection.ts b/frontend/src/lib/lunchProtection.ts index effd50d..30ca7b6 100644 --- a/frontend/src/lib/lunchProtection.ts +++ b/frontend/src/lib/lunchProtection.ts @@ -58,21 +58,20 @@ export function calculateMiddleWindow( } /** - * Find the largest gap in the middle window after accounting for bookings - * Returns the duration in minutes of the largest gap + * Find all gaps in the middle window after accounting for bookings + * Returns an array of gap durations in minutes, sorted descending */ -export function findLargestLunchGap( +export function findAllLunchGaps( middleWindowStart: number, middleWindowEnd: number, existingBookings: TimeSlot[], proposedBooking?: TimeSlot -): number { +): number[] { const allBookings: TimeSlot[] = [...existingBookings]; if (proposedBooking) { allBookings.push(proposedBooking); } - // Filter to only bookings that overlap with the middle window const relevantBookings = allBookings .filter((booking) => { const bookingStart = timeToMinutes(booking.startTime); @@ -86,30 +85,49 @@ export function findLargestLunchGap( .sort((a, b) => a.startTime - b.startTime); if (relevantBookings.length === 0) { - return middleWindowEnd - middleWindowStart; + return [middleWindowEnd - middleWindowStart]; } - let largestGap = 0; + const gaps: number[] = []; const firstBookingStart = relevantBookings[0].startTime; if (firstBookingStart > middleWindowStart) { - largestGap = Math.max(largestGap, firstBookingStart - middleWindowStart); + gaps.push(firstBookingStart - middleWindowStart); } for (let i = 0; i < relevantBookings.length - 1; i++) { const gapStart = relevantBookings[i].endTime; const gapEnd = relevantBookings[i + 1].startTime; if (gapEnd > gapStart) { - largestGap = Math.max(largestGap, gapEnd - gapStart); + gaps.push(gapEnd - gapStart); } } const lastBookingEnd = relevantBookings[relevantBookings.length - 1].endTime; if (lastBookingEnd < middleWindowEnd) { - largestGap = Math.max(largestGap, middleWindowEnd - lastBookingEnd); + gaps.push(middleWindowEnd - lastBookingEnd); } - return largestGap; + return gaps.sort((a, b) => b - a); +} + +/** + * Find the largest gap in the middle window after accounting for bookings + * Returns the duration in minutes of the largest gap + */ +export function findLargestLunchGap( + middleWindowStart: number, + middleWindowEnd: number, + existingBookings: TimeSlot[], + proposedBooking?: TimeSlot +): number { + const gaps = findAllLunchGaps( + middleWindowStart, + middleWindowEnd, + existingBookings, + proposedBooking + ); + return gaps.length > 0 ? gaps[0] : 0; } export const LUNCH_MINIMUM_USER = 60; @@ -141,10 +159,12 @@ export function checkLunchProtection( endTime: proposedSlotEnd }; - const largestGap = findLargestLunchGap(windowStart, windowEnd, existingBookings, proposedBooking); + const gapWithout = findLargestLunchGap(windowStart, windowEnd, existingBookings); + const gapsWith = findAllLunchGaps(windowStart, windowEnd, existingBookings, proposedBooking); + const gapWith = gapsWith[0] ?? 0; if (isAdmin) { - if (largestGap < LUNCH_MINIMUM_ADMIN) { + if (gapWith < LUNCH_MINIMUM_ADMIN && gapWith < gapWithout) { return { isBlocked: true, showWarning: false, @@ -152,18 +172,43 @@ export function checkLunchProtection( }; } - if (largestGap < LUNCH_WARNING_THRESHOLD) { - const gapMinutes = Math.round(largestGap); + if (gapWith < LUNCH_WARNING_THRESHOLD && gapWith < gapWithout) { + const startH = parseInt(proposedSlotStart.split(':')[0], 10); + const startM = parseInt(proposedSlotStart.split(':')[1], 10); + const endH = parseInt(proposedSlotEnd.split(':')[0], 10); + const endM = parseInt(proposedSlotEnd.split(':')[1], 10); + const fmt = (h: number, m: number) => { + const p = h >= 12 ? 'PM' : 'AM'; + return `${h % 12 || 12}:${String(m).padStart(2, '0')} ${p}`; + }; + + const viableGaps = gapsWith.filter((g) => g >= LUNCH_MINIMUM_ADMIN); + if (viableGaps.length >= 2 && gapsWith.every((g) => g >= LUNCH_MINIMUM_ADMIN)) { + const sorted = [...viableGaps].sort((a, b) => b - a); + if (sorted[0] === sorted[1]) { + return { + isBlocked: false, + showWarning: true, + warningMessage: `This booking (${fmt(startH, startM)}–${fmt(endH, endM)}) would split your suggested lunch break into two ${sorted[0]} minute blocks.` + }; + } + return { + isBlocked: false, + showWarning: true, + warningMessage: `This booking (${fmt(startH, startM)}–${fmt(endH, endM)}) would split your suggested lunch break into ${sorted[0]} and ${sorted[1]} minute blocks.` + }; + } + return { isBlocked: false, showWarning: true, - warningMessage: `Warning: This booking would reduce lunch break to ${gapMinutes} minutes.` + warningMessage: `This booking (${fmt(startH, startM)}–${fmt(endH, endM)}) would reduce lunch break to ${gapWith} minutes.` }; } return { isBlocked: false, showWarning: false }; } else { - if (largestGap < LUNCH_MINIMUM_USER) { + if (gapWith < LUNCH_MINIMUM_USER && gapWith < gapWithout) { return { isBlocked: true, showWarning: false,