View | Details | Raw Unified | Return to bug 37707
Collapse All | Expand All

(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js (-361 / +326 lines)
Lines 7-57 let bookable_items, Link Here
7
    booking_patron,
7
    booking_patron,
8
    booking_itemtype_id;
8
    booking_itemtype_id;
9
9
10
function containsAny(integers1, integers2) {
10
// ============================================================================
11
    // Create a hash set to store integers from the second array
11
// UTILITY FUNCTIONS
12
    let integerSet = {};
12
// ============================================================================
13
    for (let i = 0; i < integers2.length; i++) {
13
14
        integerSet[integers2[i]] = true;
14
/**
15
    }
15
 * Check if two arrays share any common elements
16
 * @param {Array} arr1 - First array of values
17
 * @param {Array} arr2 - Second array of values
18
 * @returns {boolean} - True if any element exists in both arrays
19
 */
20
function containsAny(arr1, arr2) {
21
    const set = new Set(arr2);
22
    return arr1.some(item => set.has(item));
23
}
16
24
17
    // Check if any integer from the first array exists in the hash set
25
/**
18
    for (let i = 0; i < integers1.length; i++) {
26
 * Parse a value to integer, with fallback to 0
19
        if (integerSet[integers1[i]]) {
27
 * @param {*} value - Value to parse
20
            return true; // Found a match, return true
28
 * @returns {number} - Parsed integer or 0
21
        }
29
 */
22
    }
30
function toInt(value) {
31
    const parsed = parseInt(value, 10);
32
    return isNaN(parsed) ? 0 : parsed;
33
}
34
35
/**
36
 * Normalize a date to start of day using dayjs
37
 * @param {Date|string|dayjs} date - Date to normalize
38
 * @returns {dayjs} - dayjs object at start of day
39
 */
40
function startOfDay(date) {
41
    return dayjs(date).startOf("day");
42
}
23
43
24
    return false; // No match found
44
/**
45
 * Check if two date ranges overlap
46
 * @param {Date|dayjs} start1 - Start of first range
47
 * @param {Date|dayjs} end1 - End of first range
48
 * @param {Date|dayjs} start2 - Start of second range
49
 * @param {Date|dayjs} end2 - End of second range
50
 * @returns {boolean} - True if ranges overlap
51
 */
52
function datesOverlap(start1, end1, start2, end2) {
53
    const s1 = startOfDay(start1);
54
    const e1 = startOfDay(end1);
55
    const s2 = startOfDay(start2);
56
    const e2 = startOfDay(end2);
57
    // Ranges overlap if neither is completely before or after the other
58
    return !(e1.isBefore(s2, "day") || s1.isAfter(e2, "day"));
25
}
59
}
26
60
27
// Check if a specific item is available for the entire booking period
61
/**
62
 * Check if a date falls within a date range (inclusive)
63
 * @param {Date|dayjs} date - Date to check
64
 * @param {Date|dayjs} start - Start of range
65
 * @param {Date|dayjs} end - End of range
66
 * @returns {boolean} - True if date is within range
67
 */
68
function isDateInRange(date, start, end) {
69
    const d = startOfDay(date);
70
    const s = startOfDay(start);
71
    const e = startOfDay(end);
72
    return d.isSameOrAfter(s, "day") && d.isSameOrBefore(e, "day");
73
}
74
75
/**
76
 * Check if a specific item is available for the entire booking period
77
 * @param {number|string} itemId - Item ID to check
78
 * @param {Date} startDate - Start of booking period
79
 * @param {Date} endDate - End of booking period
80
 * @returns {boolean} - True if item is available for the entire period
81
 */
28
function isItemAvailableForPeriod(itemId, startDate, endDate) {
82
function isItemAvailableForPeriod(itemId, startDate, endDate) {
29
    for (let booking of bookings) {
83
    const checkItemId = toInt(itemId);
84
    for (const booking of bookings) {
30
        // Skip if we're editing this booking
85
        // Skip if we're editing this booking
31
        if (booking_id && booking_id == booking.booking_id) {
86
        if (booking_id && booking_id == booking.booking_id) {
32
            continue;
87
            continue;
33
        }
88
        }
34
89
        // Skip different items
35
        if (booking.item_id !== itemId) {
90
        if (toInt(booking.item_id) !== checkItemId) {
36
            continue; // Different item, no conflict
91
            continue;
37
        }
92
        }
38
93
        // Check for overlap
39
        let booking_start = dayjs(booking.start_date);
40
        let booking_end = dayjs(booking.end_date);
41
        let checkStartDate = dayjs(startDate);
42
        let checkEndDate = dayjs(endDate);
43
44
        // Check for any overlap with our booking period
45
        if (
94
        if (
46
            !(
95
            datesOverlap(
47
                checkEndDate.isBefore(booking_start, "day") ||
96
                startDate,
48
                checkStartDate.isAfter(booking_end, "day")
97
                endDate,
98
                booking.start_date,
99
                booking.end_date
49
            )
100
            )
50
        ) {
101
        ) {
51
            return false; // Overlap detected
102
            return false;
52
        }
103
        }
53
    }
104
    }
54
    return true; // No conflicts found
105
    return true;
55
}
106
}
56
107
57
$("#placeBookingModal").on("show.bs.modal", function (e) {
108
$("#placeBookingModal").on("show.bs.modal", function (e) {
Lines 635-641 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
635
                        itemsOfType
686
                        itemsOfType
636
                    );
687
                    );
637
                    let availableItems = new Set(
688
                    let availableItems = new Set(
638
                        availableOnStart.map(item => parseInt(item.item_id, 10))
689
                        availableOnStart.map(item => toInt(item.item_id))
639
                    );
690
                    );
640
691
641
                    let currentDate = dayjs(startDate);
692
                    let currentDate = dayjs(startDate);
Lines 647-655 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
647
                            itemsOfType
698
                            itemsOfType
648
                        );
699
                        );
649
                        let availableIds = new Set(
700
                        let availableIds = new Set(
650
                            availableToday.map(item =>
701
                            availableToday.map(item => toInt(item.item_id))
651
                                parseInt(item.item_id, 10)
652
                            )
653
                        );
702
                        );
654
703
655
                        // Remove items from our pool that are no longer available (never add back)
704
                        // Remove items from our pool that are no longer available (never add back)
Lines 678-739 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
678
727
679
                // Get items of itemtype that are available on a specific date
728
                // Get items of itemtype that are available on a specific date
680
                function getAvailableItemsOnDate(date, itemsOfType) {
729
                function getAvailableItemsOnDate(date, itemsOfType) {
681
                    let unavailableItems = new Set();
730
                    const unavailableItems = new Set();
682
731
683
                    // Check all existing bookings for conflicts on this date
732
                    for (const booking of bookings) {
684
                    for (let booking of bookings) {
685
                        // Skip if we're editing this booking
733
                        // Skip if we're editing this booking
686
                        if (booking_id && booking_id == booking.booking_id) {
734
                        if (booking_id && booking_id == booking.booking_id) {
687
                            continue;
735
                            continue;
688
                        }
736
                        }
689
690
                        let start_date = dayjs(booking.start_date);
691
                        let end_date = dayjs(booking.end_date);
692
                        let checkDate = dayjs(date);
693
694
                        // Check if this date falls within this booking period
737
                        // Check if this date falls within this booking period
695
                        if (
738
                        if (
696
                            checkDate.isSameOrAfter(start_date, "day") &&
739
                            isDateInRange(
697
                            checkDate.isSameOrBefore(end_date, "day")
740
                                date,
741
                                booking.start_date,
742
                                booking.end_date
743
                            )
698
                        ) {
744
                        ) {
699
                            // All bookings have item_id, so mark this specific item as unavailable
745
                            unavailableItems.add(toInt(booking.item_id));
700
                            // Ensure integer comparison consistency
701
                            unavailableItems.add(parseInt(booking.item_id, 10));
702
                        }
746
                        }
703
                    }
747
                    }
704
748
705
                    // Return items of our type that are not unavailable
749
                    return itemsOfType.filter(
706
                    let available = itemsOfType.filter(
750
                        item => !unavailableItems.has(toInt(item.item_id))
707
                        item =>
708
                            !unavailableItems.has(parseInt(item.item_id, 10))
709
                    );
751
                    );
710
                    return available;
711
                }
752
                }
712
753
713
                // Item-specific availability logic for specific item bookings
754
                // Item-specific availability logic for specific item bookings
714
                function isDateDisabledForSpecificItem(date, selectedDates) {
755
                function isDateDisabledForSpecificItem(date, selectedDates) {
715
                    for (let booking of bookings) {
756
                    const selectedItemId = toInt(booking_item_id);
757
                    for (const booking of bookings) {
716
                        // Skip if we're editing this booking
758
                        // Skip if we're editing this booking
717
                        if (booking_id && booking_id == booking.booking_id) {
759
                        if (booking_id && booking_id == booking.booking_id) {
718
                            continue;
760
                            continue;
719
                        }
761
                        }
720
762
                        // Check if date is within booking period and same item
721
                        let start_date = dayjs(booking.start_date);
722
                        let end_date = dayjs(booking.end_date);
723
                        let checkDate = dayjs(date);
724
725
                        // Check if this booking conflicts with our selected item and date
726
                        if (
763
                        if (
727
                            checkDate.isSameOrAfter(start_date, "day") &&
764
                            isDateInRange(
728
                            checkDate.isSameOrBefore(end_date, "day")
765
                                date,
766
                                booking.start_date,
767
                                booking.end_date
768
                            ) &&
769
                            toInt(booking.item_id) === selectedItemId
729
                        ) {
770
                        ) {
730
                            // Same item, disable date (ensure integer comparison)
771
                            return true;
731
                            if (
732
                                parseInt(booking.item_id, 10) ===
733
                                parseInt(booking_item_id, 10)
734
                            ) {
735
                                return true;
736
                            }
737
                        }
772
                        }
738
                    }
773
                    }
739
                    return false;
774
                    return false;
Lines 778-806 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
778
                    booking_item_id =
813
                    booking_item_id =
779
                        e.params.data.id !== undefined &&
814
                        e.params.data.id !== undefined &&
780
                        e.params.data.id !== null
815
                        e.params.data.id !== null
781
                            ? parseInt(e.params.data.id, 10)
816
                            ? toInt(e.params.data.id)
782
                            : 0;
817
                            : 0;
783
818
784
                    // Disable invalid pickup locations
819
                    // Disable invalid pickup locations
785
                    $("#pickup_library_id > option").each(function () {
820
                    $("#pickup_library_id > option").each(function () {
786
                        let option = $(this);
821
                        const option = $(this);
787
                        if (booking_item_id == 0) {
822
                        if (booking_item_id == 0) {
788
                            option.prop("disabled", false);
823
                            option.prop("disabled", false);
789
                        } else {
824
                        } else {
790
                            let valid_items = String(
825
                            const valid_items = String(
791
                                option.data("pickup_items")
826
                                option.data("pickup_items")
792
                            )
827
                            )
793
                                .split(",")
828
                                .split(",")
794
                                .map(Number);
829
                                .map(Number);
795
                            if (
830
                            option.prop(
796
                                valid_items.includes(
831
                                "disabled",
797
                                    parseInt(booking_item_id, 10)
832
                                !valid_items.includes(toInt(booking_item_id))
798
                                )
833
                            );
799
                            ) {
800
                                option.prop("disabled", false);
801
                            } else {
802
                                option.prop("disabled", true);
803
                            }
804
                        }
834
                        }
805
                    });
835
                    });
806
                    $("#pickup_library_id").trigger("change.select2");
836
                    $("#pickup_library_id").trigger("change.select2");
Lines 1047-1054 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1047
1077
1048
                    // Iterate through each date within the range of start_date and end_date
1078
                    // Iterate through each date within the range of start_date and end_date
1049
                    // Use dayjs to maintain browser timezone consistency
1079
                    // Use dayjs to maintain browser timezone consistency
1050
                    let currentDate = dayjs(start_date).startOf("day");
1080
                    let currentDate = startOfDay(start_date);
1051
                    const endDate = dayjs(end_date).startOf("day");
1081
                    const endDate = startOfDay(end_date);
1052
                    while (currentDate.isSameOrBefore(endDate, "day")) {
1082
                    while (currentDate.isSameOrBefore(endDate, "day")) {
1053
                        // Format in browser timezone - no UTC conversion
1083
                        // Format in browser timezone - no UTC conversion
1054
                        const currentDateStr = currentDate.format("YYYY-MM-DD");
1084
                        const currentDateStr = currentDate.format("YYYY-MM-DD");
Lines 1184-1206 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1184
                            const itemConflicts = new Map();
1214
                            const itemConflicts = new Map();
1185
                            if (isAnyItemMode) {
1215
                            if (isAnyItemMode) {
1186
                                itemsOfSelectedType.forEach(item => {
1216
                                itemsOfSelectedType.forEach(item => {
1187
                                    itemConflicts.set(
1217
                                    itemConflicts.set(toInt(item.item_id), {
1188
                                        parseInt(item.item_id, 10),
1218
                                        leadConflict: false,
1189
                                        {
1219
                                        trailConflict: false,
1190
                                            leadConflict: false,
1220
                                        leadReason: {
1191
                                            trailConflict: false,
1221
                                            withTrail: false,
1192
                                            leadReason: {
1222
                                            withLead: false,
1193
                                                withTrail: false,
1223
                                            withBooking: false,
1194
                                                withLead: false,
1224
                                        },
1195
                                                withBooking: false,
1225
                                        trailReason: {
1196
                                            },
1226
                                            withTrail: false,
1197
                                            trailReason: {
1227
                                            withLead: false,
1198
                                                withTrail: false,
1228
                                            withBooking: false,
1199
                                                withLead: false,
1229
                                        },
1200
                                                withBooking: false,
1230
                                    });
1201
                                            },
1202
                                        }
1203
                                    );
1204
                                });
1231
                                });
1205
                            }
1232
                            }
1206
1233
Lines 1213-1230 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1213
                                    return;
1240
                                    return;
1214
                                }
1241
                                }
1215
1242
1216
                                const bookingItemId = parseInt(
1243
                                const bookingItemId = toInt(booking.item_id);
1217
                                    booking.item_id,
1218
                                    10
1219
                                );
1220
1244
1221
                                // For specific item mode: skip bookings for different items
1245
                                // For specific item mode: skip bookings for different items
1222
                                if (!isAnyItemMode) {
1246
                                if (!isAnyItemMode) {
1223
                                    if (
1247
                                    if (
1224
                                        booking.item_id &&
1248
                                        booking.item_id &&
1225
                                        booking_item_id &&
1249
                                        booking_item_id &&
1226
                                        bookingItemId !==
1250
                                        bookingItemId !== toInt(booking_item_id)
1227
                                            parseInt(booking_item_id, 10)
1228
                                    ) {
1251
                                    ) {
1229
                                        return;
1252
                                        return;
1230
                                    }
1253
                                    }
Lines 1235-1246 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1235
                                    }
1258
                                    }
1236
                                }
1259
                                }
1237
1260
1238
                                const bookingStart = dayjs(
1261
                                const bookingStart = startOfDay(
1239
                                    booking.start_date
1262
                                    booking.start_date
1240
                                ).startOf("day");
1263
                                );
1241
                                const bookingEnd = dayjs(
1264
                                const bookingEnd = startOfDay(booking.end_date);
1242
                                    booking.end_date
1243
                                ).startOf("day");
1244
1265
1245
                                // BIDIRECTIONAL: Mathematical checks for conflicts (works across month boundaries)
1266
                                // BIDIRECTIONAL: Mathematical checks for conflicts (works across month boundaries)
1246
                                // Calculate this booking's full protected period
1267
                                // Calculate this booking's full protected period
Lines 2128-2133 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
2128
    }
