refactor: lunch protection to find all gaps instead of largest

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-28 16:34:13 +01:00
co-authored by Sisyphus
parent 72270649b6
commit 91d20d474b
+62 -17
View File
@@ -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,