Bugzilla – Attachment 190860 Details for
Bug 37707
Lead/Trail times should work in combination
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 37707: Improve lead/trail period conflict detection
5b51b3e.patch (text/plain), 16.57 KB, created by
Martin Renvoize (ashimema)
on 2026-01-03 07:54:38 UTC
(
hide
)
Description:
Bug 37707: Improve lead/trail period conflict detection
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2026-01-03 07:54:38 UTC
Size:
16.57 KB
patch
obsolete
>From 5b51b3e2bb8d298e80049974c0942b2271c82efa Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Date: Fri, 2 Jan 2026 15:13:59 +0000 >Subject: [PATCH] Bug 37707: Improve lead/trail period conflict detection > >This implements the full "protected period" concept for bookings, where >each existing booking reserves: lead period + actual dates + trail period. > >Previously, we only checked: >- New booking lead vs existing booking actual dates >- New booking trail vs existing booking actual dates > >Now we ALSO check: >- New booking lead vs existing booking trail periods >- New booking trail vs existing booking lead periods > >This ensures librarians cannot create bookings too close together, >respecting both item preparation time (lead) and processing time (trail). > >Test plan: >1. Apply all patches and run: yarn build >2. Configure a bookable item with 2-day lead and 3-day trail periods >3. Create booking A: January 5-9 > - This reserves Jan 3-4 (lead), Jan 5-9 (actual), Jan 10-12 (trail) >4. Attempt to create booking B: January 12-15 with 2-day lead > - Lead would be Jan 10-11, which conflicts with booking A's trail >5. Verify days 10-11 are visually disabled with striped pattern >6. Verify hovering shows "conflicts with existing booking's trail period" >7. Verify you cannot select January 12 as start date >8. Verify January 13 IS selectable (no conflict) >9. Run Cypress tests to confirm: > npx cypress run --spec "t/cypress/integration/Circulation/bookingsModalDatePicker_spec.ts" >--- > .../prog/css/src/staff-global.scss | 27 +++ > .../prog/js/modals/place_booking.js | 210 +++++++++++++++++- > 2 files changed, 236 insertions(+), 1 deletion(-) > >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >index d9ad8218cba..3fe0e7f40ff 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -938,6 +938,33 @@ $dayTrailBackground: #fcdcb3 !default; > background: #f5f5f5 !important; /* Light background */ > cursor: not-allowed !important; > } >+ >+ /* BIDIRECTIONAL ENHANCEMENT: Visual indicators for existing bookings' protected periods */ >+ &.existingBookingLead { >+ /* Existing booking's lead period (preparation time before their booking starts) */ >+ /* Use diagonal stripes to distinguish from new booking's lead period */ >+ background: repeating-linear-gradient( >+ 45deg, >+ #e6d4f5, >+ #e6d4f5 10px, >+ #f0e6f8 10px, >+ #f0e6f8 20px >+ ); >+ opacity: 0.7; >+ } >+ >+ &.existingBookingTrail { >+ /* Existing booking's trail period (processing time after their booking ends) */ >+ /* Use diagonal stripes to distinguish from new booking's trail period */ >+ background: repeating-linear-gradient( >+ 45deg, >+ #fcd4e6, >+ #fcd4e6 10px, >+ #ffe6f0 10px, >+ #ffe6f0 20px >+ ); >+ opacity: 0.7; >+ } > } > > .input-warning { >diff --git a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js b/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >index a97348b6bbd..e1d19f2fc0d 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >@@ -937,6 +937,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > ) > : null; > >+ // Calculate new booking's lead/trail periods > const leadStart = startDate > ? startDate.subtract(leadDays, "day") > : hoverDate.subtract(leadDays, "day"); >@@ -944,8 +945,119 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > const trailStart = hoverDate; > const trailEnd = hoverDate.add(trailDays, "day"); > >+ // BIDIRECTIONAL ENHANCEMENT: Collect closest bookings for visual feedback >+ // and check for mathematical conflicts in a single pass >+ let closestBeforeBooking = null; >+ let closestBeforeDistance = Infinity; >+ >+ let closestAfterBooking = null; >+ let closestAfterDistance = Infinity; >+ > let leadDisable = false; > let trailDisable = false; >+ >+ bookings.forEach(booking => { >+ // Skip if we're editing this booking >+ if ( >+ booking_id && >+ booking_id == booking.booking_id >+ ) { >+ return; >+ } >+ >+ // Skip if not same item (for item-specific bookings) >+ if ( >+ booking.item_id && >+ booking_item_id && >+ booking.item_id != booking_item_id >+ ) { >+ return; >+ } >+ >+ const bookingStart = dayjs( >+ booking.start_date >+ ).startOf("day"); >+ const bookingEnd = dayjs( >+ booking.end_date >+ ).startOf("day"); >+ >+ // BIDIRECTIONAL: Mathematical checks for conflicts (works across month boundaries) >+ // Calculate this booking's full protected period >+ const existingLeadStart = bookingStart.subtract( >+ leadDays, >+ "day" >+ ); >+ const existingLeadEnd = bookingStart.subtract( >+ 1, >+ "day" >+ ); >+ const existingTrailStart = bookingEnd.add( >+ 1, >+ "day" >+ ); >+ const existingTrailEnd = bookingEnd.add( >+ trailDays, >+ "day" >+ ); >+ >+ // Check if new booking's LEAD period overlaps with existing booking's TRAIL period >+ if (!periodPicker.selectedDates[0]) { >+ if ( >+ leadStart.isSameOrBefore( >+ existingTrailEnd >+ ) && >+ leadEnd.isSameOrAfter( >+ existingTrailStart >+ ) >+ ) { >+ leadDisable = true; >+ } >+ } >+ >+ // Check if new booking's TRAIL period overlaps with existing booking's LEAD period >+ if (periodPicker.selectedDates[0]) { >+ if ( >+ trailStart.isSameOrBefore( >+ existingLeadEnd >+ ) && >+ trailEnd.isSameOrAfter( >+ existingLeadStart >+ ) >+ ) { >+ trailDisable = true; >+ } >+ } >+ >+ // Find closest bookings for visual feedback (when dates are in view) >+ if (bookingEnd.isBefore(hoverDate)) { >+ const distance = hoverDate.diff( >+ bookingEnd, >+ "day" >+ ); >+ if (distance < closestBeforeDistance) { >+ closestBeforeDistance = distance; >+ closestBeforeBooking = { >+ start: bookingStart, >+ end: bookingEnd, >+ }; >+ } >+ } >+ >+ if (bookingStart.isAfter(hoverDate)) { >+ const distance = bookingStart.diff( >+ hoverDate, >+ "day" >+ ); >+ if (distance < closestAfterDistance) { >+ closestAfterDistance = distance; >+ closestAfterBooking = { >+ start: bookingStart, >+ end: bookingEnd, >+ }; >+ } >+ } >+ }); >+ > periodPicker.calendarContainer > .querySelectorAll(".flatpickr-day") > .forEach(function (dayElem) { >@@ -953,6 +1065,15 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > dayElem.dateObj > ).startOf("day"); > >+ // Clear existing booking lead/trail classes >+ dayElem.classList.remove( >+ "existingBookingLead" >+ ); >+ dayElem.classList.remove( >+ "existingBookingTrail" >+ ); >+ >+ // Apply new booking's lead/trail period classes > dayElem.classList.toggle( > "leadRangeStart", > elemDate.isSame(leadStart) >@@ -979,7 +1100,62 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > "trailRangeEnd", > elemDate.isSame(trailEnd) > ); >- // If we're overlapping a disabled date, disable our hoverDate >+ >+ // BIDIRECTIONAL: Show closest existing booking's trail period >+ if (closestBeforeBooking && trailDays > 0) { >+ const existingTrailStart = >+ closestBeforeBooking.end.add( >+ 1, >+ "day" >+ ); >+ const existingTrailEnd = >+ closestBeforeBooking.end.add( >+ trailDays, >+ "day" >+ ); >+ >+ if ( >+ elemDate.isSameOrAfter( >+ existingTrailStart >+ ) && >+ elemDate.isSameOrBefore( >+ existingTrailEnd >+ ) >+ ) { >+ dayElem.classList.add( >+ "existingBookingTrail" >+ ); >+ } >+ } >+ >+ // BIDIRECTIONAL: Show closest existing booking's lead period >+ if (closestAfterBooking && leadDays > 0) { >+ const existingLeadStart = >+ closestAfterBooking.start.subtract( >+ leadDays, >+ "day" >+ ); >+ const existingLeadEnd = >+ closestAfterBooking.start.subtract( >+ 1, >+ "day" >+ ); >+ >+ if ( >+ elemDate.isSameOrAfter( >+ existingLeadStart >+ ) && >+ elemDate.isSameOrBefore( >+ existingLeadEnd >+ ) >+ ) { >+ dayElem.classList.add( >+ "existingBookingLead" >+ ); >+ } >+ } >+ >+ // Check for conflicts with flatpickr-disabled dates (original behavior) > if ( > dayElem.classList.contains( > "flatpickr-disabled" >@@ -1013,6 +1189,38 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > } > } > } >+ >+ // BIDIRECTIONAL: Check for conflicts with existing booking's trail period >+ if ( >+ !periodPicker.selectedDates[0] && >+ dayElem.classList.contains( >+ "existingBookingTrail" >+ ) >+ ) { >+ // New booking's lead period overlaps with existing booking's trail >+ if ( >+ elemDate.isSameOrAfter(leadStart) && >+ elemDate.isBefore(leadEnd) >+ ) { >+ leadDisable = true; >+ } >+ } >+ >+ // BIDIRECTIONAL: Check for conflicts with existing booking's lead period >+ if ( >+ dayElem.classList.contains( >+ "existingBookingLead" >+ ) >+ ) { >+ // New booking's trail period overlaps with existing booking's lead >+ if ( >+ elemDate.isAfter(trailStart) && >+ elemDate.isSameOrBefore(trailEnd) >+ ) { >+ trailDisable = true; >+ } >+ } >+ > dayElem.classList.remove("leadDisable"); > dayElem.classList.remove("trailDisable"); > dayElem.removeEventListener( >-- >2.52.0 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 37707
:
182718
|
190859
|
190860
|
190861
|
190862
|
191000
|
191001
|
191002
|
191003
|
191232
|
191233