Bug 37707

Summary: Lead/Trail times should work in combination
Product: Koha Reporter: Martin Renvoize (ashimema) <martin.renvoize>
Component: CirculationAssignee: Martin Renvoize (ashimema) <martin.renvoize>
Status: Pushed to main --- QA Contact: Nick Clemens (kidclamp) <nick>
Severity: enhancement    
Priority: P5 - low CC: andrew, dbarden, gmcharlt, kkrueger, kyle, paul.derscheid
Version: Main   
Hardware: All   
OS: All   
GIT URL: Initiative type: ---
Sponsorship status: --- Comma delimited list of Sponsors:
Crowdfunding goal: 0 Patch complexity: Medium patch
Documentation contact: Documentation submission:
Text to go in the release notes:
Version(s) released in:
26.05.00
Circulation function: Bookings
Bug Depends on: 40134    
Bug Blocks: 41129, 41514    
Attachments: Bug 37707: First draft
Bug 37707: Add Cypress tests for bidirectional lead/trail period conflicts
Bug 37707: Improve lead/trail period conflict detection
Bug 37707: (follow-up) Add visual refinements for existing booking lead/trail periods
Bug 37707: (follow-up) Add textual feedback for booking lead/trail period conflicts
Bug 37707: Add Cypress tests for bidirectional lead/trail period conflicts
Bug 37707: Improve lead/trail period conflict detection
Bug 37707: (follow-up) Add visual refinements for existing booking lead/trail periods
Bug 37707: (follow-up) Add textual feedback for booking lead/trail period conflicts
BookingBLeadConflic.png
BookingBLeadConflict_table.png
Bug 37707: Add Cypress tests for bidirectional lead/trail period conflicts
Bug 37707: Improve lead/trail period conflict detection
Bug 37707: (follow-up) Add visual refinements for existing booking lead/trail periods
Bug 37707: (follow-up) Add textual feedback for booking lead/trail period conflicts
Bug 37707: Fix timezone handling in place_booking modal
Bug 37707: Add Cypress tests for timezone handling in place_booking modal
Bug 37707: Add Cypress tests for bidirectional lead/trail period conflicts
Bug 37707: Improve lead/trail period conflict detection
Bug 37707: (follow-up) Add visual refinements for existing booking lead/trail periods
Bug 37707: (follow-up) Add textual feedback for booking lead/trail period conflicts
Bug 37707: Fix timezone handling in place_booking modal
Bug 37707: Add Cypress tests for timezone handling in place_booking modal
BookingEnddates.png
39999000000634Booking.png
Bug 37707: (follow-up) Fix timezone handling for cross-timezone bookings
Bug 37707: Add bidirectional lead/trail period conflict detection
Bug 37707: (follow-up) Support lead/trail periods for 'any item' bookings
Bug 37707: (follow-up) Fix timezone handling for bookings
Bug 37707: (follow-up) Refactor place_booking.js for maintainability
Bug 37707: (follow-up) Add comprehensive Cypress tests for bookings modal
Bug 37707: Add bidirectional lead/trail period conflict detection
Bug 37707: (follow-up) Support lead/trail periods for 'any item' bookings
Bug 37707: (follow-up) Fix timezone handling for bookings
Bug 37707: (follow-up) Refactor place_booking.js for maintainability
Bug 37707: (follow-up) Add comprehensive Cypress tests for bookings modal
Bug 37707: (QA follow-up) Fix typo in comment
Bug 37707: (QA follow-up) Fix grammatical error
Bug 37707: (QA follow-up) Fix incorrect bold date expectations in Cypress test
Bug 37707: Add bidirectional lead/trail period conflict detection
Bug 37707: (follow-up) Support lead/trail periods for 'any item' bookings
Bug 37707: (follow-up) Fix timezone handling for bookings
Bug 37707: (follow-up) Refactor place_booking.js for maintainability
Bug 37707: (follow-up) Add comprehensive Cypress tests for bookings modal
Bug 37707: (QA follow-up) Fix typo in comment
Bug 37707: (QA follow-up) Fix grammatical error
Bug 37707: (QA follow-up) Fix incorrect bold date expectations in Cypress test

Description Martin Renvoize (ashimema) 2024-08-22 09:21:24 UTC
Whilst working on the lead/trail time addition to bookings we discussed how they should affect things.

