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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user