2149
    }
2129
});
2150
});
2130
2151
2152
/**
2153
 * Set date range on the period picker
2154
 * @param {Object} periodPicker - Flatpickr instance
2155
 * @param {string} start_date - Start date string
2156
 * @param {string} end_date - End date string
2157
 */
2158
function setPickerDates(periodPicker, start_date, end_date) {
2159
    if (start_date && end_date) {
2160
        periodPicker.setDate([new Date(start_date), new Date(end_date)], true);
2161
    }
2162
}
2163
2131
function setFormValues(
2164
function setFormValues(
2132
    patron_id,
2165
    patron_id,
2133
    booking_item_id,
2166
    booking_item_id,
Lines 2140-2172 function setFormValues( Link Here
2140
    if (item_type_id) {
2173
    if (item_type_id) {
2141
        booking_itemtype_id = item_type_id;
2174
        booking_itemtype_id = item_type_id;
2142
    }
2175
    }
2176
2143
    // If passed patron, pre-select
2177
    // If passed patron, pre-select
2144
    if (patron_id) {
2178
    if (patron_id) {
2145
        let patronSelect = $("#booking_patron_id");
2179
        const patronSelect = $("#booking_patron_id");
2146
        let patron = $.ajax({
2180
        $.ajax({
2147
            url: "/api/v1/patrons/" + patron_id,
2181
            url: "/api/v1/patrons/" + patron_id,
2148
            dataType: "json",
2182
            dataType: "json",
2149
            type: "GET",
2183
            type: "GET",
2150
        });
2184
        }).done(function (patron) {
2151
2152
        $.when(patron).done(function (patron) {
2153
            // clone patron_id to id (select2 expects an id field)
2154
            patron.id = patron.patron_id;
2185
            patron.id = patron.patron_id;
2155
            patron.text =
2186
            patron.text =
2156
                escape_str(patron.surname) +
2187
                escape_str(patron.surname) +
2157
                ", " +
2188
                ", " +
2158
                escape_str(patron.firstname);
2189
                escape_str(patron.firstname);
2159
2190
2160
            // Add and select new option
2191
            const newOption = new Option(patron.text, patron.id, true, true);
2161
            let newOption = new Option(patron.text, patron.id, true, true);
2162
            patronSelect.append(newOption).trigger("change");
2192
            patronSelect.append(newOption).trigger("change");
2163
2164
            // manually trigger the `select2:select` event
2165
            patronSelect.trigger({
2193
            patronSelect.trigger({
2166
                type: "select2:select",
2194
                type: "select2:select",
2167
                params: {
2195
                params: { data: patron },
2168
                    data: patron,
2169
                },
2170
            });
2196
            });
2171
        });
2197
        });
2172
    }
2198
    }
Lines 2176-2465 function setFormValues( Link Here
2176
        // Wait a bit for the item options to be fully created with data attributes
2202
        // Wait a bit for the item options to be fully created with data attributes
2177
        setTimeout(function () {
2203
        setTimeout(function () {
2178
            $("#booking_item_id").val(booking_item_id).trigger("change");
2204
            $("#booking_item_id").val(booking_item_id).trigger("change");
2179
            // Also trigger the select2:select event with proper data
2205
            const selectedOption = $("#booking_item_id option:selected")[0];
2180
            let selectedOption = $("#booking_item_id option:selected")[0];
2181
            if (selectedOption) {
2206
            if (selectedOption) {
2182
                $("#booking_item_id").trigger({
2207
                $("#booking_item_id").trigger({
2183
                    type: "select2:select",
2208
                    type: "select2:select",
2184
                    params: {
2209
                    params: {
2185
                        data: {
2210
                        data: { id: booking_item_id, element: selectedOption },
2186
                            id: booking_item_id,
2187
                            element: selectedOption,
2188
                        },
2189
                    },
2211
                    },
2190
                });
2212
                });
2191
            }
2213
            }
2192
2214
            // Set dates AFTER item selection to ensure booking_itemtype_id is set
2193
            // IMPORTANT: Set dates AFTER item selection completes
2215
            setPickerDates(periodPicker, start_date, end_date);
2194
            // This ensures booking_itemtype_id is set before dates are validated
2195
            if (start_date) {
2196
                // Allow invalid pre-load so setDate can set date range
2197
                // periodPicker.set('allowInvalidPreload', true);
2198
                // FIXME: Why is this the case.. we're passing two valid Date objects
2199
                let start = new Date(start_date);
2200
                let end = new Date(end_date);
2201
2202
                let dates = [new Date(start_date), new Date(end_date)];
2203
                periodPicker.setDate(dates, true);
2204
            }
2205
        }, 100);
2216
        }, 100);
2217
    } else if (start_date) {
2218
        setPickerDates(periodPicker, start_date, end_date);
2219
    } else {
2220
        periodPicker.redraw();
2206
    }
2221
    }
2207
    // If no item selected but dates provided, set them now
2222
}
2208
    else if (start_date) {
2223
2209
        let start = new Date(start_date);
2224
/**
2210
        let end = new Date(end_date);
2225
 * Get available items of a specific itemtype for a booking period
2226
 * @param {string} startDate - Start date string
2227
 * @param {string} endDate - End date string
2228
 * @returns {Array} - Array of available items
2229
 */
2230
function getAvailableItemsForPeriod(startDate, endDate) {
2231
    const itemsOfType = bookable_items.filter(
2232
        item => item.effective_item_type_id === booking_itemtype_id
2233
    );
2234
    return itemsOfType.filter(item =>
2235
        isItemAvailableForPeriod(
2236
            item.item_id,
2237
            new Date(startDate),
2238
            new Date(endDate)
2239
        )
2240
    );
2241
}
2211
2242
2212
        let dates = [new Date(start_date), new Date(end_date)];
2243
/**
2213
        periodPicker.setDate(dates, true);
2244
 * Build the booking payload with item selection logic
2245
 * @param {Object} basePayload - Base payload with common fields
2246
 * @param {string} itemId - Selected item ID (0 for "any item")
2247
 * @param {string} startDate - Start date string
2248
 * @param {string} endDate - End date string
2249
 * @returns {Object|null} - Complete payload or null if no items available
2250
 */
2251
function buildBookingPayload(basePayload, itemId, startDate, endDate) {
2252
    const payload = { ...basePayload };
2253
2254
    if (itemId == 0) {
2255
        const availableItems = getAvailableItemsForPeriod(startDate, endDate);
2256
        if (availableItems.length === 0) {
2257
            return null;
2258
        } else if (availableItems.length === 1) {
2259
            payload.item_id = availableItems[0].item_id;
2260
        } else {
2261
            payload.itemtype_id = booking_itemtype_id;
2262
        }
2263
    } else {
2264
        payload.item_id = itemId;
2214
    }
2265
    }
2215
    // Reset periodPicker, biblio_id may have been nulled
2266
2216
    else {
2267
    return payload;
2217
        periodPicker.redraw();
2268
}
2269
2270
/**
2271
 * Create timeline item data from booking response
2272
 * @param {Object} data - Booking response data
2273
 * @returns {Object} - Timeline item data
2274
 */
2275
function createTimelineItem(data) {
2276
    const startServerTz = dayjs(data.start_date).tz($timezone());
2277
    const endServerTz = dayjs(data.end_date).tz($timezone());
2278
    return {
2279
        id: data.booking_id,
2280
        booking: data.booking_id,
2281
        patron: data.patron_id,
2282
        start: $toDisplayDate(startServerTz),
2283
        end: $toDisplayDate(endServerTz),
2284
        content: $patron_to_html(booking_patron, {
2285
            display_cardnumber: true,
2286
            url: false,
2287
        }),
2288
        editable: { remove: true, updateTime: true },
2289
        type: "range",
2290
        group: data.item_id ? data.item_id : 0,
2291
    };
2292
}
2293
2294
/**
2295
 * Show error message in booking result area
2296
 * @param {string} message - Error message to display
2297
 */
2298
function showBookingError(message) {
2299
    $("#booking_result").replaceWith(
2300
        '<div id="booking_result" class="alert alert-danger">' +
2301
            message +
2302
            "</div>"
2303
    );
2304
}
2305
2306
/**
2307
 * Show success feedback and close modal
2308
 * @param {string} message - Success message to display
2309
 */
2310
function showBookingSuccess(message) {
2311
    $("#transient_result").replaceWith(
2312
        '<div id="transient_result" class="alert alert-info">' +
2313
            message +
2314
            "</div>"
2315
    );
2316
    $("#placeBookingModal").modal("hide");
2317
}
2318
2319
/**
2320
 * Refresh bookings table if present
2321
 */
2322
function refreshBookingsTable() {
2323
    if (typeof bookings_table !== "undefined" && bookings_table !== null) {
2324
        bookings_table.api().ajax.reload();
2218
    }
2325
    }
2219
}
2326
}
2220
2327
2221
$("#placeBookingForm").on("submit", function (e) {
2328
$("#placeBookingForm").on("submit", function (e) {
2222
    e.preventDefault();
2329
    e.preventDefault();
2223
2330
2224
    let url = "/api/v1/bookings";
2331
    const url = "/api/v1/bookings";
2225
2332
    const start_date = $("#booking_start_date").val();
2226
    let start_date = $("#booking_start_date").val();
2333
    const end_date = $("#booking_end_date").val();
2227
    let end_date = $("#booking_end_date").val();
2334
    const item_id = $("#booking_item_id").val();
2228
    let pickup_library_id = $("#pickup_library_id").val();
2229
    let biblio_id = $("#booking_biblio_id").val();
2230
    let item_id = $("#booking_item_id").val();
2231
2335
2232
    // Prepare booking payload
2336
    const basePayload = {
2233
    let booking_payload = {
2234
        start_date: start_date,
2337
        start_date: start_date,
2235
        end_date: end_date,
2338
        end_date: end_date,
2236
        pickup_library_id: pickup_library_id,
2339
        pickup_library_id: $("#pickup_library_id").val(),
2237
        biblio_id: biblio_id,
2340
        biblio_id: $("#booking_biblio_id").val(),
2238
        patron_id: $("#booking_patron_id").find(":selected").val(),
2341
        patron_id: $("#booking_patron_id").find(":selected").val(),
2239
    };
2342
    };
2240
2343
2241
    // If "any item" is selected, determine whether to send item_id or itemtype_id
2344
    const payload = buildBookingPayload(
2242
    if (item_id == 0) {
2345
        basePayload,
2243
        // Get items of the selected itemtype that are available for the period
2346
        item_id,
2244
        let itemsOfType = bookable_items.filter(
2347
        start_date,
2245
            item => item.effective_item_type_id === booking_itemtype_id
2348
        end_date
2246
        );
2349
    );
2247
2350
    if (!payload) {
2248
        let availableItems = itemsOfType.filter(item => {
2351
        showBookingError(__("No suitable item found for booking"));
2249
            return isItemAvailableForPeriod(
2352
        return;
2250
                item.item_id,
2251
                new Date(start_date),
2252
                new Date(end_date)
2253
            );
2254
        });
2255
2256
        if (availableItems.length === 0) {
2257
            $("#booking_result").replaceWith(
2258
                '<div id="booking_result" class="alert alert-danger">' +
2259
                    __("No suitable item found for booking") +
2260
                    "</div>"
2261
            );
2262
            return;
2263
        } else if (availableItems.length === 1) {
2264
            // Only one item available - optimization: send specific item_id
2265
            booking_payload.item_id = availableItems[0].item_id;
2266
        } else {
2267
            // Multiple items available - let server choose optimal item
2268
            booking_payload.itemtype_id = booking_itemtype_id;
2269
        }
2270
    } else {
2271
        // Specific item selected
2272
        booking_payload.item_id = item_id;
2273
    }
2353
    }
2274
2354
2275
    if (!booking_id) {
2355
    if (!booking_id) {
2276
        let posting = $.post(url, JSON.stringify(booking_payload));
2356
        // Create new booking
2277
2357
        $.post(url, JSON.stringify(payload))
2278
        posting.done(function (data) {
2358
            .done(function (data) {
2279
            // Update bookings store for subsequent bookings
2359
                bookings.push(data);
2280
            bookings.push(data);
2360
                refreshBookingsTable();
2281
2361
2282
            // Update bookings page as required
2362
                if (typeof timeline !== "undefined" && timeline !== null) {
2283
            if (
2363
                    timeline.itemsData.add(createTimelineItem(data));
2284
                typeof bookings_table !== "undefined" &&
2364
                    timeline.focus(data.booking_id);
2285
                bookings_table !== null
2365
                }
2286
            ) {
2287
                bookings_table.api().ajax.reload();
2288
            }
2289
            if (typeof timeline !== "undefined" && timeline !== null) {
2290
                // Convert to library timezone for timeline display
2291
                const startServerTz = dayjs(data.start_date).tz($timezone());
2292
                const endServerTz = dayjs(data.end_date).tz($timezone());
2293
                timeline.itemsData.add({
2294
                    id: data.booking_id,
2295
                    booking: data.booking_id,
2296
                    patron: data.patron_id,
2297
                    start: $toDisplayDate(startServerTz),
2298
                    end: $toDisplayDate(endServerTz),
2299
                    content: $patron_to_html(booking_patron, {
2300
                        display_cardnumber: true,
2301
                        url: false,
2302
                    }),
2303
                    editable: { remove: true, updateTime: true },
2304
                    type: "range",
2305
                    group: data.item_id ? data.item_id : 0,
2306
                });
2307
                timeline.focus(data.booking_id);
2308
            }
2309
2310
            // Update bookings counts
2311
            $(".bookings_count").html(
2312
                parseInt($(".bookings_count").html(), 10) + 1
2313
            );
2314
2315
            // Set feedback
2316
            $("#transient_result").replaceWith(
2317
                '<div id="transient_result" class="alert alert-info">' +
2318
                    __("Booking successfully placed") +
2319
                    "</div>"
2320
            );
2321
2322
            // Close modal
2323
            $("#placeBookingModal").modal("hide");
2324
        });
2325
2366
2326
        posting.fail(function (data) {
2367
                $(".bookings_count").html(
2327
            $("#booking_result").replaceWith(
2368
                    toInt($(".bookings_count").html()) + 1
2328
                '<div id="booking_result" class="alert alert-danger">' +
2329
                    __("Failure") +
2330
                    "</div>"
2331
            );
2332
        });
2333
    } else {
2334
        // For edits with "any item" (item_id == 0), use same hybrid approach as new bookings
2335
        let edit_payload = {
2336
            booking_id: booking_id,
2337
            start_date: start_date,
2338
            end_date: end_date,
2339
            pickup_library_id: pickup_library_id,
2340
            biblio_id: biblio_id,
2341
            patron_id: $("#booking_patron_id").find(":selected").val(),
2342
        };
2343
2344
        if (item_id == 0) {
2345
            // Get items of the selected itemtype that are available for the period
2346
            let itemsOfType = bookable_items.filter(
2347
                item => item.effective_item_type_id === booking_itemtype_id
2348
            );
2349
2350
            let availableItems = itemsOfType.filter(item => {
2351
                return isItemAvailableForPeriod(
2352
                    item.item_id,
2353
                    new Date(start_date),
2354
                    new Date(end_date)
2355
                );
2369
                );
2370
                showBookingSuccess(__("Booking successfully placed"));
2371
            })
2372
            .fail(function () {
2373
                showBookingError(__("Failure"));
2356
            });
2374
            });
2375
    } else {
2376
        // Update existing booking
2377
        payload.booking_id = booking_id;
2357
2378
2358
            if (availableItems.length === 0) {
2379
        $.ajax({
2359
                $("#booking_result").replaceWith(
2360
                    '<div id="booking_result" class="alert alert-danger">' +
2361
                        __("No suitable item found for booking") +
2362
                        "</div>"
2363
                );
2364
                return;
2365
            } else if (availableItems.length === 1) {
2366
                // Only one item available - send specific item_id
2367
                edit_payload.item_id = availableItems[0].item_id;
2368
            } else {
2369
                // Multiple items available - let server choose optimal item
2370
                edit_payload.itemtype_id = booking_itemtype_id;
2371
            }
2372
        } else {
2373
            // Specific item selected
2374
            edit_payload.item_id = item_id;
2375
        }
2376
2377
        url += "/" + booking_id;
2378
        let putting = $.ajax({
2379
            method: "PUT",
2380
            method: "PUT",
2380
            url: url,
2381
            url: url + "/" + booking_id,
2381
            contentType: "application/json",
2382
            contentType: "application/json",
2382
            data: JSON.stringify(edit_payload),
2383
            data: JSON.stringify(payload),
2383
        });
2384
        })
2384
2385
            .done(function (data) {
2385
        putting.done(function (data) {
2386
                const target = bookings.find(
2386
            update_success = 1;
2387
                    obj => obj.booking_id === data.booking_id
2387
2388
                );
2388
            // Update bookings store for subsequent bookings
2389
                if (target) {
2389
            let target = bookings.find(
2390
                    Object.assign(target, data);
2390
                obj => obj.booking_id === data.booking_id
2391
                }
2391
            );
2392
                refreshBookingsTable();
2392
            Object.assign(target, data);
2393
2394
            // Update bookings page as required
2395
            if (
2396
                typeof bookings_table !== "undefined" &&
2397
                bookings_table !== null
2398
            ) {
2399
                bookings_table.api().ajax.reload();
2400
            }
2401
            if (typeof timeline !== "undefined" && timeline !== null) {
2402
                // Convert to library timezone for timeline display
2403
                const startServerTz = dayjs(data.start_date).tz($timezone());
2404
                const endServerTz = dayjs(data.end_date).tz($timezone());
2405
                timeline.itemsData.update({
2406
                    id: data.booking_id,
2407
                    booking: data.booking_id,
2408
                    patron: data.patron_id,
2409
                    start: $toDisplayDate(startServerTz),
2410
                    end: $toDisplayDate(endServerTz),
2411
                    content: $patron_to_html(booking_patron, {
2412
                        display_cardnumber: true,
2413
                        url: false,
2414
                    }),
2415
                    editable: { remove: true, updateTime: true },
2416
                    type: "range",
2417
                    group: data.item_id ? data.item_id : 0,
2418
                });
2419
                timeline.focus(data.booking_id);
2420
            }
2421
2422
            // Set feedback
2423
            $("#transient_result").replaceWith(
2424
                '<div id="transient_result" class="alert alert-info">' +
2425
                    __("Booking successfully updated") +
2426
                    "</div>"
2427
            );
2428
2393
2429
            // Close modal
2394
                if (typeof timeline !== "undefined" && timeline !== null) {
2430
            $("#placeBookingModal").modal("hide");
2395
                    timeline.itemsData.update(createTimelineItem(data));
2431
        });
2396
                    timeline.focus(data.booking_id);
2397
                }
2432
2398
2433
        putting.fail(function (data) {
2399
                showBookingSuccess(__("Booking successfully updated"));
2434
            $("#booking_result").replaceWith(
2400
            })
2435
                '<div id="booking_result" class="alert alert-danger">' +
2401
            .fail(function () {
2436
                    __("Failure") +
2402
                showBookingError(__("Failure"));
2437
                    "</div>"
2403
            });
2438
            );
2439
        });
2440
    }
2404
    }
2441
});
2405
});
2442
2406
2443
$("#placeBookingModal").on("hidden.bs.modal", function (e) {
2407
$("#placeBookingModal").on("hidden.bs.modal", function (e) {
2444
    // Reset patron select
2408
    // Reset patron select
2445
    $("#booking_patron_id").val(null).trigger("change");
2409
    $("#booking_patron_id")
2446
    $("#booking_patron_id").empty();
2410
        .val(null)
2447
    $("#booking_patron_id").prop("disabled", false);
2411
        .trigger("change")
2412
        .empty()
2413
        .prop("disabled", false);
2448
    booking_patron = undefined;
2414
    booking_patron = undefined;
2449
2415
2450
    // Reset item select
2416
    // Reset item select
2451
    $("#booking_item_id").val(parseInt(0)).trigger("change");
2417
    $("#booking_item_id").val(0).trigger("change").prop("disabled", true);
2452
    $("#booking_item_id").prop("disabled", true);
2453
2418
2454
    // Reset itemtype select
2419
    // Reset itemtype select
2455
    $("#booking_itemtype").val(null).trigger("change");
2420
    $("#booking_itemtype").val(null).trigger("change").prop("disabled", true);
2456
    $("#booking_itemtype").prop("disabled", true);
2457
    booking_itemtype_id = undefined;
2421
    booking_itemtype_id = undefined;
2458
2422
2459
    // Reset pickup library select
2423
    // Reset pickup library select
2460
    $("#pickup_library_id").val(null).trigger("change");
2424
    $("#pickup_library_id")
2461
    $("#pickup_library_id").empty();
2425
        .val(null)
2462
    $("#pickup_library_id").prop("disabled", true);
2426
        .trigger("change")
2427
        .empty()
2428
        .prop("disabled", true);
2463
2429
2464
    // Reset booking period picker
2430
    // Reset booking period picker
2465
    $("#period").get(0)._flatpickr.clear();
2431
    $("#period").get(0)._flatpickr.clear();
2466
- 

Return to bug 37707