Bugzilla – Attachment 191746 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) Fix timezone handling for cross-timezone bookings
Bug-37707-follow-up-Fix-timezone-handling-for-cros.patch (text/plain), 11.34 KB, created by
Paul Derscheid
on 2026-01-21 09:09:10 UTC
(
hide
)
Description:
Bug 37707: (follow-up) Fix timezone handling for cross-timezone bookings
Filename:
MIME Type:
Creator:
Paul Derscheid
Created:
2026-01-21 09:09:10 UTC
Size:
11.34 KB
patch
obsolete
>From 923137c4113397bf1f85c684477e2e8a8153e4a3 Mon Sep 17 00:00:00 2001 >From: Paul Derscheid <paul.derscheid@lmscloud.de> >Date: Wed, 21 Jan 2026 09:40:46 +0100 >Subject: [PATCH] Bug 37707: (follow-up) Fix timezone handling for > cross-timezone bookings > >This patch fixes timezone issues that caused booking dates to shift >when the browser and server are in different timezones. > >The code used implicit timezone conversions via >.toISOString() and .toDate(), which work when browser and server >timezones match but fail otherwise. QA testing showed: >- PST browser: end date shifts +1 day >- NZDT browser: end date shifts +2 days > >Make timezone handling explicit by establishing that >booking dates are interpreted in the library's timezone. > >- Form submission: Extract date value (YYYY-MM-DD) and send as > explicit UTC day boundaries using dayjs.utc().startOf/endOf('day') >- Timeline display: Convert UTC to library timezone using $timezone() > and new $toDisplayDate() helper for consistent rendering >- Added $timezone() to expose server timezone to JavaScript >- Added $toDisplayDate() to create display-ready Date objects that > render correctly regardless of browser timezone > >To test: >1. Apply patch and run: yarn build >2. Configure a bookable item with 2-day lead and 3-day trail periods > >Timezone handling (PST): >3. Set browser timezone to PST (America/Los_Angeles), for example with >Chrome Devtools -> Ctrl/Cmd + Shift + P -> sensors -> Location >4. Create a single-day booking for Jan 20 >5. Verify the booking table shows start: Jan 20, end: Jan 20 > (not Jan 21) >6. Verify the timeline shows the booking on Jan 20 only >7. Create a 3-day booking (Jan 22-24) >8. Verify the table shows start: Jan 22, end: Jan 24 >9. Verify the timeline spans exactly 3 days > >Timezone handling (NZDT): >10. Set browser timezone to NZDT (Pacific/Auckland) >11. Repeat steps 4-9 >12. Verify dates do not shift by +2 days as previously reported > >Test lead/trail conflict detection: >13. With existing booking on Jan 20 (trail period: Jan 21-23) >14. Attempt to create new booking starting Jan 24 (lead: Jan 22-23) >15. Verify conflict is detected (lead overlaps with trail) >16. Verify booking starting Jan 26 (lead: Jan 24-25) is allowed > >Test multi-item filtering: >17. On a bib with multiple bookable items >18. Create booking on item A for Jan 20 >19. Open booking modal, select item B >20. Verify calendar does NOT show conflicts from item A's booking >--- > .../prog/en/includes/js-date-format.inc | 22 ++++++++++ > .../prog/en/modules/bookings/list.tt | 7 ++- > .../prog/js/modals/place_booking.js | 43 +++++++++++-------- > 3 files changed, 53 insertions(+), 19 deletions(-) > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc >index 2a95e218c1d..029fa91862d 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc >@@ -118,6 +118,28 @@ > } > } > >+ /** >+ * Returns the configured Koha server timezone. >+ * Useful for components that need to display dates/times in library timezone. >+ */ >+ window.$timezone = function() { >+ return def_tz; >+ } >+ >+ /** >+ * Converts a dayjs object to a Date for timeline/calendar display. >+ * Creates a Date that displays these exact values regardless of browser timezone. >+ * (Strips timezone info so JS interprets the values as browser-local time) >+ * >+ * Use this when you need to display a specific date/time from a known timezone >+ * (e.g., library timezone) and want it to appear the same for all users. >+ * >+ * @param {dayjs.Dayjs} dayjsObj - A dayjs object (typically converted to a specific timezone) >+ * @returns {Date} A Date object that will display the dayjs values in the browser >+ */ >+ window.$toDisplayDate = function(dayjsObj) { >+ return new Date(dayjsObj.format('YYYY-MM-DDTHH:mm:ss')); >+ } > > })(); > </script> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt >index 56b3da2954c..98b1bf86206 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt >@@ -106,13 +106,16 @@ > display_cardnumber: true, > url: false > }); >+ // Convert to library timezone for timeline display >+ const startServerTz = dayjs(booking.start_date).tz($timezone()); >+ const endServerTz = dayjs(booking.end_date).tz($timezone()); > bookingsSet.add({ > id: booking.booking_id, > booking: booking.booking_id, > patron: booking.patron_id, > pickup_library: booking.pickup_library_id, >- start: dayjs(booking.start_date).toDate(), >- end: dayjs(booking.end_date).toDate(), >+ start: $toDisplayDate(startServerTz), >+ end: $toDisplayDate(endServerTz), > content: !isActive ? `<s>${patronContent}</s>` : patronContent, > className: booking.status === "cancelled" ? "cancelled" : "", > [% IF CAN_user_circulate_manage_bookings %] >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 e1d19f2fc0d..c37896b4ce9 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >@@ -373,7 +373,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > end_date: checkout.due_date, > item_id: checkout.item_id, > patron_id: checkout.patron_id, >- start_date: new Date().toISOString(), >+ start_date: dayjs().format(), > }; > bookings.unshift(booking); > } >@@ -777,15 +777,16 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > // Range set, update hidden fields and set available items > else if (selectedDates[0] && selectedDates[1]) { > // set form fields from picker >- let picker_start = dayjs(selectedDates[0]); >- let picker_end = dayjs(selectedDates[1]).endOf( >- "day" >- ); >+ // Extract local date and send as explicit UTC day boundaries >+ // This preserves the user's selected DATE regardless of browser timezone >+ // Using dayjs.utc() ensures startOf/endOf operate in UTC, not browser TZ >+ let startDate = dayjs(selectedDates[0]).format("YYYY-MM-DD"); >+ let endDate = dayjs(selectedDates[1]).format("YYYY-MM-DD"); > $("#booking_start_date").val( >- picker_start.toISOString() >+ dayjs.utc(startDate).startOf("day").toISOString() > ); > $("#booking_end_date").val( >- picker_end.toISOString() >+ dayjs.utc(endDate).endOf("day").toISOString() > ); > > // set available items in select2 >@@ -871,9 +872,10 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > // Iterate through each date within the range of start_date and end_date > let currentDate = new Date(start_date); > while (currentDate <= end_date) { >- const currentDateStr = currentDate >- .toISOString() >- .split("T")[0]; >+ // Use dayjs to format date in local timezone (not UTC) >+ const currentDateStr = dayjs(currentDate).format( >+ "YYYY-MM-DD" >+ ); > > // If the date key doesn't exist in the hash, create an empty array for it > if (!bookingsByDate[currentDateStr]) { >@@ -896,9 +898,10 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > periodPicker.config.onDayCreate.push( > function dayCreate(dObj, dStr, instance, dayElem) { > const currentDate = dayElem.dateObj; >- const dateString = currentDate >- .toISOString() >- .split("T")[0]; >+ // Use dayjs to format date in local timezone (not UTC) >+ const dateString = dayjs(currentDate).format( >+ "YYYY-MM-DD" >+ ); > > const isBold = boldDates.some( > boldDate => >@@ -1409,12 +1412,15 @@ $("#placeBookingForm").on("submit", function (e) { > bookings_table.api().ajax.reload(); > } > if (typeof timeline !== "undefined" && timeline !== null) { >+ // Convert to library timezone for timeline display >+ const startServerTz = dayjs(data.start_date).tz($timezone()); >+ const endServerTz = dayjs(data.end_date).tz($timezone()); > timeline.itemsData.add({ > id: data.booking_id, > booking: data.booking_id, > patron: data.patron_id, >- start: dayjs(data.start_date).toDate(), >- end: dayjs(data.end_date).toDate(), >+ start: $toDisplayDate(startServerTz), >+ end: $toDisplayDate(endServerTz), > content: $patron_to_html(booking_patron, { > display_cardnumber: true, > url: false, >@@ -1483,12 +1489,15 @@ $("#placeBookingForm").on("submit", function (e) { > bookings_table.api().ajax.reload(); > } > if (typeof timeline !== "undefined" && timeline !== null) { >+ // Convert to library timezone for timeline display >+ const startServerTz = dayjs(data.start_date).tz($timezone()); >+ const endServerTz = dayjs(data.end_date).tz($timezone()); > timeline.itemsData.update({ > id: data.booking_id, > booking: data.booking_id, > patron: data.patron_id, >- start: dayjs(data.start_date).toDate(), >- end: dayjs(data.end_date).toDate(), >+ start: $toDisplayDate(startServerTz), >+ end: $toDisplayDate(endServerTz), > content: $patron_to_html(booking_patron, { > display_cardnumber: true, > url: false, >-- >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