Bugzilla – Attachment 183211 Details for
Bug 40134
Fix and optimise 'Any item' functionality of bookings
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 40134: Implement dynamic item assignment with smart window maximization
Bug-40134-Implement-dynamic-item-assignment-with-s.patch (text/plain), 45.99 KB, created by
Martin Renvoize (ashimema)
on 2025-06-12 14:54:44 UTC
(
hide
)
Description:
Bug 40134: Implement dynamic item assignment with smart window maximization
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2025-06-12 14:54:44 UTC
Size:
45.99 KB
patch
obsolete
>From 8e9c4ac8ea4b6f4450adf1df609aff31e19440d5 Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Date: Thu, 12 Jun 2025 13:20:09 +0100 >Subject: [PATCH] Bug 40134: Implement dynamic item assignment with smart > window maximization > >This patch implements smart window maximization for "any item of itemtype X" >bookings using dynamic item pool reduction. The core principle is "never re-add >items to pool" - once an item is removed from the available pool because it >becomes unavailable, it's never re-added even if it becomes available again >later. This ensures optimal resource allocation and maximum booking windows. > >Key features: >- Dynamic item pool reduction algorithm in isDateInMaximumWindow() >- Smart itemtype availability calculation in isDateDisabledForItemtype() >- Optimal item selection for booking submission in selectOptimalItem() >- Future availability calculation to choose best items >- Comprehensive JSDoc documentation with detailed examples > >The algorithm works by: >1. Starting with items available on the selected start date >2. Walking through each day from start to target end date >3. Removing items from pool when they become unavailable >4. Never re-adding items even if they become available again later >5. Disabling dates when no items remain in pool > >This maximizes booking windows while preventing conflicts by being conservative >about item availability. > >Test plan: >1. Apply patch and restart services >2. Navigate to a biblio detail page with bookable items of multiple types >3. Click "Place booking" and select a patron and pickup location >4. Select an itemtype (not a specific item) to enable "any item" mode >5. Select a start date - verify end date options are maximized based on > available items across the booking period >6. Test with complex booking scenarios where items have overlapping bookings >7. Verify that the algorithm correctly reduces the item pool as items become > unavailable and extends booking windows as far as possible >8. Confirm that items which become available again later in the period are > not re-added to the pool (key algorithm principle) >9. Submit booking and verify optimal item is selected automatically >--- > .../prog/js/modals/place_booking.js | 711 +++++++++++++----- > 1 file changed, 502 insertions(+), 209 deletions(-) > >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 bed0fe8388c..7d64e5bd99f 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >@@ -93,29 +93,29 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > let $patron = $("<span></span>") > .append( > "" + >- (patron.surname >- ? escape_str(patron.surname) + ", " >- : "") + >- (patron.firstname >- ? escape_str(patron.firstname) + " " >- : "") + >- (patron.cardnumber >- ? " (" + escape_str(patron.cardnumber) + ")" >- : "") + >- "<small>" + >- (patron.date_of_birth >- ? ' <span class="age_years">' + >- $get_age(patron.date_of_birth) + >- " " + >- __("years") + >- "</span>" >- : "") + >- (patron.library >- ? ' <span class="ac-library">' + >- escape_str(patron.library.name) + >- "</span>" >- : "") + >- "</small>" >+ (patron.surname >+ ? escape_str(patron.surname) + ", " >+ : "") + >+ (patron.firstname >+ ? escape_str(patron.firstname) + " " >+ : "") + >+ (patron.cardnumber >+ ? " (" + escape_str(patron.cardnumber) + ")" >+ : "") + >+ "<small>" + >+ (patron.date_of_birth >+ ? ' <span class="age_years">' + >+ $get_age(patron.date_of_birth) + >+ " " + >+ __("years") + >+ "</span>" >+ : "") + >+ (patron.library >+ ? ' <span class="ac-library">' + >+ escape_str(patron.library.name) + >+ "</span>" >+ : "") + >+ "</small>" > ) > .addClass(loggedInClass); > return $patron; >@@ -212,10 +212,10 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > ) { > let option = $( > '<option value="' + >- pickup_location.library_id + >- '">' + >- pickup_location.name + >- "</option>" >+ pickup_location.library_id + >+ '">' + >+ pickup_location.name + >+ "</option>" > ); > > option.attr( >@@ -414,8 +414,8 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > if ( > !$("#booking_itemtype").find( > "option[value='" + >- item.item_type.item_type_id + >- "']" >+ item.item_type.item_type_id + >+ "']" > ).length > ) { > // Create a DOM Option and de-select by default >@@ -440,15 +440,6 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > // set local copy of selectedDates > let selectedDates = periodPicker.selectedDates; > >- // set booked counter >- let booked = 0; >- >- // reset the unavailable items array >- let unavailable_items = []; >- >- // reset the biblio level bookings array >- let biblio_bookings = []; >- > // disable dates before selected date > if ( > !selectedDates[1] && >@@ -458,159 +449,423 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > return true; > } > >- // iterate existing bookings >- for (let booking of bookings) { >- // Skip if we're editing this booking >- if ( >- booking_id && >- booking_id == booking.booking_id >- ) { >- continue; >- } >+ // We should always have an itemtype selected and either specific item or "any item" >+ if (!booking_itemtype_id) { >+ return true; // No itemtype selected, disable everything >+ } > >- let start_date = flatpickr.parseDate( >- booking.start_date >+ // If "any item of itemtype" is selected, use smart window maximization >+ if (!booking_item_id) { >+ return isDateDisabledForItemtype( >+ date, >+ selectedDates > ); >- let end_date = flatpickr.parseDate( >- booking.end_date >+ } >+ // If specific item is selected, use item-specific logic >+ else { >+ return isDateDisabledForSpecificItem( >+ date, >+ selectedDates > ); >+ } >+ } >+ ); >+ } > >- // patron has selected a start date (end date checks) >- if (selectedDates[0]) { >- // new booking start date is between existing booking start and end dates >- if ( >- selectedDates[0] >= start_date && >- selectedDates[0] <= end_date >- ) { >- if (booking.item_id) { >- if ( >- unavailable_items.indexOf( >- booking.item_id >- ) === -1 >- ) { >- unavailable_items.push( >- booking.item_id >- ); >- } >- } else { >- if ( >- biblio_bookings.indexOf( >- booking.booking_id >- ) === -1 >- ) { >- biblio_bookings.push( >- booking.booking_id >- ); >- } >- } >- } >+ /** >+ * SMART ITEMTYPE AVAILABILITY CALCULATION >+ * For "any item of type X" bookings with dynamic item pool reduction >+ * >+ * ALGORITHM OVERVIEW: >+ * This function implements smart window maximization for itemtype bookings by using >+ * dynamic item pool reduction. The core principle is "never re-add items to pool" - >+ * once an item is removed because it becomes unavailable, it's never re-added even >+ * if it becomes available again later. This ensures optimal resource allocation. >+ * >+ * FLOW: >+ * 1. For start date selection: Disable if ALL items of itemtype are booked >+ * 2. For end date selection: Use smart window maximization algorithm >+ * >+ * SMART WINDOW MAXIMIZATION: >+ * - Start with items available on the selected start date >+ * - Walk through each day from start to target end date >+ * - Remove items from pool when they become unavailable >+ * - NEVER re-add items even if they become available again later >+ * - Disable date when no items remain in pool >+ * >+ * EXAMPLE: >+ * Items: A, B, C >+ * A available: days 1-5, booked 6-10, available again 11+ >+ * B available: days 1-8, booked 9-15, available again 16+ >+ * C available: days 1-12, booked 13-20, available again 21+ >+ * >+ * Start day 3: >+ * - Initial pool: A, B, C >+ * - Days 3-5: Pool A, B, C (all available) >+ * - Day 6: Remove A (becomes booked), Pool now B, C >+ * - Day 9: Remove B (becomes booked), Pool now C >+ * - Day 13: Remove C (becomes booked), Pool now EMPTY â disable dates >+ * - Result: Can book days 3-12, day 13+ disabled >+ * - Note: A becomes available on day 11 but is NOT re-added to pool >+ * >+ * @param {Date} date - The date being checked for availability >+ * @param {Array} selectedDates - Array of selected dates from flatpickr [startDate, endDate?] >+ * @returns {boolean} - True if date should be disabled, false if available >+ */ >+ function isDateDisabledForItemtype(date, selectedDates) { >+ // Get items of the selected itemtype >+ let itemsOfType = bookable_items.filter( >+ item => >+ item.effective_item_type_id === booking_itemtype_id >+ ); > >- // new booking end date would be between existing booking start and end dates >- else if ( >- date >= start_date && >- date <= end_date >- ) { >- if (booking.item_id) { >- if ( >- unavailable_items.indexOf( >- booking.item_id >- ) === -1 >- ) { >- unavailable_items.push( >- booking.item_id >- ); >- } >- } else { >- if ( >- biblio_bookings.indexOf( >- booking.booking_id >- ) === -1 >- ) { >- biblio_bookings.push( >- booking.booking_id >- ); >- } >- } >- } >+ // For start date selection: disable if ALL items of itemtype are booked on this date >+ if (!selectedDates[0]) { >+ return ( >+ getAvailableItemsOnDate(date, itemsOfType) >+ .length === 0 >+ ); >+ } > >- // new booking would span existing booking >- else if ( >- selectedDates[0] <= start_date && >- date >= end_date >- ) { >- if (booking.item_id) { >- if ( >- unavailable_items.indexOf( >- booking.item_id >- ) === -1 >- ) { >- unavailable_items.push( >- booking.item_id >- ); >- } >- } else { >- if ( >- biblio_bookings.indexOf( >- booking.booking_id >- ) === -1 >- ) { >- biblio_bookings.push( >- booking.booking_id >- ); >- } >- } >- } >+ // For end date selection: use smart window maximization >+ if (selectedDates[0] && !selectedDates[1]) { >+ let result = !isDateInMaximumWindow( >+ selectedDates[0], >+ date, >+ itemsOfType >+ ); >+ return result; >+ } > >- // new booking would not conflict >- else { >- continue; >- } >+ return false; >+ } > >- // check that there are available items >- // available = all bookable items - booked items - booked biblios >- let total_available = >- bookable_items.length - >- unavailable_items.length - >- biblio_bookings.length; >- if (total_available === 0) { >- return true; >- } >- } >+ /** >+ * MAXIMUM BOOKING WINDOW CALCULATION ALGORITHM >+ * Core Implementation of "Never Re-add Items to Pool" Principle >+ * >+ * PURPOSE: >+ * Calculate the maximum possible booking window for "any item of itemtype X" bookings >+ * by dynamically reducing the available item pool as items become unavailable. >+ * >+ * CORE ALGORITHM: "Never Re-add Items to Pool" >+ * 1. Start with items available on the selected start date ONLY >+ * 2. Walk through each day from start to target end date >+ * 3. Remove items from pool when they become unavailable (booking starts) >+ * 4. NEVER re-add items even if they become available again later (booking ends) >+ * 5. Return false (disable date) when no items remain in pool >+ * >+ * WHY THIS WORKS: >+ * - Maximizes booking windows by ensuring optimal resource allocation >+ * - Prevents booking conflicts by being conservative about item availability >+ * - Ensures that if a booking can start on date X, there will always be an >+ * item available for the entire duration (no conflicts) >+ * >+ * DETAILED EXAMPLE: >+ * Items: TABLET001, TABLET002, TABLET003 >+ * TABLET001: Available 1-9, Booked 10-15, Available 16+ >+ * TABLET002: Available 1-12, Booked 13-20, Available 21+ >+ * TABLET003: Available 1-17, Booked 18-25, Available 26+ >+ * >+ * Testing: Can we book from day 5 to day 20? >+ * >+ * Step 1: Day 5 (start) - Initial pool: {TABLET001, TABLET002, TABLET003} >+ * Step 2: Day 6-9 - All items available, pool unchanged >+ * Step 3: Day 10 - TABLET001 becomes unavailable â Remove from pool >+ * Pool now: {TABLET002, TABLET003} >+ * Step 4: Day 11-12 - Remaining items available, pool unchanged >+ * Step 5: Day 13 - TABLET002 becomes unavailable â Remove from pool >+ * Pool now: {TABLET003} >+ * Step 6: Day 14-17 - TABLET003 available, pool unchanged >+ * Step 7: Day 18 - TABLET003 becomes unavailable â Remove from pool >+ * Pool now: {} (empty) >+ * Step 8: Pool is empty â Return false (cannot book to day 20) >+ * >+ * Result: Can book from day 5 to day 17, but NOT to day 18+ >+ * >+ * CRITICAL NOTE: Even though TABLET001 becomes available again on day 16, >+ * it is NOT re-added to the pool. This is the key principle that ensures >+ * booking reliability and optimal resource allocation. >+ * >+ * PERFORMANCE: O(n à d) where n = items of type, d = days in range >+ * >+ * @param {Date} startDate - Selected start date from flatpickr >+ * @param {Date} endDate - Target end date being checked for availability >+ * @param {Array} itemsOfType - Items of the selected itemtype >+ * @returns {boolean} - True if date is within maximum window, false if beyond >+ */ >+ function isDateInMaximumWindow( >+ startDate, >+ endDate, >+ itemsOfType >+ ) { >+ // Start with only items available on the start date - never add items back >+ let availableOnStart = getAvailableItemsOnDate( >+ startDate, >+ itemsOfType >+ ); >+ let availableItems = new Set( >+ availableOnStart.map(item => parseInt(item.item_id, 10)) >+ ); > >- // patron has not yet selected a start date (start date checks) >- else if ( >- date <= end_date && >- date >= start_date >- ) { >- // same item, disable date >- if ( >- booking.item_id && >- booking.item_id == booking_item_id >- ) { >- return true; >- } >+ let currentDate = dayjs(startDate); > >- // count all clashes, both item and biblio level >- booked++; >- if (booked == bookable) { >- return true; >- } >+ // Walk through each day from start to end date >+ while (currentDate.isSameOrBefore(endDate, "day")) { >+ let availableToday = getAvailableItemsOnDate( >+ currentDate, >+ itemsOfType >+ ); >+ let availableIds = new Set( >+ availableToday.map(item => >+ parseInt(item.item_id, 10) >+ ) >+ ); > >- // FIXME: The above is not intelligent enough to spot >- // cases where an item must be used for a biblio level booking >- // due to all other items being booking within the biblio level >- // booking period... we end up with a clash >- // To reproduce: >- // * One bib with two bookable items. >- // * Add item level booking >- // * Add biblio level booking that extends one day beyond the item level booking >- // * Try to book the item without an item level booking from the day before the biblio level >- // booking is to be returned. Note this is a clash, the only item available for the biblio >- // level booking is the item you just booked out overlapping the end date. >- } >+ // Remove items from our pool that are no longer available (never add back) >+ // Only remove items that are unavailable today, don't re-add previously removed items >+ let itemsToRemove = []; >+ for (let itemId of availableItems) { >+ if (!availableIds.has(itemId)) { >+ itemsToRemove.push(itemId); > } > } >+ itemsToRemove.forEach(itemId => >+ availableItems.delete(itemId) >+ ); >+ >+ // If no items left in the pool, this date is beyond the maximum window >+ if (availableItems.size === 0) { >+ return false; >+ } >+ >+ // Move to next day >+ currentDate = currentDate.add(1, "day"); >+ } >+ >+ return true; // Date is within the maximum window >+ } >+ >+ // Get items of itemtype that are available on a specific date >+ function getAvailableItemsOnDate(date, itemsOfType) { >+ let unavailableItems = new Set(); >+ >+ // Check all existing bookings for conflicts on this date >+ for (let booking of bookings) { >+ // Skip if we're editing this booking >+ if (booking_id && booking_id == booking.booking_id) { >+ continue; >+ } >+ >+ let start_date = dayjs(booking.start_date); >+ let end_date = dayjs(booking.end_date); >+ let checkDate = dayjs(date); >+ >+ // Check if this date falls within this booking period >+ if ( >+ checkDate.isSameOrAfter(start_date, "day") && >+ checkDate.isSameOrBefore(end_date, "day") >+ ) { >+ // All bookings have item_id, so mark this specific item as unavailable >+ // Ensure integer comparison consistency >+ unavailableItems.add(parseInt(booking.item_id, 10)); >+ } >+ } >+ >+ // Return items of our type that are not unavailable >+ let available = itemsOfType.filter( >+ item => >+ !unavailableItems.has(parseInt(item.item_id, 10)) > ); >+ return available; >+ } >+ >+ // Item-specific availability logic for specific item bookings >+ function isDateDisabledForSpecificItem(date, selectedDates) { >+ for (let booking of bookings) { >+ // Skip if we're editing this booking >+ if (booking_id && booking_id == booking.booking_id) { >+ continue; >+ } >+ >+ let start_date = dayjs(booking.start_date); >+ let end_date = dayjs(booking.end_date); >+ let checkDate = dayjs(date); >+ >+ // Check if this booking conflicts with our selected item and date >+ if ( >+ checkDate.isSameOrAfter(start_date, "day") && >+ checkDate.isSameOrBefore(end_date, "day") >+ ) { >+ // Same item, disable date (ensure integer comparison) >+ if ( >+ parseInt(booking.item_id, 10) === >+ parseInt(booking_item_id, 10) >+ ) { >+ return true; >+ } >+ } >+ } >+ return false; >+ } >+ >+ /** >+ * OPTIMAL ITEM SELECTION FOR "ANY ITEM" BOOKINGS >+ * >+ * PURPOSE: >+ * When submitting an "any item of itemtype X" booking, select the specific item >+ * that provides the best future availability for subsequent bookings. >+ * >+ * SELECTION CRITERIA: >+ * 1. Item must be available for the entire requested booking period >+ * 2. Among available items, choose the one with longest future availability >+ * 3. This maximizes opportunities for future bookings by preserving >+ * items with shorter future availability for those bookings >+ * >+ * ALGORITHM: >+ * 1. Filter items of the specified type >+ * 2. Find items available for the entire booking period >+ * 3. For each available item, calculate future availability after booking ends >+ * 4. Select item with longest future availability >+ * 5. If tie, select first item (arbitrary but consistent) >+ * >+ * EXAMPLE: >+ * Booking period: Days 5-10 >+ * TABLET001: Available 5-10, then booked 11-15, then free 16+ >+ * TABLET002: Available 5-10, then free until 25, then booked 26+ >+ * TABLET003: Available 5-10, then booked 11-30 >+ * >+ * Future availability calculations (after day 10): >+ * TABLET001: 0 days (booked immediately after) >+ * TABLET002: 15 days (free until day 25) >+ * TABLET003: 0 days (booked immediately after) >+ * >+ * Result: Select TABLET002 (longest future availability) >+ * Benefit: Preserves TABLET001 for bookings that might need days 11-15 >+ * >+ * @param {string} itemTypeId - The itemtype ID to select from >+ * @param {Date} startDate - Booking start date >+ * @param {Date} endDate - Booking end date >+ * @returns {string|null} - Selected item ID, or null if none available >+ */ >+ function selectOptimalItem(itemTypeId, startDate, endDate) { >+ // Get items of the selected itemtype >+ let itemsOfType = bookable_items.filter( >+ item => item.effective_item_type_id === itemTypeId >+ ); >+ >+ // Find items available for the entire booking period >+ let availableItems = itemsOfType.filter(item => { >+ return isItemAvailableForPeriod( >+ item.item_id, >+ startDate, >+ endDate >+ ); >+ }); >+ >+ if (availableItems.length === 0) { >+ return null; // No items available >+ } >+ >+ if (availableItems.length === 1) { >+ return availableItems[0].item_id; // Only one choice >+ } >+ >+ // Calculate future availability for each item and choose the best one >+ let bestItem = null; >+ let longestFutureAvailability = -1; >+ >+ for (let item of availableItems) { >+ let futureAvailability = calculateFutureAvailability( >+ item.item_id, >+ endDate >+ ); >+ >+ if (futureAvailability > longestFutureAvailability) { >+ longestFutureAvailability = futureAvailability; >+ bestItem = item; >+ } >+ } >+ >+ return bestItem >+ ? bestItem.item_id >+ : availableItems[0].item_id; >+ } >+ >+ // Check if a specific item is available for the entire booking period >+ function isItemAvailableForPeriod(itemId, startDate, endDate) { >+ for (let booking of bookings) { >+ // Skip if we're editing this booking >+ if (booking_id && booking_id == booking.booking_id) { >+ continue; >+ } >+ >+ if (booking.item_id !== itemId) { >+ continue; // Different item, no conflict >+ } >+ >+ let booking_start = dayjs(booking.start_date); >+ let booking_end = dayjs(booking.end_date); >+ let checkStartDate = dayjs(startDate); >+ let checkEndDate = dayjs(endDate); >+ >+ // Check for any overlap with our booking period >+ if ( >+ !( >+ checkEndDate.isBefore(booking_start, "day") || >+ checkStartDate.isAfter(booking_end, "day") >+ ) >+ ) { >+ return false; // Overlap detected >+ } >+ } >+ return true; // No conflicts found >+ } >+ >+ // Calculate how many days an item is available after the booking end date >+ function calculateFutureAvailability(itemId, bookingEndDate) { >+ let daysAvailable = 0; >+ let checkDate = dayjs(bookingEndDate).add(1, "day"); // Start day after booking ends >+ >+ // Check availability for next 365 days (or until we hit a booking) >+ for (let i = 0; i < 365; i++) { >+ let isAvailable = true; >+ >+ for (let booking of bookings) { >+ // Skip if we're editing this booking >+ if ( >+ booking_id && >+ booking_id == booking.booking_id >+ ) { >+ continue; >+ } >+ >+ if (booking.item_id !== itemId) { >+ continue; // Different item >+ } >+ >+ let booking_start = dayjs(booking.start_date); >+ let booking_end = dayjs(booking.end_date); >+ >+ // Check if this date is within an existing booking >+ if ( >+ checkDate.isSameOrAfter(booking_start, "day") && >+ checkDate.isSameOrBefore(booking_end, "day") >+ ) { >+ isAvailable = false; >+ break; >+ } >+ } >+ >+ if (!isAvailable) { >+ break; // Hit a booking, stop counting >+ } >+ >+ daysAvailable++; >+ checkDate = checkDate.add(1, "day"); >+ } >+ >+ return daysAvailable; > } > > // Setup listener for itemtype select2 >@@ -649,9 +904,11 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > > // Setup listener for item select2 > $("#booking_item_id").on("select2:select", function (e) { >- booking_item_id = e.params.data.id >- ? e.params.data.id >- : null; >+ booking_item_id = >+ e.params.data.id !== undefined && >+ e.params.data.id !== null >+ ? parseInt(e.params.data.id, 10) >+ : 0; > > // Disable invalid pickup locations > $("#pickup_library_id > option").each(function () { >@@ -665,7 +922,9 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > .split(",") > .map(Number); > if ( >- valid_items.includes(parseInt(booking_item_id)) >+ valid_items.includes( >+ parseInt(booking_item_id, 10) >+ ) > ) { > option.prop("disabled", false); > } else { >@@ -753,7 +1012,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > for (let i = 0; i < renewalsAllowed; i++) { > nextDate.setDate( > nextDate.getDate() + >- parseInt(renewalLength) >+ parseInt(renewalLength) > ); > boldDates.push(new Date(nextDate)); > } >@@ -864,16 +1123,14 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > let bookingsByDate = {}; > // Iterate through the bookings array > bookings.forEach(booking => { >- const start_date = flatpickr.parseDate(booking.start_date); >- const end_date = flatpickr.parseDate(booking.end_date); >+ const start_date = dayjs(booking.start_date); >+ const end_date = dayjs(booking.end_date); > const item_id = booking.item_id; > > // 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]; >+ let currentDate = dayjs(start_date); >+ while (currentDate.isSameOrBefore(end_date, "day")) { >+ const currentDateStr = currentDate.format("YYYY-MM-DD"); > > // If the date key doesn't exist in the hash, create an empty array for it > if (!bookingsByDate[currentDateStr]) { >@@ -884,7 +1141,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > bookingsByDate[currentDateStr].push(item_id); > > // Move to the next day >- currentDate.setDate(currentDate.getDate() + 1); >+ currentDate = currentDate.add(1, "day"); > } > }); > >@@ -933,8 +1190,8 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > ); > const startDate = periodPicker.selectedDates[0] > ? dayjs(periodPicker.selectedDates[0]).startOf( >- "day" >- ) >+ "day" >+ ) > : null; > > const leadStart = startDate >@@ -960,7 +1217,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > dayElem.classList.toggle( > "leadRange", > elemDate.isSameOrAfter(leadStart) && >- elemDate.isBefore(leadEnd) >+ elemDate.isBefore(leadEnd) > ); > dayElem.classList.toggle( > "leadRangeEnd", >@@ -973,7 +1230,7 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { > dayElem.classList.toggle( > "trailRange", > elemDate.isAfter(trailStart) && >- elemDate.isSameOrBefore(trailEnd) >+ elemDate.isSameOrBefore(trailEnd) > ); > dayElem.classList.toggle( > "trailRangeEnd", >@@ -1116,7 +1373,7 @@ function setFormValues( > // If passed an itemnumber, pre-select > if (booking_item_id) { > // Wait a bit for the item options to be fully created with data attributes >- setTimeout(function() { >+ setTimeout(function () { > $("#booking_item_id").val(booking_item_id).trigger("change"); > // Also trigger the select2:select event with proper data > let selectedOption = $("#booking_item_id option:selected")[0]; >@@ -1126,9 +1383,9 @@ function setFormValues( > params: { > data: { > id: booking_item_id, >- element: selectedOption >- } >- } >+ element: selectedOption, >+ }, >+ }, > }); > } > }, 100); >@@ -1162,6 +1419,24 @@ $("#placeBookingForm").on("submit", function (e) { > let biblio_id = $("#booking_biblio_id").val(); > let item_id = $("#booking_item_id").val(); > >+ // If "any item" is selected, find the optimal item >+ if (item_id == 0) { >+ item_id = selectOptimalItem( >+ booking_itemtype_id, >+ new Date(start_date), >+ new Date(end_date) >+ ); >+ >+ if (!item_id) { >+ $("#booking_result").replaceWith( >+ '<div id="booking_result" class="alert alert-danger">' + >+ __("No suitable item found for booking") + >+ "</div>" >+ ); >+ return; >+ } >+ } >+ > if (!booking_id) { > let posting = $.post( > url, >@@ -1170,7 +1445,7 @@ $("#placeBookingForm").on("submit", function (e) { > end_date: end_date, > pickup_library_id: pickup_library_id, > biblio_id: biblio_id, >- item_id: item_id != 0 ? item_id : null, >+ item_id: item_id, > patron_id: $("#booking_patron_id").find(":selected").val(), > }) > ); >@@ -1212,8 +1487,8 @@ $("#placeBookingForm").on("submit", function (e) { > // Set feedback > $("#transient_result").replaceWith( > '<div id="transient_result" class="alert alert-info">' + >- __("Booking successfully placed") + >- "</div>" >+ __("Booking successfully placed") + >+ "</div>" > ); > > // Close modal >@@ -1223,11 +1498,29 @@ $("#placeBookingForm").on("submit", function (e) { > posting.fail(function (data) { > $("#booking_result").replaceWith( > '<div id="booking_result" class="alert alert-danger">' + >- __("Failure") + >- "</div>" >+ __("Failure") + >+ "</div>" > ); > }); > } else { >+ // For edits, also handle optimal item selection >+ if (item_id == 0) { >+ item_id = selectOptimalItem( >+ booking_itemtype_id, >+ new Date(start_date), >+ new Date(end_date) >+ ); >+ >+ if (!item_id) { >+ $("#booking_result").replaceWith( >+ '<div id="booking_result" class="alert alert-danger">' + >+ __("No suitable item found for booking") + >+ "</div>" >+ ); >+ return; >+ } >+ } >+ > url += "/" + booking_id; > let putting = $.ajax({ > method: "PUT", >@@ -1239,7 +1532,7 @@ $("#placeBookingForm").on("submit", function (e) { > end_date: end_date, > pickup_library_id: pickup_library_id, > biblio_id: biblio_id, >- item_id: item_id != 0 ? item_id : null, >+ item_id: item_id, > patron_id: $("#booking_patron_id").find(":selected").val(), > }), > }); >@@ -1281,8 +1574,8 @@ $("#placeBookingForm").on("submit", function (e) { > // Set feedback > $("#transient_result").replaceWith( > '<div id="transient_result" class="alert alert-info">' + >- __("Booking successfully updated") + >- "</div>" >+ __("Booking successfully updated") + >+ "</div>" > ); > > // Close modal >@@ -1292,8 +1585,8 @@ $("#placeBookingForm").on("submit", function (e) { > putting.fail(function (data) { > $("#booking_result").replaceWith( > '<div id="booking_result" class="alert alert-danger">' + >- __("Failure") + >- "</div>" >+ __("Failure") + >+ "</div>" > ); > }); > } >@@ -1307,7 +1600,7 @@ $("#placeBookingModal").on("hidden.bs.modal", function (e) { > booking_patron = undefined; > > // Reset item select >- $("#booking_item_id").val(0).trigger("change"); >+ $("#booking_item_id").val(parseInt(0)).trigger("change"); > $("#booking_item_id").prop("disabled", true); > > // Reset itemtype select >-- >2.49.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 40134
: 183211