Bugzilla – Attachment 192006 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: Add bidirectional lead/trail period conflict detection
12c1a89.patch (text/plain), 52.20 KB, created by
Martin Renvoize (ashimema)
on 2026-01-25 13:33:51 UTC
(
hide
)
Description:
Bug 37707: Add bidirectional lead/trail period conflict detection
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2026-01-25 13:33:51 UTC
Size:
52.20 KB
patch
obsolete
>From 12c1a8913d9fc2f571a5b879448bc04b9c636e35 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: Add bidirectional lead/trail period conflict > detection > >Enhance the booking modal to detect and prevent lead/trail period >conflicts in both directions: > >- New booking's lead period vs existing booking's trail period >- New booking's trail period vs existing booking's lead period > >Visual feedback: >- Highlight existing booking's lead periods (before booking starts) >- Highlight existing booking's trail periods (after booking ends) >- Add CSS classes for start/end boundaries with rounded corners > >User feedback: >- Show contextual messages explaining why dates are blocked >- Differentiate between lead period, trail period, and booking conflicts >- Display informational messages when hovering available dates > >The conflict detection uses mathematical date range comparisons that >work across month boundaries, not just visible calendar dates. >--- > .../intranet-tmpl/prog/css/src/bookings.scss | 2 +- > .../prog/css/src/staff-global.scss | 201 +++++- > .../prog/js/modals/place_booking.js | 651 +++++++++++++++++- > 3 files changed, 815 insertions(+), 39 deletions(-) > >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/bookings.scss b/koha-tmpl/intranet-tmpl/prog/css/src/bookings.scss >index 3f72fbf488f..0f9a7da6fea 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/bookings.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/bookings.scss >@@ -10,4 +10,4 @@ > ); > } > } >-} >\ No newline at end of file >+} >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 ebb964719da..23c42d00425 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -884,26 +884,33 @@ input { > > $dayLeadBackground: #fad2cf !default; > $dayTrailBackground: #fcdcb3 !default; >+$existingLeadBackground: #e8f0d4 !default; >+$existingTrailBackground: #d0e6f5 !default; >+$trailClashLead: #d0f0e0 !default; >+$leadClashTrail: #e5d4f5 !default; > .flatpickr-day { >- > &.leadRangeStart { > border-radius: 50px 0 0 50px; > } > > &.leadRange { >- box-shadow: -2.5 * $dayMargin 0 0 $dayLeadBackground, 2.5 * $dayMargin 0 0 $dayLeadBackground; >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $dayLeadBackground, >+ 2.5 * $dayMargin 0 0 $dayLeadBackground; > background: $dayLeadBackground; > &.flatpickr-disabled { > $darkLeadBackground: darken($dayLeadBackground, 10); >- box-shadow: -2.5 * $dayMargin 0 0 $darkLeadBackground, 2.5 * $dayMargin 0 0 $darkLeadBackground; >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $darkLeadBackground, >+ 2.5 * $dayMargin 0 0 $darkLeadBackground; > background: $darkLeadBackground; > } > } > > &.selected { >- &.leadRangeEnd { >- border-radius: 0; >- } >+ &.leadRangeEnd { >+ border-radius: 0; >+ } > } > > &:hover, >@@ -914,11 +921,15 @@ $dayTrailBackground: #fcdcb3 !default; > } > > &.trailRange { >- box-shadow: -2.5 * $dayMargin 0 0 $dayTrailBackground, 2.5 * $dayMargin 0 0 $dayTrailBackground; >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $dayTrailBackground, >+ 2.5 * $dayMargin 0 0 $dayTrailBackground; > background: $dayTrailBackground; > &.flatpickr-disabled { > $darkTrailBackground: darken($dayTrailBackground, 10); >- box-shadow: -2.5 * $dayMargin 0 0 $darkTrailBackground, 2.5 * $dayMargin 0 0 $darkTrailBackground; >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $darkTrailBackground, >+ 2.5 * $dayMargin 0 0 $darkTrailBackground; > background: $darkTrailBackground; > } > } >@@ -928,16 +939,182 @@ $dayTrailBackground: #fcdcb3 !default; > } > > &.leadDisable { >- color: #aaa !important; /* Gray out the text */ >- background: #f5f5f5 !important; /* Light background */ >+ color: #aaa !important; /* Gray out the text */ >+ background: #f5f5f5 !important; /* Light background */ > cursor: not-allowed !important; > } > > &.trailDisable { >- color: #aaa !important; /* Gray out the text */ >- background: #f5f5f5 !important; /* Light background */ >+ color: #aaa !important; /* Gray out the text */ >+ background: #f5f5f5 !important; /* Light background */ > cursor: not-allowed !important; > } >+ >+ /* BIDIRECTIONAL ENHANCEMENT: Visual indicators for existing bookings' protected periods */ >+ >+ /* Existing booking's lead period (pastel green - preparation time before their booking) */ >+ &.existingBookingLeadStart { >+ border-radius: 50px 0 0 50px; >+ >+ /* Remove right rounding if it meets new booking trail start */ >+ &.trailRange { >+ border-radius: 0; >+ } >+ >+ /* Special case: leadRangeEnd + trailRangeStart on existingBookingLeadStart */ >+ /* Left shadow should be pink to match the leadRange to the left */ >+ &.leadRangeEnd.trailRangeStart { >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $dayLeadBackground, >+ 2.5 * $dayMargin 0 0 $existingLeadBackground !important; >+ } >+ >+ /* Special case: trailRangeEnd on existingBookingLeadStart */ >+ /* Right shadow should be pastel green to match the existingBookingLead to the right */ >+ &.trailRangeEnd { >+ background: $existingLeadBackground !important; /* Green base fills the corners */ >+ border-radius: 0 !important; /* Right-side radius */ >+ position: relative; >+ z-index: 1; >+ >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $trailClashLead, >+ 2.5 * $dayMargin 0 0 $existingLeadBackground !important; >+ >+ &::before { >+ content: ""; >+ position: absolute; >+ top: 0; >+ left: -1px; >+ right: 0px; >+ bottom: 0; >+ background: $trailClashLead; /* Olive collision color */ >+ border-radius: 0 50px 50px 0; >+ z-index: -1; >+ } >+ >+ display: flex; >+ align-items: center; >+ justify-content: center; >+ } >+ } >+ >+ &.existingBookingLead { >+ background: $existingLeadBackground; /* Pastel green */ >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $existingLeadBackground, >+ 2.5 * $dayMargin 0 0 $existingLeadBackground; >+ } >+ >+ /* New booking trail overlaps with existing booking lead */ >+ &.trailRange.existingBookingLead { >+ background: $trailClashLead !important; /* Light olive (orange + green merge) */ >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $trailClashLead, >+ 2.5 * $dayMargin 0 0 $trailClashLead; >+ } >+ >+ &.trailRangeEnd.existingBookingLead { >+ background: $existingLeadBackground !important; /* Base fills corners with Existing Lead Green */ >+ border-radius: 0 !important; >+ position: relative; >+ z-index: 1; >+ >+ /* Shadows: Left matches the previous zone, Right matches the Green zone */ >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $trailClashLead, >+ 2.5 * $dayMargin 0 0 $existingLeadBackground !important; >+ >+ &::before { >+ content: ""; >+ position: absolute; >+ top: 0; >+ right: 0; >+ left: -1px; >+ bottom: 0; >+ background: $trailClashLead; /* Light olive overlap color */ >+ border-radius: 0 50px 50px 0; >+ z-index: -1; >+ } >+ >+ display: flex; >+ align-items: center; >+ justify-content: center; >+ } >+ >+ /* Existing booking's trail period (pastel blue - processing time after their booking) */ >+ &.existingBookingTrailEnd { >+ border-radius: 0 50px 50px 0; >+ >+ /* Remove left rounding if it meets new booking lead end */ >+ &.leadRange { >+ border-radius: 0; >+ } >+ >+ /* Special case: leadRangeEnd + trailRangeStart on existingBookingTrailEnd */ >+ /* Left shadow should be lavender to match the leadRange.existingBookingTrail to the left */ >+ &.leadRangeEnd.trailRangeStart { >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $leadClashTrail, >+ 2.5 * $dayMargin 0 0 $existingTrailBackground !important; >+ } >+ } >+ >+ &.existingBookingTrail { >+ background: $existingTrailBackground; /* Pastel blue */ >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $existingTrailBackground, >+ 2.5 * $dayMargin 0 0 $existingTrailBackground; >+ } >+ >+ /* New booking lead overlaps with existing booking trail */ >+ &.leadRange.existingBookingTrail { >+ background: $leadClashTrail !important; /* Lavender (pink + blue merge) */ >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $leadClashTrail, >+ 2.5 * $dayMargin 0 0 $leadClashTrail; >+ } >+ >+ &.leadRangeStart.existingBookingTrail { >+ background: $existingTrailBackground !important; /* Blue base fills the top/bottom left corners */ >+ border-radius: 0 !important; /* Base cell must stay square */ >+ position: relative; >+ z-index: 1; >+ >+ /* Shadows: Left is Blue, Right is Purple */ >+ box-shadow: >+ -2.5 * $dayMargin 0 0 $existingTrailBackground, >+ 2.5 * $dayMargin 0 0 $leadClashTrail !important; >+ >+ &::before { >+ content: ""; >+ position: absolute; >+ top: 0; >+ left: 0; >+ right: -1px; >+ bottom: 0; >+ background: $leadClashTrail; /* Lavender */ >+ border-radius: 50px 0 0 50px; >+ z-index: -1; >+ } >+ >+ display: flex; >+ align-items: center; >+ justify-content: center; >+ } >+} >+ >+// Booking conflict feedback message styles >+.booking-conflict-feedback { >+ // Override Bootstrap's default alert padding and sizing for compact display >+ padding: 0.5rem 0.75rem; >+ margin-top: 0.5rem; >+ margin-bottom: 0 !important; // Override Bootstrap's default 1rem margin-bottom >+ min-height: 1.25rem; >+ text-align: center; >+ >+ // Bootstrap's .alert-danger, .alert-warning, and .alert-info classes >+ // provide the color schemes via CSS variables when combined with .alert > } > > .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 b5b7e9452fb..8c983485864 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >@@ -1050,6 +1050,17 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > ); > } > >+ // Create feedback message container below the calendar >+ let feedbackDiv = periodPicker.calendarContainer.querySelector( >+ ".booking-conflict-feedback" >+ ); >+ if (!feedbackDiv) { >+ feedbackDiv = document.createElement("div"); >+ feedbackDiv.className = >+ "booking-conflict-feedback alert d-none"; >+ periodPicker.calendarContainer.appendChild(feedbackDiv); >+ } >+ > // Add hints for days before the start range and after the end range > periodPicker.calendarContainer.addEventListener( > "mouseover", >@@ -1065,15 +1076,188 @@ $("#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"); >- const leadEnd = startDate ? startDate : hoverDate; >- const trailStart = hoverDate; >- const trailEnd = hoverDate.add(trailDays, "day"); >+ const leadEnd = startDate >+ ? startDate.subtract(1, "day") >+ : hoverDate.subtract(1, "day"); >+ const trailStart = startDate >+ ? hoverDate.add(1, "day") >+ : hoverDate.add(1, "day"); >+ const trailEnd = startDate >+ ? hoverDate.add(trailDays, "day") >+ : 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; >+ >+ // Track conflict reasons for messaging >+ let leadConflictReason = { >+ withTrail: false, >+ withLead: false, >+ withBooking: false, >+ }; >+ let trailConflictReason = { >+ withTrail: false, >+ withLead: false, >+ withBooking: 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 >+ if (!periodPicker.selectedDates[0]) { >+ // Check overlap with existing booking's trail period >+ if ( >+ leadStart.isSameOrBefore( >+ existingTrailEnd >+ ) && >+ leadEnd.isSameOrAfter( >+ existingTrailStart >+ ) >+ ) { >+ leadDisable = true; >+ leadConflictReason.withTrail = true; >+ } >+ // Check overlap with existing booking's lead period >+ else if ( >+ leadStart.isSameOrBefore( >+ existingLeadEnd >+ ) && >+ leadEnd.isSameOrAfter(existingLeadStart) >+ ) { >+ leadDisable = true; >+ leadConflictReason.withLead = true; >+ } >+ // Check overlap with existing booking itself >+ else if ( >+ leadStart.isSameOrBefore(bookingEnd) && >+ leadEnd.isSameOrAfter(bookingStart) >+ ) { >+ leadDisable = true; >+ leadConflictReason.withBooking = true; >+ } >+ } >+ >+ // Check if new booking's TRAIL period overlaps with existing booking >+ if (periodPicker.selectedDates[0]) { >+ // Check overlap with existing booking's lead period >+ if ( >+ trailStart.isSameOrBefore( >+ existingLeadEnd >+ ) && >+ trailEnd.isSameOrAfter( >+ existingLeadStart >+ ) >+ ) { >+ trailDisable = true; >+ trailConflictReason.withLead = true; >+ } >+ // Check overlap with existing booking's trail period >+ else if ( >+ trailStart.isSameOrBefore( >+ existingTrailEnd >+ ) && >+ trailEnd.isSameOrAfter( >+ existingTrailStart >+ ) >+ ) { >+ trailDisable = true; >+ trailConflictReason.withTrail = true; >+ } >+ // Check overlap with existing booking itself >+ else if ( >+ trailStart.isSameOrBefore(bookingEnd) && >+ trailEnd.isSameOrAfter(bookingStart) >+ ) { >+ trailDisable = true; >+ trailConflictReason.withBooking = 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, >+ }; >+ } >+ } >+ }); >+ >+ // Work through all days in view and add classes appropraitely based on hovered date > periodPicker.calendarContainer > .querySelectorAll(".flatpickr-day") > .forEach(function (dayElem) { >@@ -1081,33 +1265,156 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > dayElem.dateObj > ).startOf("day"); > >- dayElem.classList.toggle( >- "leadRangeStart", >- elemDate.isSame(leadStart) >+ // Clear existing booking lead/trail classes (including start/end) >+ dayElem.classList.remove( >+ "existingBookingLead" > ); >- dayElem.classList.toggle( >- "leadRange", >- elemDate.isSameOrAfter(leadStart) && >- elemDate.isBefore(leadEnd) >+ dayElem.classList.remove( >+ "existingBookingLeadStart" > ); >- dayElem.classList.toggle( >- "leadRangeEnd", >- elemDate.isSame(leadEnd) >+ dayElem.classList.remove( >+ "existingBookingLeadEnd" > ); >- dayElem.classList.toggle( >- "trailRangeStart", >- elemDate.isSame(trailStart) >+ dayElem.classList.remove( >+ "existingBookingTrail" > ); >- dayElem.classList.toggle( >- "trailRange", >- elemDate.isAfter(trailStart) && >- elemDate.isSameOrBefore(trailEnd) >+ dayElem.classList.remove( >+ "existingBookingTrailStart" > ); >- dayElem.classList.toggle( >- "trailRangeEnd", >- elemDate.isSame(trailEnd) >+ dayElem.classList.remove( >+ "existingBookingTrailEnd" > ); >- // If we're overlapping a disabled date, disable our hoverDate >+ >+ // Apply proposed booking's lead/trail period classes >+ // Only apply lead classes if lead period > 0 >+ if (leadDays > 0) { >+ dayElem.classList.toggle( >+ "leadRangeStart", >+ elemDate.isSame(leadStart) >+ ); >+ dayElem.classList.toggle( >+ "leadRange", >+ elemDate.isSameOrAfter(leadStart) && >+ elemDate.isSameOrBefore(leadEnd) >+ ); >+ dayElem.classList.toggle( >+ "leadRangeEnd", >+ elemDate.isSame(leadEnd) >+ ); >+ } >+ >+ // Only apply trail classes if trail period > 0 >+ if (trailDays > 0) { >+ dayElem.classList.toggle( >+ "trailRangeStart", >+ elemDate.isSame(trailStart) >+ ); >+ dayElem.classList.toggle( >+ "trailRange", >+ elemDate.isSameOrAfter( >+ trailStart >+ ) && >+ elemDate.isSameOrBefore( >+ trailEnd >+ ) >+ ); >+ dayElem.classList.toggle( >+ "trailRangeEnd", >+ elemDate.isSame(trailEnd) >+ ); >+ } >+ >+ // Show closest preceding 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" >+ ); >+ // Add start/end classes for rounded borders >+ if ( >+ elemDate.isSame( >+ existingTrailStart >+ ) >+ ) { >+ dayElem.classList.add( >+ "existingBookingTrailStart" >+ ); >+ } >+ if ( >+ elemDate.isSame( >+ existingTrailEnd >+ ) >+ ) { >+ dayElem.classList.add( >+ "existingBookingTrailEnd" >+ ); >+ } >+ } >+ } >+ >+ // Show closest following 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" >+ ); >+ // Add start/end classes for rounded borders >+ if ( >+ elemDate.isSame( >+ existingLeadStart >+ ) >+ ) { >+ dayElem.classList.add( >+ "existingBookingLeadStart" >+ ); >+ } >+ if ( >+ elemDate.isSame(existingLeadEnd) >+ ) { >+ dayElem.classList.add( >+ "existingBookingLeadEnd" >+ ); >+ } >+ } >+ } >+ >+ // Check for conflicts with flatpickr-disabled dates > if ( > dayElem.classList.contains( > "flatpickr-disabled" >@@ -1116,12 +1423,15 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > if ( > !periodPicker.selectedDates[0] && > elemDate.isSameOrAfter(leadStart) && >- elemDate.isBefore(leadEnd) >+ elemDate.isSameOrBefore(leadEnd) > ) { > leadDisable = true; > } > if ( >- elemDate.isAfter(trailStart) && >+ periodPicker.selectedDates[0] && >+ elemDate.isSameOrAfter( >+ trailStart >+ ) && > elemDate.isSameOrBefore(trailEnd) > ) { > // Only consider this a conflict if the disabled date is within the max date range >@@ -1141,6 +1451,41 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > } > } > } >+ >+ // 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.isSameOrBefore(leadEnd) >+ ) { >+ leadDisable = true; >+ } >+ } >+ >+ // Check for conflicts with existing booking's lead period >+ if ( >+ periodPicker.selectedDates[0] && >+ dayElem.classList.contains( >+ "existingBookingLead" >+ ) >+ ) { >+ // New booking's trail period overlaps with existing booking's lead >+ if ( >+ elemDate.isSameOrAfter( >+ trailStart >+ ) && >+ elemDate.isSameOrBefore(trailEnd) >+ ) { >+ trailDisable = true; >+ } >+ } >+ > dayElem.classList.remove("leadDisable"); > dayElem.classList.remove("trailDisable"); > dayElem.removeEventListener( >@@ -1150,6 +1495,25 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > ); > }); > >+ // Additional check for hovering directly on existing booking's lead/trail periods >+ // If hovering on an existing booking's lead period when selecting start date, block selection >+ if ( >+ !periodPicker.selectedDates[0] && >+ target.classList.contains("existingBookingLead") >+ ) { >+ leadDisable = true; >+ } >+ >+ // If hovering on an existing booking's trail period when selecting end date, block selection >+ if ( >+ periodPicker.selectedDates[0] && >+ target.classList.contains( >+ "existingBookingTrail" >+ ) >+ ) { >+ trailDisable = true; >+ } >+ > if (leadDisable) { > target.classList.add("leadDisable"); > } >@@ -1163,6 +1527,241 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > true > ); > } >+ >+ // Update feedback message >+ const feedbackDiv = >+ periodPicker.calendarContainer.querySelector( >+ ".booking-conflict-feedback" >+ ); >+ if (feedbackDiv) { >+ let message = ""; >+ let messageType = "info"; // info, warning, error >+ >+ // Determine what the hovered date is (needed for both error and info messages) >+ const today = dayjs().startOf("day"); >+ const isDisabled = >+ target.classList.contains( >+ "flatpickr-disabled" >+ ); >+ const isInExistingLead = >+ target.classList.contains( >+ "existingBookingLead" >+ ); >+ const isInExistingTrail = >+ target.classList.contains( >+ "existingBookingTrail" >+ ); >+ >+ // Generate appropriate feedback messages based on conflicts >+ if (leadDisable || trailDisable) { >+ messageType = "error"; >+ >+ // When selecting START date (no date selected yet) >+ if (!periodPicker.selectedDates[0]) { >+ // Check direct state first (what IS this date?) >+ if (hoverDate.isBefore(today)) { >+ message = __( >+ "Cannot select: date is in the past" >+ ); >+ } else if (isDisabled) { >+ message = __( >+ "Cannot select: this date is part of an existing booking" >+ ); >+ } else if (isInExistingLead) { >+ message = __( >+ "Cannot select: this date is part of an existing booking's lead period" >+ ); >+ } else if (isInExistingTrail) { >+ message = __( >+ "Cannot select: this date is part of an existing booking's trail period" >+ ); >+ } >+ // Then check calculated lead period conflicts >+ else if ( >+ leadDays > 0 && >+ leadStart.isSameOrBefore(today) >+ ) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "insufficient lead time (%s days required before start)" >+ ).format(leadDays); >+ } else if (leadDays > 0) { >+ // Use mathematical conflict detection (works across month boundaries) >+ if (leadConflictReason.withTrail) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "lead period (%s days before start) conflicts with an existing booking's trail period" >+ ).format(leadDays); >+ } else if ( >+ leadConflictReason.withLead >+ ) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "lead period (%s days before start) conflicts with an existing booking's lead period" >+ ).format(leadDays); >+ } else if ( >+ leadConflictReason.withBooking >+ ) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "lead period (%s days before start) conflicts with an existing booking" >+ ).format(leadDays); >+ } >+ } else { >+ message = __( >+ "Cannot select: conflicts with an existing booking" >+ ); >+ } >+ } >+ // When selecting END date (start date already selected) >+ else if (periodPicker.selectedDates[0]) { >+ // Check direct state first (what IS this date?) >+ if (isDisabled) { >+ message = __( >+ "Cannot select: this date is part of an existing booking" >+ ); >+ } else if (isInExistingLead) { >+ message = __( >+ "Cannot select: this date is part of an existing booking's lead period" >+ ); >+ } else if (isInExistingTrail) { >+ message = __( >+ "Cannot select: this date is part of an existing booking's trail period" >+ ); >+ } >+ // Then check calculated trail period conflicts >+ else if (trailDays > 0) { >+ // Use mathematical conflict detection (works across month boundaries) >+ if (trailConflictReason.withLead) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "trail period (%s days after return) conflicts with an existing booking's lead period" >+ ).format(trailDays); >+ } else if ( >+ trailConflictReason.withTrail >+ ) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "trail period (%s days after return) conflicts with an existing booking's trail period" >+ ).format(trailDays); >+ } else if ( >+ trailConflictReason.withBooking >+ ) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "trail period (%s days after return) conflicts with an existing booking" >+ ).format(trailDays); >+ } >+ } else { >+ message = __( >+ "Cannot select: conflicts with existing an booking" >+ ); >+ } >+ } >+ } else { >+ // Show helpful info messages when no conflicts >+ if (!periodPicker.selectedDates[0]) { >+ // When selecting start date, show both lead and trail info >+ if (leadDays > 0 && trailDays > 0) { >+ message = >+ __("Selecting start date") + >+ ". " + >+ __( >+ "Lead period: %s days before start" >+ ).format(leadDays) + >+ ". " + >+ __( >+ "Trail period: %s days after return" >+ ).format(trailDays); >+ } else if (leadDays > 0) { >+ message = >+ __("Selecting start date") + >+ ". " + >+ __( >+ "Lead period: %s days before start" >+ ).format(leadDays); >+ } else if (trailDays > 0) { >+ message = >+ __("Selecting start date") + >+ ". " + >+ __( >+ "Trail period: %s days after return" >+ ).format(trailDays); >+ } else { >+ message = __( >+ "Selecting start date" >+ ); >+ } >+ messageType = "info"; >+ } else { >+ if (trailDays > 0) { >+ message = >+ __("Selecting end date") + >+ ". " + >+ __( >+ "Trail period: %s days after return" >+ ).format(trailDays); >+ } else { >+ message = __("Selecting end date"); >+ } >+ messageType = "info"; >+ } >+ >+ // Show additional context if hovering over existing booking periods >+ if (isInExistingLead) { >+ message += >+ " ⢠" + >+ __( >+ "hovering existing booking's lead period" >+ ); >+ } else if (isInExistingTrail) { >+ message += >+ " ⢠" + >+ __( >+ "hovering existing booking's trail period" >+ ); >+ } >+ } >+ >+ feedbackDiv.textContent = message; >+ feedbackDiv.classList.remove( >+ "alert-danger", >+ "alert-warning", >+ "alert-info" >+ ); >+ >+ if (message) { >+ feedbackDiv.classList.remove("d-none"); >+ // Apply appropriate Bootstrap alert class based on message type >+ if (messageType === "error") { >+ feedbackDiv.classList.add( >+ "alert-danger" >+ ); >+ } else if (messageType === "warning") { >+ feedbackDiv.classList.add( >+ "alert-warning" >+ ); >+ } else { >+ feedbackDiv.classList.add("alert-info"); >+ } >+ } else { >+ feedbackDiv.classList.add("d-none"); >+ } >+ } > } > } > ); >-- >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
|
191350
|
191351
|
191352
|
191353
|
191354
|
191355
|
191356
|
191357
|
191358
|
191359
|
191360
|
191361
|
191394
|
191395
|
191746
|
192006
|
192007
|
192008
|
192009
|
192010
|
192800
|
192801
|
192802
|
192803
|
192804
|
192805
|
192806
|
192807
|
192863
|
192864
|
192865
|
192866
|
192867
|
192868
|
192869
|
192870