In the end we decided that they should work in combination when placing bookings so the window of unbookable time before a bookings should be the previous bookings 'trail' period + the current bookings 'lead' period.

I've run out of capacity to make that change in the aforementioned bug, so I'm splitting it out here to work on that as a expedient follow-up.

Lets get the core functionality in and then continue to polish it.

A- | TA >|< LB | -B- | TB >|< LC | -C
Comment 1 Martin Renvoize (ashimema) 2025-05-21 13:26:31 UTC
*** Bug 39390 has been marked as a duplicate of this bug. ***
Comment 2 Martin Renvoize (ashimema) 2025-05-22 06:48:35 UTC Comment hidden (obsolete)
Comment 3 Andrew Fuerste-Henry 2025-09-15 20:51:57 UTC
I can confirm this works as described -- bookings cannot be created such that the trailing time of one booking intersects with the leading time of the next. The interface grays out dates that cannot be selected and changes the mouse icon to a crossed-out-circle. Martin, what do you still feel is missing in the display here? Our libraries would love to see this functionality in Koha sooner rather than later.
Comment 4 Kristi Krueger 2025-10-01 19:54:59 UTC
We are eager to see this one move forward.
Comment 5 Martin Renvoize (ashimema) 2025-10-24 06:54:46 UTC
I'm still targeting 25.11 for this. There's been a few holdups in terms of getting consistent Cypress tests working upstream of this (I don't want to break existing functionality whilst working on this), and the test infrastructure has been changing multiple times whilst I've been working on that.  There's also a Vue refactor of the current Modal code to make extending it simpler, which Paul has ready to submit.

In short.. It's all just around the corner, but every time I hit it of late, I find the tides have shifted beneath my feet.
Comment 6 Martin Renvoize (ashimema) 2026-01-03 07:54:36 UTC Comment hidden (obsolete)
Comment 7 Martin Renvoize (ashimema) 2026-01-03 07:54:38 UTC Comment hidden (obsolete)
Comment 8 Martin Renvoize (ashimema) 2026-01-03 07:54:39 UTC Comment hidden (obsolete)
Comment 9 Martin Renvoize (ashimema) 2026-01-03 07:54:41 UTC Comment hidden (obsolete)
Comment 10 Kristi Krueger 2026-01-05 13:50:42 UTC
Is this something that could be tested in a sandbox?
Comment 11 Andrew Fuerste-Henry 2026-01-05 20:05:16 UTC
(In reply to Martin Renvoize (ashimema) from comment #9)
> Created attachment 190862 [details] [review] [review]
> Bug 37707: (follow-up) Add textual feedback for booking lead/trail period
> conflicts

This seems to be enforcing allowable dates correctly, but the messaging doesn't match what's given in the test plan and is not entirely accurate.

Additionally, some messages use "lead period" and "trail period" which other use  "preparation time" and "processing period." We should use "lead period" and "trail period" consistently.

> 5. Hover over Jan 12 (part of existing booking)
>    - Verify message: "This date is part of an existing booking"

Message I receive here is "Cannot select: Lead period (2 days before start) conflicts with existing booking." 
This is accurate in that the whole of the lead period for the new booking would be within the date range of the existing booking. But the test plan says I should just get told that the date I'm trying to select is within an existing booking and that would be a simpler message.

> 6. Hover over Jan 8 (existing booking's lead period)
>    - Verify message explains it's part of the lead period

Message I receive here is "Cannot select: Processing period (3 days after return) conflicts with existing booking's preparation time." 
This is potentially accurate -- if I'm trying to make a booking that starts on 1/8 and ends on 1/9, then the trail period of the new booking would include 1/9, which is in the lead period for my existing booking. But it would be more clear and true to tell me I can't start a booking on 1/8 because 1/8 is within the lead time for the existing booking.

> 7. 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: "conflicts with existing booking's trail period"

Hovering over 1/19 or 1/20, I get the message "Cannot select: Lead period (2 days before start) conflicts with existing booking."
This is inaccurate. I should be told the new lead period would conflict with the existing trail period.

Hovering over 1/16, 1/17, or 1/18, I get the message "Cannot select: Lead period (2 days before start) conflicts with existing item's processing time."
This is inaccurate for 1/16; the lead time for a booking starting 1/16 would be fully within the existing booking rather than in its trail period.

> 8. Hover over Jan 21 (no conflicts)
>    - Verify info message about lead/trail requirements

These messages get confusing if I edit my circ rules so that I'm only setting a lead time *or* a trail time. If I put a 2 in "Booking pre-processing" and leave "Booking post-processing" blank, the circ rules interface actually saves a blank  booking_trail_period value and then the message when placing a booking reads "Selecting end date. Processing period: days after return" It should either not mention lead periods or trail periods at all if the given value is blank. In fact, it should probably not mention them for values of 0, either.

Additionally, if I have a lead period of 2 days defined and try to place a booking starting fewer than 2 days in the future, I get the message "Cannot select: Lead period (2 days before start) conflicts with existing booking." I get this message even on a record with no existing bookings at all. If I'm trying to select a date in the past, it should tell me I cannot select a date in the past. If I'm trying to select a date that would put my lead period in the past, it should tell me that the date cannot be selected because it would provide insufficient lead time.
Comment 12 Martin Renvoize (ashimema) 2026-01-07 17:57:20 UTC Comment hidden (obsolete)
Comment 13 Martin Renvoize (ashimema) 2026-01-07 17:57:22 UTC Comment hidden (obsolete)
Comment 14 Martin Renvoize (ashimema) 2026-01-07 17:57:23 UTC Comment hidden (obsolete)
Comment 15 Martin Renvoize (ashimema) 2026-01-07 17:57:24 UTC Comment hidden (obsolete)
Comment 16 Martin Renvoize (ashimema) 2026-01-07 17:59:03 UTC
Thanks for the feedback Andrew, hopefully my replaced last patch makes the messages clearer.

Kristi, yes you should be able to test this using a sandbox I believe.
Comment 17 Andrew Fuerste-Henry 2026-01-07 21:38:00 UTC
Following the test plan in comment 15, I've run into some issues.
I've got a bib with just 1 item on it. That item is bookable.

After creating a booking for January 10-15, the calendar to create a new booking has January 10-16 greyed out, as if my booking were 10-16.

In the calendar, mousing over days in the past gives error messages as if all past dates are part of an existing booking.

When mousing over a date to select the start of a booking, the system gives feedback about the potential booking's trail period. I would suggest we should only give feedback about the potential booking's trail period when selecting the end of the potential booking. Until the user is proposing a booking end date, we cannot know what that booking's trail will conflict with.

The datepicker and message area generally appeared above the Booking Dates input field. However, sometimes the datepicker and message area appeared below the input field instead. When this happened, the bottom of the message field pushed out of my window and could not be seen without zooming my browswer out. I was not able to scroll down to see the rest of the message.

The messages contain inconsistent capitalization. Errors all start "Cannot select:" but the first word following that colon is sometimes capitalized and sometimes not.

 (In reply to Martin Renvoize (ashimema) from comment #15)
> 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"

This gives me the message "lead period conflicts with existing booking; trail period conflicts with an existing booking." The first statement will be true if I select 1/12 as the start of my booking. The second statement will be true if I select 1/12 as the end of my booking. In either case, the proposed message of "This date is part of an existing booking" would be more clear.

>    - Hover over Jan 8 (existing booking's lead period)
>      * Verify message: "Cannot select: This date is part of an existing
> booking's lead period"

This gives me the message "lead period conflicts with existing booking; trail period conflicts with existing booking's lead period." The first statement is *not* true, the lead period for a booking starting 1/8 would need to start 1/6, which is in the past and does not have a booking.

>    - Hover over Jan 16 (existing booking's trail period)
>      * Verify message: "Cannot select: This date is part of an existing
> booking's trail period"

This gives me the message "This date is part of an exiting booking." This is *not* true, 1/16 is part of the trail period of my existing booking.
Comment 18 Kristi Krueger 2026-01-09 22:20:40 UTC
In the first test I did, things looked almost right (the color-coding is really helpful)

1. Placed booking A for dates 1/12-1/17. Lead period should be 1/10 and 1/11. Trail period should be 1/18, 19, and 20.

2. Going to place booking B, and attempting to start my lead time on 1/20 is still allowing the booking without an error message. The message in blue reads "Selecting start date. Lead period: 2 days before start" 
screenshot attached (BookingBLeadConflic.png). With this booking, the lead time would start on 1/20, which is still the trail period for Booking A, and should be prevented. I was able to submit the booking (BookingBLeadConflict_table.png).
Comment 19 Kristi Krueger 2026-01-09 22:21:43 UTC Comment hidden (obsolete)
Comment 20 Kristi Krueger 2026-01-09 22:22:09 UTC Comment hidden (obsolete)
Comment 21 Martin Renvoize (ashimema) 2026-01-13 13:31:29 UTC Comment hidden (obsolete)
Comment 22 Martin Renvoize (ashimema) 2026-01-13 13:31:30 UTC Comment hidden (obsolete)
Comment 23 Martin Renvoize (ashimema) 2026-01-13 13:31:32 UTC Comment hidden (obsolete)
Comment 24 Martin Renvoize (ashimema) 2026-01-13 13:31:34 UTC Comment hidden (obsolete)
Comment 25 Martin Renvoize (ashimema) 2026-01-13 13:31:35 UTC Comment hidden (obsolete)
Comment 26 Martin Renvoize (ashimema) 2026-01-13 13:31:37 UTC Comment hidden (obsolete)
Comment 27 Martin Renvoize (ashimema) 2026-01-13 13:40:21 UTC Comment hidden (obsolete)
Comment 28 Martin Renvoize (ashimema) 2026-01-13 13:40:23 UTC Comment hidden (obsolete)
Comment 29 Martin Renvoize (ashimema) 2026-01-13 13:40:24 UTC Comment hidden (obsolete)
Comment 30 Martin Renvoize (ashimema) 2026-01-13 13:40:26 UTC Comment hidden (obsolete)
Comment 31 Martin Renvoize (ashimema) 2026-01-13 13:40:28 UTC Comment hidden (obsolete)
Comment 32 Martin Renvoize (ashimema) 2026-01-13 13:40:29 UTC Comment hidden (obsolete)
Comment 33 Martin Renvoize (ashimema) 2026-01-13 13:43:26 UTC
OK.. I've fixed up the messages now and also reconsidered the timezone handling as there were clearly some gremlins crept back in.

Should all be ready for another round of testing pretty please and thankyou.
Comment 34 Andrew Fuerste-Henry 2026-01-13 19:04:06 UTC
Everything I did in the UI worked great! Messages clear and accurate.

But I've got a failing Cypress test:
  1) Booking Modal Date Picker Tests
       should handle date range validation correctly:
     AssertionError: Timed out retrying after 10000ms: expected '<span.flatpickr-day.leadDisable>' to have class 'selected'
      at cy.get.should.timeout (webpack://koha/./t/cypress/support/flatpickr.js:337:58)
Comment 35 Kristi Krueger 2026-01-13 20:01:21 UTC
I set my browser timezone for PST. As before, I did 2 days lead and 3 days trail.

When setting a booking for Jan. 16 only (I could not use Jan. 15 with the 2 day lead, which is expected.) I noticed the following:

1. The start and end dates on the booking table for the bib record are 01/16/2026 and 01/17/2026. Shouldn't I see same start and end date there?

2. Trying to book a second booking, the lead and trail time looked to be enforced. I tried placing a second booking on 1/21, and got an error because the lead time would conflict with the 1st booking's trail time. Nice! I could place a booking beginning on 1/22, which would put booking B's lead time (1/20 and 1/21) out of conflict with Booking A's trail time (1/17, 1/18, 1/19). 

3. Placing Booking B for dates 1/22-1/27 shows an end date in the table of 1/28 though (BookingEnddates.png)

I am going to try working through the rest of the test plan bookings.
Comment 36 Kristi Krueger 2026-01-13 20:02:13 UTC Comment hidden (obsolete)
Comment 37 Kristi Krueger 2026-01-13 20:19:45 UTC
I've also noticed that for bookings on bibs with more than one item, the calendar is showing conflicts for the other items. For example, on bib 15, I have two bookings placed on item 	39999000000672. When I go to place a booking on item 39999000000634, the calendar/date picker gives me the same errors as if I were trying to place a conflicting booking on 39999000000672. See 39999000000634Booking.png
Comment 38 Kristi Krueger 2026-01-13 20:20:24 UTC Comment hidden (obsolete)
Comment 39 Kristi Krueger 2026-01-13 20:57:37 UTC
I've gone through:
Test plan:
1. Set browser to PST (UTC-8), server to UTC
2. Create a booking for January 15
3. Verify booking displays on Jan 15 only (not Jan 15-16)
4. Create a 3-day booking (Jan 15-17)
5. Verify displays on exactly 3 days
6. Test conflict detection with overlapping bookings
7. Repeat tests with browser in HST (UTC-10) and NZDT (UTC+13)


-- I ran into trouble with NZT. The end date in the table was adding two days. For example, for a booking set on the calendar for 1/16-1/18, the table end date is reading 1/20/2026. In the date picker, the booking dates (not trail time) cover  1/16, 17, 18 and 19. The trail period goes through 1/22 instead of 1/21. 

To recap from before: 
-- for PST and HST, the date picker dates looked correct, but the end date in the table was always an extra day.
-- When trying to book on another item from the same bib, the date picker calendar will show ineligible dates from bookings on another item. If I hard refreshed the page, the display in the date picker would reset, but this could be confusing to users.

Let me know if I can look at anything else here.
Comment 40 Paul Derscheid 2026-01-21 09:02:22 UTC
Hi folks, started QA on this bug yesterday.

Problem: implicit timezones
- The timezone issues stem from the code being implicitly timezone-aware without declaring which timezone is authoritative.
- When code uses .toISOString() or .toDate(), it performs timezone conversions as side effects rather than intentional operations. This works when browser and server timezones happen to match, but breaks otherwise.

The QA failures demonstrate this:
- PST browser: end date shifts +1 day
- NZDT browser: end date shifts +2 days
- The shift correlates directly with UTC offset

Potential fix for this: the library timezone is authoritative

I dug up some examples to check how other booking systems handle this, since they have the same problem:
- Airbnb: Check-in/checkout times are in "local time" (property timezone)(www.airbnb.com/help/article/41)
- Booking.com: "All timestamps returned by Booking.com's Demand API are in UTC... convert from UTC dynamically based on user or property location" and "Timing is based on the property's local timezone"
(developers.booking.com/demand/docs/development-guide/code-conventions,
 developers.booking.com/connectivity/docs/reporting-api/b_xml-reporting)

My understanding is that booking dates should be interpreted in the library's timezone, not the patron's. This makes sense because:
- The item is physically (for common use cases right now) at the library
- Pickup/return happens during library hours
- Lead/trail periods align with library operations

How I implemented this:
- Form submission - Extract the DATE value the user selected (just YYYY-MM-DD), then send as explicit UTC boundaries using dayjs.utc() to ensure startOf/endOf operate in UTC, not browser timezone:
  dayjs.utc(startDate).startOf("day").toISOString()  // "2026-01-20T00:00:00.000Z"
  dayjs.utc(endDate).endOf("day").toISOString()      // "2026-01-20T23:59:59.999Z"
- This preserves the date regardless of browser timezone.
- Timeline display - Convert stored UTC back to library timezone using $timezone(), then use $toDisplayDate() helper to create Date objects that render consistently:
  const startServerTz = dayjs(data.start_date).tz($timezone());
  start: $toDisplayDate(startServerTz)
- This ensures staff see dates in library time, not their browser's time.
- Conflict detection - All date comparisons normalized to start-of-day to avoid time-component interference.

The fix works, but the timezone handling should be made more explicit:
1. UI indicator - Show "(Library time)" near the date picker so patrons in different timezones understand (conditionally, only show if needed due to tz differences)
2. API contract - Document that booking dates represent library-local days, stored as UTC
3. Future-proofing - When hourly bookings are added, the same principle applies: times are in library timezone, converted to UTC for storage

This approach eliminates timezone bugs by design. I have a follow-up patch to validate the idea and to retest what surfaced in QA. What do we think?
Comment 41 Paul Derscheid 2026-01-21 09:09:10 UTC Comment hidden (obsolete)
Comment 42 Martin Renvoize (ashimema) 2026-01-25 13:33:51 UTC
Created attachment 192006 [details] [review]
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.
Comment 43 Martin Renvoize (ashimema) 2026-01-25 13:33:53 UTC
Created attachment 192007 [details] [review]
Bug 37707: (follow-up) Support lead/trail periods for 'any item' bookings

When booking "any item of type X", lead/trail period handling must
consider that multiple items may be available:

- Track per-item conflicts instead of global conflicts
- Only disable dates when ALL items of the type have conflicts
- Find closest "all items booked" dates for visual feedback
- Use mathematical search (180 days) to detect conflicts across
  month boundaries

Visual feedback for 'any item' mode:
- Show lead/trail periods around "all items booked" dates
- These represent dates where no item of the type is available

Skip redundant class-based conflict checks since mathematical
detection already handles cross-month boundary cases.
Comment 44 Martin Renvoize (ashimema) 2026-01-25 13:33:54 UTC
Created attachment 192008 [details] [review]
Bug 37707: (follow-up) Fix timezone handling for bookings

Fix date handling issues that caused bookings to shift by a day
when the browser timezone differs from the server timezone:

- Normalize dates to browser timezone for calendar display
- Use dayjs without UTC conversion for bookingsByDate keys
- Extract local date and send as explicit UTC day boundaries
- Preserve user's selected DATE regardless of browser timezone

Timeline display:
- Convert server dates to library timezone for display
- Add $timezone() and $toDisplayDate() helper functions

This ensures that a booking for "June 15" displays and saves as
June 15 regardless of the user's timezone offset.
Comment 45 Martin Renvoize (ashimema) 2026-01-25 13:33:55 UTC
Created attachment 192009 [details] [review]
Bug 37707: (follow-up) Refactor place_booking.js for maintainability

Extract utility functions and reduce code duplication:

- Add toInt(), startOfDay(), datesOverlap(), isDateInRange() helpers
- Simplify containsAny() using Set and Array.some()
- Refactor isItemAvailableForPeriod() to use new utilities
- Refactor getAvailableItemsOnDate() and isDateDisabledForSpecificItem()
- Extract submit handler helpers: getAvailableItemsForPeriod(),
  buildBookingPayload(), createTimelineItem(), showBookingError(),
  showBookingSuccess(), refreshBookingsTable(), setPickerDates()
- Replace parseInt() calls with toInt() for consistency
- Simplify modal reset handler with jQuery method chaining

No functional changes - prepares codebase for future Vue conversion.
Comment 46 Martin Renvoize (ashimema) 2026-01-25 13:33:57 UTC
Created attachment 192010 [details] [review]
Bug 37707: (follow-up) Add comprehensive Cypress tests for bookings modal

Reorganize and expand Cypress tests into three focused spec files:

bookingsModalBasic_spec.ts:
- Modal initialization and field interactions
- Form validation and submission
- Edit booking functionality
- 'Any item' booking with server-side item selection

bookingsModalDatePicker_spec.ts:
- Flatpickr initialization and constraints
- Date range validation
- Circulation rules (issue length, renewals)
- Lead/trail period conflict detection
- 'Any item' mode date availability
- Event dots for existing bookings

bookingsModalTimezone_spec.ts:
- Cross-timezone date display
- Multi-day booking spans
- Conflict detection with timezone awareness
- API round-trip date preservation
- Month boundary handling

Add shared test utilities to e2e.js for date helpers.
Comment 47 Paul Derscheid 2026-02-09 14:50:25 UTC
Created attachment 192800 [details] [review]
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.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Comment 48 Paul Derscheid 2026-02-09 14:50:28 UTC
Created attachment 192801 [details] [review]
Bug 37707: (follow-up) Support lead/trail periods for 'any item' bookings

When booking "any item of type X", lead/trail period handling must
consider that multiple items may be available:

- Track per-item conflicts instead of global conflicts
- Only disable dates when ALL items of the type have conflicts
- Find closest "all items booked" dates for visual feedback
- Use mathematical search (180 days) to detect conflicts across
  month boundaries

Visual feedback for 'any item' mode:
- Show lead/trail periods around "all items booked" dates
- These represent dates where no item of the type is available

Skip redundant class-based conflict checks since mathematical
detection already handles cross-month boundary cases.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Comment 49 Paul Derscheid 2026-02-09 14:50:31 UTC
Created attachment 192802 [details] [review]
Bug 37707: (follow-up) Fix timezone handling for bookings

Fix date handling issues that caused bookings to shift by a day
when the browser timezone differs from the server timezone:

- Normalize dates to browser timezone for calendar display
- Use dayjs without UTC conversion for bookingsByDate keys
- Extract local date and send as explicit UTC day boundaries
- Preserve user's selected DATE regardless of browser timezone

Timeline display:
- Convert server dates to library timezone for display
- Add $timezone() and $toDisplayDate() helper functions

This ensures that a booking for "June 15" displays and saves as
June 15 regardless of the user's timezone offset.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Comment 50 Paul Derscheid 2026-02-09 14:50:34 UTC
Created attachment 192803 [details] [review]
Bug 37707: (follow-up) Refactor place_booking.js for maintainability

Extract utility functions and reduce code duplication:

- Add toInt(), startOfDay(), datesOverlap(), isDateInRange() helpers
- Simplify containsAny() using Set and Array.some()
- Refactor isItemAvailableForPeriod() to use new utilities
- Refactor getAvailableItemsOnDate() and isDateDisabledForSpecificItem()
- Extract submit handler helpers: getAvailableItemsForPeriod(),
  buildBookingPayload(), createTimelineItem(), showBookingError(),
  showBookingSuccess(), refreshBookingsTable(), setPickerDates()
- Replace parseInt() calls with toInt() for consistency
- Simplify modal reset handler with jQuery method chaining

No functional changes - prepares codebase for future Vue conversion.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Comment 51 Paul Derscheid 2026-02-09 14:50:37 UTC
Created attachment 192804 [details] [review]
Bug 37707: (follow-up) Add comprehensive Cypress tests for bookings modal

Reorganize and expand Cypress tests into three focused spec files:

bookingsModalBasic_spec.ts:
- Modal initialization and field interactions
- Form validation and submission
- Edit booking functionality
- 'Any item' booking with server-side item selection

bookingsModalDatePicker_spec.ts:
- Flatpickr initialization and constraints
- Date range validation
- Circulation rules (issue length, renewals)
- Lead/trail period conflict detection
- 'Any item' mode date availability
- Event dots for existing bookings

bookingsModalTimezone_spec.ts:
- Cross-timezone date display
- Multi-day booking spans
- Conflict detection with timezone awareness
- API round-trip date preservation
- Month boundary handling

Add shared test utilities to e2e.js for date helpers.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Comment 52 Paul Derscheid 2026-02-09 14:50:40 UTC
Created attachment 192805 [details] [review]
Bug 37707: (QA follow-up) Fix typo in comment

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Comment 53 Paul Derscheid 2026-02-09 14:50:43 UTC
Created attachment 192806 [details] [review]
Bug 37707: (QA follow-up) Fix grammatical error

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Comment 54 Paul Derscheid 2026-02-09 14:50:46 UTC
Created attachment 192807 [details] [review]
Bug 37707: (QA follow-up) Fix incorrect bold date expectations in Cypress test

- The test for circulation rules date calculations did not account for
  the start date being bold, only the period endpoint dates
- place_booking.js explicitly adds the selected start date to boldDates
  (boldDates = [new Date(startDate)]), giving it the "title" class
- The verification loop checking for unexpected bold dates then failed
  when it encountered the start date, which was bold but not expected
- Add clearZoneStart to expectedBoldDates to match actual behavior

To test:
- yarn cypress run --spec t/cypress/integration/Circulation/bookingsModalDatePicker_spec.ts
- Verify that the "should handle circulation rules date calculations and visual feedback" test now passes

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Comment 55 Nick Clemens (kidclamp) 2026-02-10 12:48:40 UTC
Created attachment 192863 [details] [review]
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.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Comment 56 Nick Clemens (kidclamp) 2026-02-10 12:48:44 UTC
Created attachment 192864 [details] [review]
Bug 37707: (follow-up) Support lead/trail periods for 'any item' bookings

When booking "any item of type X", lead/trail period handling must
consider that multiple items may be available:

- Track per-item conflicts instead of global conflicts
- Only disable dates when ALL items of the type have conflicts
- Find closest "all items booked" dates for visual feedback
- Use mathematical search (180 days) to detect conflicts across
  month boundaries

Visual feedback for 'any item' mode:
- Show lead/trail periods around "all items booked" dates
- These represent dates where no item of the type is available

Skip redundant class-based conflict checks since mathematical
detection already handles cross-month boundary cases.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Comment 57 Nick Clemens (kidclamp) 2026-02-10 12:48:47 UTC
Created attachment 192865 [details] [review]
Bug 37707: (follow-up) Fix timezone handling for bookings

Fix date handling issues that caused bookings to shift by a day
when the browser timezone differs from the server timezone:

- Normalize dates to browser timezone for calendar display
- Use dayjs without UTC conversion for bookingsByDate keys
- Extract local date and send as explicit UTC day boundaries
- Preserve user's selected DATE regardless of browser timezone

Timeline display:
- Convert server dates to library timezone for display
- Add $timezone() and $toDisplayDate() helper functions

This ensures that a booking for "June 15" displays and saves as
June 15 regardless of the user's timezone offset.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Comment 58 Nick Clemens (kidclamp) 2026-02-10 12:48:51 UTC
Created attachment 192866 [details] [review]
Bug 37707: (follow-up) Refactor place_booking.js for maintainability

Extract utility functions and reduce code duplication:

- Add toInt(), startOfDay(), datesOverlap(), isDateInRange() helpers
- Simplify containsAny() using Set and Array.some()
- Refactor isItemAvailableForPeriod() to use new utilities
- Refactor getAvailableItemsOnDate() and isDateDisabledForSpecificItem()
- Extract submit handler helpers: getAvailableItemsForPeriod(),
  buildBookingPayload(), createTimelineItem(), showBookingError(),
  showBookingSuccess(), refreshBookingsTable(), setPickerDates()
- Replace parseInt() calls with toInt() for consistency
- Simplify modal reset handler with jQuery method chaining

No functional changes - prepares codebase for future Vue conversion.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Comment 59 Nick Clemens (kidclamp) 2026-02-10 12:48:55 UTC
Created attachment 192867 [details] [review]
Bug 37707: (follow-up) Add comprehensive Cypress tests for bookings modal

Reorganize and expand Cypress tests into three focused spec files:

bookingsModalBasic_spec.ts:
- Modal initialization and field interactions
- Form validation and submission
- Edit booking functionality
- 'Any item' booking with server-side item selection

bookingsModalDatePicker_spec.ts:
- Flatpickr initialization and constraints
- Date range validation
- Circulation rules (issue length, renewals)
- Lead/trail period conflict detection
- 'Any item' mode date availability
- Event dots for existing bookings

bookingsModalTimezone_spec.ts:
- Cross-timezone date display
- Multi-day booking spans
- Conflict detection with timezone awareness
- API round-trip date preservation
- Month boundary handling

Add shared test utilities to e2e.js for date helpers.

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Comment 60 Nick Clemens (kidclamp) 2026-02-10 12:48:59 UTC
Created attachment 192868 [details] [review]
Bug 37707: (QA follow-up) Fix typo in comment

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Comment 61 Nick Clemens (kidclamp) 2026-02-10 12:49:02 UTC
Created attachment 192869 [details] [review]
Bug 37707: (QA follow-up) Fix grammatical error

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Comment 62 Nick Clemens (kidclamp) 2026-02-10 12:49:06 UTC
Created attachment 192870 [details] [review]
Bug 37707: (QA follow-up) Fix incorrect bold date expectations in Cypress test

- The test for circulation rules date calculations did not account for
  the start date being bold, only the period endpoint dates
- place_booking.js explicitly adds the selected start date to boldDates
  (boldDates = [new Date(startDate)]), giving it the "title" class
- The verification loop checking for unexpected bold dates then failed
  when it encountered the start date, which was bold but not expected
- Add clearZoneStart to expectedBoldDates to match actual behavior

To test:
- yarn cypress run --spec t/cypress/integration/Circulation/bookingsModalDatePicker_spec.ts
- Verify that the "should handle circulation rules date calculations and visual feedback" test now passes

Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Comment 63 Lucas Gass (lukeg) 2026-02-10 15:53:05 UTC
Nice work everyone!

Pushed to main for 26.05