Bugzilla – Attachment 191003 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: (follow-up) Add textual feedback for booking lead/trail period conflicts
2701651.patch (text/plain), 33.32 KB, created by
Martin Renvoize (ashimema)
on 2026-01-07 17:57:24 UTC
(
hide
)
Description:
Bug 37707: (follow-up) Add textual feedback for booking lead/trail period conflicts
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2026-01-07 17:57:24 UTC
Size:
33.32 KB
patch
obsolete
>From 270165130713bad22e0b2ac0d2aa365693987ac3 Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Date: Sat, 3 Jan 2026 07:34:09 +0000 >Subject: [PATCH] Bug 37707: (follow-up) Add textual feedback for booking > lead/trail period conflicts > >This patch adds a feedback message container below the calendar that >provides contextual information about lead/trail period requirements >and conflicts, improving accessibility and user understanding. > >The feedback updates dynamically on hover, showing: >- Info messages when hovering selectable dates >- Error messages when hovering dates disabled due to conflicts >- Specific explanations distinguishing between conflict types > (e.g., new lead vs existing trail, new trail vs existing lead) >- Multiple conflict messages when both lead and trail periods conflict >- Specific messages for past dates and insufficient lead time > >Key improvements: >- Standardized terminology consistently uses "lead period" and "trail period" >- Proper internationalization using __() for all user-facing strings >- DRY code with detectPeriodConflict() helper function >- Styling moved from inline JavaScript to SCSS using Bootstrap alert classes >- Empty/zero periods are handled gracefully (not mentioned in messages) >- Accurate conflict detection prioritizes what the hovered date IS over > what it conflicts with > >This complements the visual color indicators by providing explicit >textual explanations, improving accessibility and user understanding. > >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 an existing booking for January 10-15 (protected: Jan 8-18) >4. Open the booking calendar and observe feedback area below calendar > >5. Test past date and insufficient lead time messages: > - Hover on today's date > * Verify message: "Cannot select: Date is in the past" > - Hover on tomorrow (with 2-day lead period) > * Verify message: "Cannot select: Insufficient lead time (2 days required before start)" > - Hover on the day after tomorrow > * Verify message: "Cannot select: Insufficient lead time (2 days required before start)" > * Should NOT show "conflicts with existing booking" if no booking exists > >6. Test existing booking conflict messages: > - Hover over Jan 12 (part of existing booking) > * Verify message: "Cannot select: This date is part of an existing booking" > - Hover over Jan 8 (existing booking's lead period) > * Verify message: "Cannot select: This date is part of an existing booking's lead period" > - Hover over Jan 16 (existing booking's trail period) > * Verify message: "Cannot select: This date is part of an existing booking's trail period" > >7. Test lead/trail period conflict messages: > - Try to select Jan 19-20 as booking dates (2-day lead = Jan 17-18) > - Hover over Jan 19 when it appears disabled > * Verify error message: "Cannot select: Lead period (2 days before start) conflicts with existing booking's trail period" > >8. Test info messages for valid dates: > - Hover over Jan 21 (no conflicts, with lead period configured) > * Verify info message: "Selecting start date. Lead period: 2 days before start" > - Configure booking with NO lead period (0 or blank) > - Hover over a valid date > * Verify message: "Selecting start date" (no mention of lead period) > >9. Test multiple conflict messages: > - Position calendar to hover between two bookings where both lead and trail conflict > * Verify message shows both conflicts separated by semicolon > * Example: "Cannot select: lead period conflicts with existing booking's trail period; trail period conflicts with existing booking's lead period" > >10. Verify all feedback messages: > - Are properly translated (check .po files if applicable) > - Use consistent "lead period" and "trail period" terminology > - Are clear and helpful for librarians > - Update color coding (red for errors, blue for info) >--- > .../intranet-tmpl/prog/css/src/bookings.scss | 2 +- > .../prog/css/src/staff-global.scss | 13 + > .../prog/js/modals/place_booking.js | 400 +++++++++++++++++- > 3 files changed, 408 insertions(+), 7 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 a75fca56e89..8e3dec110a8 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -1103,6 +1103,19 @@ $leadClashTrail: #e5d4f5 !default; > } > } > >+// 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 { > background-color: #ff9; > border-color: #900; >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 dc04dc17d49..a95632600e6 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >@@ -922,6 +922,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", >@@ -1058,6 +1069,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > } > }); > >+ // Work through all days in view and add classes appropraitely based on hovered date > periodPicker.calendarContainer > .querySelectorAll(".flatpickr-day") > .forEach(function (dayElem) { >@@ -1085,7 +1097,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > "existingBookingTrailEnd" > ); > >- // Apply new booking's lead/trail period classes >+ // Apply proposed booking's lead/trail period classes > dayElem.classList.toggle( > "leadRangeStart", > elemDate.isSame(leadStart) >@@ -1113,7 +1125,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > elemDate.isSame(trailEnd) > ); > >- // BIDIRECTIONAL: Show closest existing booking's trail period >+ // Show closest preceding existing booking's trail period > if (closestBeforeBooking && trailDays > 0) { > const existingTrailStart = > closestBeforeBooking.end.add( >@@ -1159,7 +1171,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > } > } > >- // BIDIRECTIONAL: Show closest existing booking's lead period >+ // Show closest following existing booking's lead period > if (closestAfterBooking && leadDays > 0) { > const existingLeadStart = > closestAfterBooking.start.subtract( >@@ -1203,7 +1215,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > } > } > >- // Check for conflicts with flatpickr-disabled dates (original behavior) >+ // Check for conflicts with flatpickr-disabled dates > if ( > dayElem.classList.contains( > "flatpickr-disabled" >@@ -1238,7 +1250,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > } > } > >- // BIDIRECTIONAL: Check for conflicts with existing booking's trail period >+ // Check for conflicts with existing booking's trail period > if ( > !periodPicker.selectedDates[0] && > dayElem.classList.contains( >@@ -1254,7 +1266,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > } > } > >- // BIDIRECTIONAL: Check for conflicts with existing booking's lead period >+ // Check for conflicts with existing booking's lead period > if ( > dayElem.classList.contains( > "existingBookingLead" >@@ -1278,6 +1290,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"); > } >@@ -1291,6 +1322,363 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > true > ); > } >+ >+ // Helper function to detect what a period (lead or trail) conflicts with >+ // Returns an object with conflict type flags >+ function detectPeriodConflict( >+ startDate, >+ endDate, >+ calendarContainer >+ ) { >+ const conflict = { >+ withTrail: false, >+ withLead: false, >+ withBooking: false, >+ }; >+ >+ for ( >+ let checkDate = startDate; >+ checkDate.isSameOrBefore(endDate); >+ checkDate = checkDate.add(1, "day") >+ ) { >+ const dayElem = >+ calendarContainer.querySelector( >+ `.flatpickr-day[aria-label="${checkDate.format("MMMM D, YYYY")}"]` >+ ); >+ if (dayElem) { >+ if ( >+ dayElem.classList.contains( >+ "existingBookingTrail" >+ ) >+ ) { >+ conflict.withTrail = true; >+ break; >+ } >+ if ( >+ dayElem.classList.contains( >+ "existingBookingLead" >+ ) >+ ) { >+ conflict.withLead = true; >+ break; >+ } >+ if ( >+ dayElem.classList.contains( >+ "flatpickr-disabled" >+ ) >+ ) { >+ conflict.withBooking = true; >+ break; >+ } >+ } >+ } >+ >+ return conflict; >+ } >+ >+ // 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"; >+ >+ // CASE 1: Both lead and trail conflicts (selecting start between two bookings) >+ if ( >+ leadDisable && >+ trailDisable && >+ !periodPicker.selectedDates[0] >+ ) { >+ const leadConflict = >+ detectPeriodConflict( >+ leadStart, >+ leadEnd, >+ periodPicker.calendarContainer >+ ); >+ const trailConflict = >+ detectPeriodConflict( >+ trailStart.add(1, "day"), >+ trailEnd, >+ periodPicker.calendarContainer >+ ); >+ >+ let leadMsg = ""; >+ let trailMsg = ""; >+ >+ if (leadConflict.withTrail) { >+ leadMsg = __( >+ "lead period conflicts with existing booking's trail period" >+ ); >+ } else if (leadConflict.withLead) { >+ leadMsg = __( >+ "lead period conflicts with existing booking's lead period" >+ ); >+ } else if (leadConflict.withBooking) { >+ leadMsg = __( >+ "lead period conflicts with existing booking" >+ ); >+ } >+ >+ if (trailConflict.withLead) { >+ trailMsg = __( >+ "trail period conflicts with existing booking's lead period" >+ ); >+ } else if (trailConflict.withTrail) { >+ trailMsg = __( >+ "trail period conflicts with existing booking's trail period" >+ ); >+ } else if (trailConflict.withBooking) { >+ trailMsg = __( >+ "trail period conflicts with existing booking" >+ ); >+ } >+ >+ if (leadMsg && trailMsg) { >+ message = >+ __("Cannot select") + >+ ": " + >+ leadMsg + >+ "; " + >+ trailMsg; >+ } else if (leadMsg) { >+ message = >+ __("Cannot select") + >+ ": " + >+ leadMsg; >+ } else if (trailMsg) { >+ message = >+ __("Cannot select") + >+ ": " + >+ trailMsg; >+ } else { >+ message = __( >+ "Cannot select: Multiple conflicts with existing bookings" >+ ); >+ } >+ } >+ // CASE 2: Lead period conflict only (selecting start date) >+ else if ( >+ leadDisable && >+ !periodPicker.selectedDates[0] >+ ) { >+ if (hoverDate.isBefore(today)) { >+ message = __( >+ "Cannot select: Date is in the past" >+ ); >+ } else if ( >+ leadDays > 0 && >+ leadStart.isSameOrBefore(today) >+ ) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "Insufficient lead time (%s days required before start)" >+ ).format(leadDays); >+ } 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" >+ ); >+ } else if (leadDays > 0) { >+ const conflict = >+ detectPeriodConflict( >+ leadStart, >+ leadEnd, >+ periodPicker.calendarContainer >+ ); >+ >+ if (conflict.withTrail) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "Lead period (%s days before start) conflicts with existing booking's trail period" >+ ).format(leadDays); >+ } else if (conflict.withLead) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "Lead period (%s days before start) conflicts with existing booking's lead period" >+ ).format(leadDays); >+ } else if (conflict.withBooking) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "Lead period (%s days before start) conflicts with existing booking" >+ ).format(leadDays); >+ } else { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "Lead period (%s days before start) conflicts with existing booking" >+ ).format(leadDays); >+ } >+ } else { >+ message = __( >+ "Cannot select: Conflicts with existing booking" >+ ); >+ } >+ } >+ // CASE 3: Trail period conflict only (selecting end date) >+ else if (trailDisable) { >+ 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" >+ ); >+ } else if (trailDays > 0) { >+ const conflict = >+ detectPeriodConflict( >+ trailStart.add(1, "day"), >+ trailEnd, >+ periodPicker.calendarContainer >+ ); >+ >+ if (conflict.withLead) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "Trail period (%s days after return) conflicts with existing booking's lead period" >+ ).format(trailDays); >+ } else if (conflict.withTrail) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "Trail period (%s days after return) conflicts with existing booking's trail period" >+ ).format(trailDays); >+ } else if (conflict.withBooking) { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "Trail period (%s days after return) conflicts with existing booking" >+ ).format(trailDays); >+ } else { >+ message = >+ __("Cannot select") + >+ ": " + >+ __( >+ "Trail period (%s days after return) conflicts with existing booking" >+ ).format(trailDays); >+ } >+ } else { >+ message = __( >+ "Cannot select: Conflicts with existing booking" >+ ); >+ } >+ } >+ } else { >+ // Show helpful info messages when no conflicts >+ if (!periodPicker.selectedDates[0]) { >+ if (leadDays > 0) { >+ message = >+ __("Selecting start date") + >+ ". " + >+ __( >+ "Lead period: %s days before start" >+ ).format(leadDays); >+ } 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