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
|
* Find all gaps in the middle window after accounting for bookings
|
||||||
* Returns the duration in minutes of the largest gap
|
* Returns an array of gap durations in minutes, sorted descending
|
||||||
*/
|
*/
|
||||||
export function findLargestLunchGap(
|
export function findAllLunchGaps(
|
||||||
middleWindowStart: number,
|
middleWindowStart: number,
|
||||||
middleWindowEnd: number,
|
middleWindowEnd: number,
|
||||||
existingBookings: TimeSlot[],
|
existingBookings: TimeSlot[],
|
||||||
proposedBooking?: TimeSlot
|
proposedBooking?: TimeSlot
|
||||||
): number {
|
): number[] {
|
||||||
const allBookings: TimeSlot[] = [...existingBookings];
|
const allBookings: TimeSlot[] = [...existingBookings];
|
||||||
if (proposedBooking) {
|
if (proposedBooking) {
|
||||||
allBookings.push(proposedBooking);
|
allBookings.push(proposedBooking);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter to only bookings that overlap with the middle window
|
|
||||||
const relevantBookings = allBookings
|
const relevantBookings = allBookings
|
||||||
.filter((booking) => {
|
.filter((booking) => {
|
||||||
const bookingStart = timeToMinutes(booking.startTime);
|
const bookingStart = timeToMinutes(booking.startTime);
|
||||||
@@ -86,30 +85,49 @@ export function findLargestLunchGap(
|
|||||||
.sort((a, b) => a.startTime - b.startTime);
|
.sort((a, b) => a.startTime - b.startTime);
|
||||||
|
|
||||||
if (relevantBookings.length === 0) {
|
if (relevantBookings.length === 0) {
|
||||||
return middleWindowEnd - middleWindowStart;
|
return [middleWindowEnd - middleWindowStart];
|
||||||
}
|
}
|
||||||
|
|
||||||
let largestGap = 0;
|
const gaps: number[] = [];
|
||||||
|
|
||||||
const firstBookingStart = relevantBookings[0].startTime;
|
const firstBookingStart = relevantBookings[0].startTime;
|
||||||
if (firstBookingStart > middleWindowStart) {
|
if (firstBookingStart > middleWindowStart) {
|
||||||
largestGap = Math.max(largestGap, firstBookingStart - middleWindowStart);
|
gaps.push(firstBookingStart - middleWindowStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < relevantBookings.length - 1; i++) {
|
for (let i = 0; i < relevantBookings.length - 1; i++) {
|
||||||
const gapStart = relevantBookings[i].endTime;
|
const gapStart = relevantBookings[i].endTime;
|
||||||
const gapEnd = relevantBookings[i + 1].startTime;
|
const gapEnd = relevantBookings[i + 1].startTime;
|
||||||
if (gapEnd > gapStart) {
|
if (gapEnd > gapStart) {
|
||||||
largestGap = Math.max(largestGap, gapEnd - gapStart);
|
gaps.push(gapEnd - gapStart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastBookingEnd = relevantBookings[relevantBookings.length - 1].endTime;
|
const lastBookingEnd = relevantBookings[relevantBookings.length - 1].endTime;
|
||||||
if (lastBookingEnd < middleWindowEnd) {
|
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;
|
export const LUNCH_MINIMUM_USER = 60;
|
||||||
@@ -141,10 +159,12 @@ export function checkLunchProtection(
|
|||||||
endTime: proposedSlotEnd
|
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 (isAdmin) {
|
||||||
if (largestGap < LUNCH_MINIMUM_ADMIN) {
|
if (gapWith < LUNCH_MINIMUM_ADMIN && gapWith < gapWithout) {
|
||||||
return {
|
return {
|
||||||
isBlocked: true,
|
isBlocked: true,
|
||||||
showWarning: false,
|
showWarning: false,
|
||||||
@@ -152,18 +172,43 @@ export function checkLunchProtection(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (largestGap < LUNCH_WARNING_THRESHOLD) {
|
if (gapWith < LUNCH_WARNING_THRESHOLD && gapWith < gapWithout) {
|
||||||
const gapMinutes = Math.round(largestGap);
|
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 {
|
return {
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
showWarning: true,
|
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 };
|
return { isBlocked: false, showWarning: false };
|
||||||
} else {
|
} else {
|
||||||
if (largestGap < LUNCH_MINIMUM_USER) {
|
if (gapWith < LUNCH_MINIMUM_USER && gapWith < gapWithout) {
|
||||||
return {
|
return {
|
||||||
isBlocked: true,
|
isBlocked: true,
|
||||||
showWarning: false,
|
showWarning: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user