From 8e9c4ac8ea4b6f4450adf1df609aff31e19440d5 Mon Sep 17 00:00:00 2001 From: Martin Renvoize 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 = $("") .append( "" + - (patron.surname - ? escape_str(patron.surname) + ", " - : "") + - (patron.firstname - ? escape_str(patron.firstname) + " " - : "") + - (patron.cardnumber - ? " (" + escape_str(patron.cardnumber) + ")" - : "") + - "" + - (patron.date_of_birth - ? ' ' + - $get_age(patron.date_of_birth) + - " " + - __("years") + - "" - : "") + - (patron.library - ? ' ' + - escape_str(patron.library.name) + - "" - : "") + - "" + (patron.surname + ? escape_str(patron.surname) + ", " + : "") + + (patron.firstname + ? escape_str(patron.firstname) + " " + : "") + + (patron.cardnumber + ? " (" + escape_str(patron.cardnumber) + ")" + : "") + + "" + + (patron.date_of_birth + ? ' ' + + $get_age(patron.date_of_birth) + + " " + + __("years") + + "" + : "") + + (patron.library + ? ' ' + + escape_str(patron.library.name) + + "" + : "") + + "" ) .addClass(loggedInClass); return $patron; @@ -212,10 +212,10 @@ $("#placeBookingModal").on("show.bs.modal", function (e) { ) { let option = $( '" + pickup_location.library_id + + '">' + + pickup_location.name + + "" ); 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( + '
' + + __("No suitable item found for booking") + + "
" + ); + 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( '
' + - __("Booking successfully placed") + - "
" + __("Booking successfully placed") + + "" ); // Close modal @@ -1223,11 +1498,29 @@ $("#placeBookingForm").on("submit", function (e) { posting.fail(function (data) { $("#booking_result").replaceWith( '
' + - __("Failure") + - "
" + __("Failure") + + "" ); }); } 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( + '
' + + __("No suitable item found for booking") + + "
" + ); + 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( '
' + - __("Booking successfully updated") + - "
" + __("Booking successfully updated") + + "" ); // Close modal @@ -1292,8 +1585,8 @@ $("#placeBookingForm").on("submit", function (e) { putting.fail(function (data) { $("#booking_result").replaceWith( '
' + - __("Failure") + - "
" + __("Failure") + + "" ); }); } @@ -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