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 1025-1032 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1025
1055
1026
                    // Iterate through each date within the range of start_date and end_date
1056
                    // Iterate through each date within the range of start_date and end_date
1027
                    // Use dayjs to maintain browser timezone consistency
1057
                    // Use dayjs to maintain browser timezone consistency
1028
                    let currentDate = dayjs(start_date).startOf("day");
1058
                    let currentDate = startOfDay(start_date);
1029
                    const endDate = dayjs(end_date).startOf("day");
1059
                    const endDate = startOfDay(end_date);
1030
                    while (currentDate.isSameOrBefore(endDate, "day")) {
1060
                    while (currentDate.isSameOrBefore(endDate, "day")) {
1031
                        // Format in browser timezone - no UTC conversion
1061
                        // Format in browser timezone - no UTC conversion
1032
                        const currentDateStr = currentDate.format("YYYY-MM-DD");
1062
                        const currentDateStr = currentDate.format("YYYY-MM-DD");
Lines 1162-1184 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1162
                            const itemConflicts = new Map();
1192
                            const itemConflicts = new Map();
1163
                            if (isAnyItemMode) {
1193
                            if (isAnyItemMode) {
1164
                                itemsOfSelectedType.forEach(item => {
1194
                                itemsOfSelectedType.forEach(item => {
1165
                                    itemConflicts.set(
1195
                                    itemConflicts.set(toInt(item.item_id), {
1166
                                        parseInt(item.item_id, 10),
1196
                                        leadConflict: false,
1167
                                        {
1197
                                        trailConflict: false,
1168
                                            leadConflict: false,
1198
                                        leadReason: {
1169
                                            trailConflict: false,
1199
                                            withTrail: false,
1170
                                            leadReason: {
1200
                                            withLead: false,
1171
                                                withTrail: false,
1201
                                            withBooking: false,
1172
                                                withLead: false,
1202
                                        },
1173
                                                withBooking: false,
1203
                                        trailReason: {
1174
                                            },
1204
                                            withTrail: false,
1175
                                            trailReason: {
1205
                                            withLead: false,
1176
                                                withTrail: false,
1206
                                            withBooking: false,
1177
                                                withLead: false,
1207
                                        },
1178
                                                withBooking: false,
1208
                                    });
1179
                                            },
1180
                                        }
1181
                                    );
1182
                                });
1209
                                });
1183
                            }
1210
                            }
1184
1211
Lines 1191-1208 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1191
                                    return;
1218
                                    return;
1192
                                }
1219
                                }
1193
1220
1194
                                const bookingItemId = parseInt(
1221
                                const bookingItemId = toInt(booking.item_id);
1195
                                    booking.item_id,
1196
                                    10
1197
                                );
1198
1222
1199
                                // For specific item mode: skip bookings for different items
1223
                                // For specific item mode: skip bookings for different items
1200
                                if (!isAnyItemMode) {
1224
                                if (!isAnyItemMode) {
1201
                                    if (
1225
                                    if (
1202
                                        booking.item_id &&
1226
                                        booking.item_id &&
1203
                                        booking_item_id &&
1227
                                        booking_item_id &&
1204
                                        bookingItemId !==
1228
                                        bookingItemId !== toInt(booking_item_id)
1205
                                            parseInt(booking_item_id, 10)
1206
                                    ) {
1229
                                    ) {
1207
                                        return;
1230
                                        return;
1208
                                    }
1231
                                    }
Lines 1213-1224 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1213
                                    }
1236
                                    }
1214
                                }
1237
                                }
1215
1238
1216
                                const bookingStart = dayjs(
1239
                                const bookingStart = startOfDay(
1217
                                    booking.start_date
1240
                                    booking.start_date
1218
                                ).startOf("day");
1241
                                );
1219
                                const bookingEnd = dayjs(
1242
                                const bookingEnd = startOfDay(booking.end_date);
1220
                                    booking.end_date
1221
                                ).startOf("day");
1222
1243
1223
                                // BIDIRECTIONAL: Mathematical checks for conflicts (works across month boundaries)
1244
                                // BIDIRECTIONAL: Mathematical checks for conflicts (works across month boundaries)
1224
                                // Calculate this booking's full protected period
1245
                                // Calculate this booking's full protected period
Lines 2106-2111 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
2106
    }
2127
    }
2107
});
2128
});
2108
2129
2130
/**
2131
 * Set date range on the period picker
2132
 * @param {Object} periodPicker - Flatpickr instance
2133
 * @param {string} start_date - Start date string
2134
 * @param {string} end_date - End date string
2135
 */
2136
function setPickerDates(periodPicker, start_date, end_date) {
2137
    if (start_date && end_date) {
2138
        periodPicker.setDate([new Date(start_date), new Date(end_date)], true);
2139
    }
2140
}
2141
2109
function setFormValues(
2142
function setFormValues(
2110
    patron_id,
2143
    patron_id,
2111
    booking_item_id,
2144
    booking_item_id,
Lines 2118-2150 function setFormValues( Link Here
2118
    if (item_type_id) {
2151
    if (item_type_id) {
2119
        booking_itemtype_id = item_type_id;
2152
        booking_itemtype_id = item_type_id;
2120
    }
2153
    }
2154
2121
    // If passed patron, pre-select
2155
    // If passed patron, pre-select
2122
    if (patron_id) {
2156
    if (patron_id) {
2123
        let patronSelect = $("#booking_patron_id");
2157
        const patronSelect = $("#booking_patron_id");
2124
        let patron = $.ajax({
2158
        $.ajax({
2125
            url: "/api/v1/patrons/" + patron_id,
2159
            url: "/api/v1/patrons/" + patron_id,
2126
            dataType: "json",
2160
            dataType: "json",
2127
            type: "GET",
2161
            type: "GET",
2128
        });
2162
        }).done(function (patron) {
2129
2130
        $.when(patron).done(function (patron) {
2131
            // clone patron_id to id (select2 expects an id field)
2132
            patron.id = patron.patron_id;
2163
            patron.id = patron.patron_id;
2133
            patron.text =
2164
            patron.text =
2134
                escape_str(patron.surname) +
2165
                escape_str(patron.surname) +
2135
                ", " +
2166
                ", " +
2136
                escape_str(patron.firstname);
2167
                escape_str(patron.firstname);
2137
2168
2138
            // Add and select new option
2169
            const newOption = new Option(patron.text, patron.id, true, true);
2139
            let newOption = new Option(patron.text, patron.id, true, true);
2140
            patronSelect.append(newOption).trigger("change");
2170
            patronSelect.append(newOption).trigger("change");
2141
2142
            // manually trigger the `select2:select` event
2143
            patronSelect.trigger({
2171
            patronSelect.trigger({
2144
                type: "select2:select",
2172
                type: "select2:select",
2145
                params: {
2173
                params: { data: patron },
2146
                    data: patron,
2147
                },
2148
            });
2174
            });
2149
        });
2175
        });
2150
    }
2176
    }
Lines 2154-2443 function setFormValues( Link Here
2154
        // Wait a bit for the item options to be fully created with data attributes
2180
        // Wait a bit for the item options to be fully created with data attributes
2155
        setTimeout(function () {
2181
        setTimeout(function () {
2156
            $("#booking_item_id").val(booking_item_id).trigger("change");
2182
            $("#booking_item_id").val(booking_item_id).trigger("change");
2157
            // Also trigger the select2:select event with proper data
2183
            const selectedOption = $("#booking_item_id option:selected")[0];
2158
            let selectedOption = $("#booking_item_id option:selected")[0];
2159
            if (selectedOption) {
2184
            if (selectedOption) {
2160
                $("#booking_item_id").trigger({
2185
                $("#booking_item_id").trigger({
2161
                    type: "select2:select",
2186
                    type: "select2:select",
2162
                    params: {
2187
                    params: {
2163
                        data: {
2188
                        data: { id: booking_item_id, element: selectedOption },
2164
                            id: booking_item_id,
2165
                            element: selectedOption,
2166
                        },
2167
                    },
2189
                    },
2168
                });
2190
                });
2169
            }
2191
            }
2170
2192
            // Set dates AFTER item selection to ensure booking_itemtype_id is set
2171
            // IMPORTANT: Set dates AFTER item selection completes
2193
            setPickerDates(periodPicker, start_date, end_date);
2172
            // This ensures booking_itemtype_id is set before dates are validated
2173
            if (start_date) {
2174
                // Allow invalid pre-load so setDate can set date range
2175
                // periodPicker.set('allowInvalidPreload', true);
2176
                // FIXME: Why is this the case.. we're passing two valid Date objects
2177
                let start = new Date(start_date);
2178
                let end = new Date(end_date);
2179
2180
                let dates = [new Date(start_date), new Date(end_date)];
2181
                periodPicker.setDate(dates, true);
2182
            }
2183
        }, 100);
2194
        }, 100);
2195
    } else if (start_date) {
2196
        setPickerDates(periodPicker, start_date, end_date);
2197
    } else {
2198
        periodPicker.redraw();
2184
    }
2199
    }
2185
    // If no item selected but dates provided, set them now
2200
}
2186
    else if (start_date) {
2201
2187
        let start = new Date(start_date);
2202
/**
2188
        let end = new Date(end_date);
2203
 * Get available items of a specific itemtype for a booking period
2204
 * @param {string} startDate - Start date string
2205
 * @param {string} endDate - End date string
2206
 * @returns {Array} - Array of available items
2207
 */
2208
function getAvailableItemsForPeriod(startDate, endDate) {
2209
    const itemsOfType = bookable_items.filter(
2210
        item => item.effective_item_type_id === booking_itemtype_id
2211
    );
2212
    return itemsOfType.filter(item =>
2213
        isItemAvailableForPeriod(
2214
            item.item_id,
2215
            new Date(startDate),
2216
            new Date(endDate)
2217
        )
2218
    );
2219
}
2189
2220
2190
        let dates = [new Date(start_date), new Date(end_date)];
2221
/**
2191
        periodPicker.setDate(dates, true);
2222
 * Build the booking payload with item selection logic
2223
 * @param {Object} basePayload - Base payload with common fields
2224
 * @param {string} itemId - Selected item ID (0 for "any item")
2225
 * @param {string} startDate - Start date string
2226
 * @param {string} endDate - End date string
2227
 * @returns {Object|null} - Complete payload or null if no items available
2228
 */
2229
function buildBookingPayload(basePayload, itemId, startDate, endDate) {
2230
    const payload = { ...basePayload };
2231
2232
    if (itemId == 0) {
2233
        const availableItems = getAvailableItemsForPeriod(startDate, endDate);
2234
        if (availableItems.length === 0) {
2235
            return null;
2236
        } else if (availableItems.length === 1) {
2237
            payload.item_id = availableItems[0].item_id;
2238
        } else {
2239
            payload.itemtype_id = booking_itemtype_id;
2240
        }
2241
    } else {
2242
        payload.item_id = itemId;
2192
    }
2243
    }
2193
    // Reset periodPicker, biblio_id may have been nulled
2244
2194
    else {
2245
    return payload;
2195
        periodPicker.redraw();
2246
}
2247
2248
/**
2249
 * Create timeline item data from booking response
2250
 * @param {Object} data - Booking response data
2251
 * @returns {Object} - Timeline item data
2252
 */
2253
function createTimelineItem(data) {
2254
    const startServerTz = dayjs(data.start_date).tz($timezone());
2255
    const endServerTz = dayjs(data.end_date).tz($timezone());
2256
    return {
2257
        id: data.booking_id,
2258
        booking: data.booking_id,
2259
        patron: data.patron_id,
2260
        start: $toDisplayDate(startServerTz),
2261
        end: $toDisplayDate(endServerTz),
2262
        content: $patron_to_html(booking_patron, {
2263
            display_cardnumber: true,
2264
            url: false,
2265
        }),
2266
        editable: { remove: true, updateTime: true },
2267
        type: "range",
2268
        group: data.item_id ? data.item_id : 0,
2269
    };
2270
}
2271
2272
/**
2273
 * Show error message in booking result area
2274
 * @param {string} message - Error message to display
2275
 */
2276
function showBookingError(message) {
2277
    $("#booking_result").replaceWith(
2278
        '<div id="booking_result" class="alert alert-danger">' +
2279
            message +
2280
            "</div>"
2281
    );
2282
}
2283
2284
/**
2285
 * Show success feedback and close modal
2286
 * @param {string} message - Success message to display
2287
 */
2288
function showBookingSuccess(message) {
2289
    $("#transient_result").replaceWith(
2290
        '<div id="transient_result" class="alert alert-info">' +
2291
            message +
2292
            "</div>"
2293
    );
2294
    $("#placeBookingModal").modal("hide");
2295
}
2296
2297
/**
2298
 * Refresh bookings table if present
2299
 */
2300
function refreshBookingsTable() {
2301
    if (typeof bookings_table !== "undefined" && bookings_table !== null) {
2302
        bookings_table.api().ajax.reload();
2196
    }
2303
    }
2197
}
2304
}
2198
2305
2199
$("#placeBookingForm").on("submit", function (e) {
2306
$("#placeBookingForm").on("submit", function (e) {
2200
    e.preventDefault();
2307
    e.preventDefault();
2201
2308
2202
    let url = "/api/v1/bookings";
2309
    const url = "/api/v1/bookings";
2203
2310
    const start_date = $("#booking_start_date").val();
2204
    let start_date = $("#booking_start_date").val();
2311
    const end_date = $("#booking_end_date").val();
2205
    let end_date = $("#booking_end_date").val();
2312
    const item_id = $("#booking_item_id").val();
2206
    let pickup_library_id = $("#pickup_library_id").val();
2207
    let biblio_id = $("#booking_biblio_id").val();
2208
    let item_id = $("#booking_item_id").val();
2209
2313
2210
    // Prepare booking payload
2314
    const basePayload = {
2211
    let booking_payload = {
2212
        start_date: start_date,
2315
        start_date: start_date,
2213
        end_date: end_date,
2316
        end_date: end_date,
2214
        pickup_library_id: pickup_library_id,
2317
        pickup_library_id: $("#pickup_library_id").val(),
2215
        biblio_id: biblio_id,
2318
        biblio_id: $("#booking_biblio_id").val(),
2216
        patron_id: $("#booking_patron_id").find(":selected").val(),
2319
        patron_id: $("#booking_patron_id").find(":selected").val(),
2217
    };
2320
    };
2218
2321
2219
    // If "any item" is selected, determine whether to send item_id or itemtype_id
2322
    const payload = buildBookingPayload(
2220
    if (item_id == 0) {
2323
        basePayload,
2221
        // Get items of the selected itemtype that are available for the period
2324
        item_id,
2222
        let itemsOfType = bookable_items.filter(
2325
        start_date,
2223
            item => item.effective_item_type_id === booking_itemtype_id
2326
        end_date
2224
        );
2327
    );
2225
2328
    if (!payload) {
2226
        let availableItems = itemsOfType.filter(item => {
2329
        showBookingError(__("No suitable item found for booking"));
2227
            return isItemAvailableForPeriod(
2330
        return;
2228
                item.item_id,
2229
                new Date(start_date),
2230
                new Date(end_date)
2231
            );
2232
        });
2233
2234
        if (availableItems.length === 0) {
2235
            $("#booking_result").replaceWith(
2236
                '<div id="booking_result" class="alert alert-danger">' +
2237
                    __("No suitable item found for booking") +
2238
                    "</div>"
2239
            );
2240
            return;
2241
        } else if (availableItems.length === 1) {
2242
            // Only one item available - optimization: send specific item_id
2243
            booking_payload.item_id = availableItems[0].item_id;
2244
        } else {
2245
            // Multiple items available - let server choose optimal item
2246
            booking_payload.itemtype_id = booking_itemtype_id;
2247
        }
2248
    } else {
2249
        // Specific item selected
2250
        booking_payload.item_id = item_id;
2251
    }
2331
    }
2252
2332
2253
    if (!booking_id) {
2333
    if (!booking_id) {
2254
        let posting = $.post(url, JSON.stringify(booking_payload));
2334
        // Create new booking
2255
2335
        $.post(url, JSON.stringify(payload))
2256
        posting.done(function (data) {
2336
            .done(function (data) {
2257
            // Update bookings store for subsequent bookings
2337
                bookings.push(data);
2258
            bookings.push(data);
2338
                refreshBookingsTable();
2259
2339
2260
            // Update bookings page as required
2340
                if (typeof timeline !== "undefined" && timeline !== null) {
2261
            if (
2341
                    timeline.itemsData.add(createTimelineItem(data));
2262
                typeof bookings_table !== "undefined" &&
2342
                    timeline.focus(data.booking_id);
2263
                bookings_table !== null
2343
                }
2264
            ) {
2265
                bookings_table.api().ajax.reload();
2266
            }
2267
            if (typeof timeline !== "undefined" && timeline !== null) {
2268
                // Convert to library timezone for timeline display
2269
                const startServerTz = dayjs(data.start_date).tz($timezone());
2270
                const endServerTz = dayjs(data.end_date).tz($timezone());
2271
                timeline.itemsData.add({
2272
                    id: data.booking_id,
2273
                    booking: data.booking_id,
2274
                    patron: data.patron_id,
2275
                    start: $toDisplayDate(startServerTz),
2276
                    end: $toDisplayDate(endServerTz),
2277
                    content: $patron_to_html(booking_patron, {
2278
                        display_cardnumber: true,
2279
                        url: false,
2280
                    }),
2281
                    editable: { remove: true, updateTime: true },
2282
                    type: "range",
2283
                    group: data.item_id ? data.item_id : 0,
2284
                });
2285
                timeline.focus(data.booking_id);
2286
            }
2287
2288
            // Update bookings counts
2289
            $(".bookings_count").html(
2290
                parseInt($(".bookings_count").html(), 10) + 1
2291
            );
2292
2293
            // Set feedback
2294
            $("#transient_result").replaceWith(
2295
                '<div id="transient_result" class="alert alert-info">' +
2296
                    __("Booking successfully placed") +
2297
                    "</div>"
2298
            );
2299
2300
            // Close modal
2301
            $("#placeBookingModal").modal("hide");
2302
        });
2303
2344
2304
        posting.fail(function (data) {
2345
                $(".bookings_count").html(
2305
            $("#booking_result").replaceWith(
2346
                    toInt($(".bookings_count").html()) + 1
2306
                '<div id="booking_result" class="alert alert-danger">' +
2307
                    __("Failure") +
2308
                    "</div>"
2309
            );
2310
        });
2311
    } else {
2312
        // For edits with "any item" (item_id == 0), use same hybrid approach as new bookings
2313
        let edit_payload = {
2314
            booking_id: booking_id,
2315
            start_date: start_date,
2316
            end_date: end_date,
2317
            pickup_library_id: pickup_library_id,
2318
            biblio_id: biblio_id,
2319
            patron_id: $("#booking_patron_id").find(":selected").val(),
2320
        };
2321
2322
        if (item_id == 0) {
2323
            // Get items of the selected itemtype that are available for the period
2324
            let itemsOfType = bookable_items.filter(
2325
                item => item.effective_item_type_id === booking_itemtype_id
2326
            );
2327
2328
            let availableItems = itemsOfType.filter(item => {
2329
                return isItemAvailableForPeriod(
2330
                    item.item_id,
2331
                    new Date(start_date),
2332
                    new Date(end_date)
2333
                );
2347
                );
2348
                showBookingSuccess(__("Booking successfully placed"));
2349
            })
2350
            .fail(function () {
2351
                showBookingError(__("Failure"));
2334
            });
2352
            });
2353
    } else {
2354
        // Update existing booking
2355
        payload.booking_id = booking_id;
2335
2356
2336
            if (availableItems.length === 0) {
2357
        $.ajax({
2337
                $("#booking_result").replaceWith(
2338
                    '<div id="booking_result" class="alert alert-danger">' +
2339
                        __("No suitable item found for booking") +
2340
                        "</div>"
2341
                );
2342
                return;
2343
            } else if (availableItems.length === 1) {
2344
                // Only one item available - send specific item_id
2345
                edit_payload.item_id = availableItems[0].item_id;
2346
            } else {
2347
                // Multiple items available - let server choose optimal item
2348
                edit_payload.itemtype_id = booking_itemtype_id;
2349
            }
2350
        } else {
2351
            // Specific item selected
2352
            edit_payload.item_id = item_id;
2353
        }
2354
2355
        url += "/" + booking_id;
2356
        let putting = $.ajax({
2357
            method: "PUT",
2358
            method: "PUT",
2358
            url: url,
2359
            url: url + "/" + booking_id,
2359
            contentType: "application/json",
2360
            contentType: "application/json",
2360
            data: JSON.stringify(edit_payload),
2361
            data: JSON.stringify(payload),
2361
        });
2362
        })
2362
2363
            .done(function (data) {
2363
        putting.done(function (data) {
2364
                const target = bookings.find(
2364
            update_success = 1;
2365
                    obj => obj.booking_id === data.booking_id
2365
2366
                );
2366
            // Update bookings store for subsequent bookings
2367
                if (target) {
2367
            let target = bookings.find(
2368
                    Object.assign(target, data);
2368
                obj => obj.booking_id === data.booking_id
2369
                }
2369
            );
2370
                refreshBookingsTable();
2370
            Object.assign(target, data);
2371
2372
            // Update bookings page as required
2373
            if (
2374
                typeof bookings_table !== "undefined" &&
2375
                bookings_table !== null
2376
            ) {
2377
                bookings_table.api().ajax.reload();
2378
            }
2379
            if (typeof timeline !== "undefined" && timeline !== null) {
2380
                // Convert to library timezone for timeline display
2381
                const startServerTz = dayjs(data.start_date).tz($timezone());
2382
                const endServerTz = dayjs(data.end_date).tz($timezone());
2383
                timeline.itemsData.update({
2384
                    id: data.booking_id,
2385
                    booking: data.booking_id,
2386
                    patron: data.patron_id,
2387
                    start: $toDisplayDate(startServerTz),
2388
                    end: $toDisplayDate(endServerTz),
2389
                    content: $patron_to_html(booking_patron, {
2390
                        display_cardnumber: true,
2391
                        url: false,
2392
                    }),
2393
                    editable: { remove: true, updateTime: true },
2394
                    type: "range",
2395
                    group: data.item_id ? data.item_id : 0,
2396
                });
2397
                timeline.focus(data.booking_id);
2398
            }
2399
2400
            // Set feedback
2401
            $("#transient_result").replaceWith(
2402
                '<div id="transient_result" class="alert alert-info">' +
2403
                    __("Booking successfully updated") +
2404
                    "</div>"
2405
            );
2406
2371
2407
            // Close modal
2372
                if (typeof timeline !== "undefined" && timeline !== null) {
2408
            $("#placeBookingModal").modal("hide");
2373
                    timeline.itemsData.update(createTimelineItem(data));
2409
        });
2374
                    timeline.focus(data.booking_id);
2375
                }
2410
2376
2411
        putting.fail(function (data) {
2377
                showBookingSuccess(__("Booking successfully updated"));
2412
            $("#booking_result").replaceWith(
2378
            })
2413
                '<div id="booking_result" class="alert alert-danger">' +
2379
            .fail(function () {
2414
                    __("Failure") +
2380
                showBookingError(__("Failure"));
2415
                    "</div>"
2381
            });
2416
            );
2417
        });
2418
    }
2382
    }
2419
});
2383
});
2420
2384
2421
$("#placeBookingModal").on("hidden.bs.modal", function (e) {
2385
$("#placeBookingModal").on("hidden.bs.modal", function (e) {
2422
    // Reset patron select
2386
    // Reset patron select
2423
    $("#booking_patron_id").val(null).trigger("change");
2387
    $("#booking_patron_id")
2424
    $("#booking_patron_id").empty();
2388
        .val(null)
2425
    $("#booking_patron_id").prop("disabled", false);
2389
        .trigger("change")
2390
        .empty()
2391
        .prop("disabled", false);
2426
    booking_patron = undefined;
2392
    booking_patron = undefined;
2427
2393
2428
    // Reset item select
2394
    // Reset item select
2429
    $("#booking_item_id").val(parseInt(0)).trigger("change");
2395
    $("#booking_item_id").val(0).trigger("change").prop("disabled", true);
2430
    $("#booking_item_id").prop("disabled", true);
2431
2396
2432
    // Reset itemtype select
2397
    // Reset itemtype select
2433
    $("#booking_itemtype").val(null).trigger("change");
2398
    $("#booking_itemtype").val(null).trigger("change").prop("disabled", true);
2434
    $("#booking_itemtype").prop("disabled", true);
2435
    booking_itemtype_id = undefined;
2399
    booking_itemtype_id = undefined;
2436
2400
2437
    // Reset pickup library select
2401
    // Reset pickup library select
2438
    $("#pickup_library_id").val(null).trigger("change");
2402
    $("#pickup_library_id")
2439
    $("#pickup_library_id").empty();
2403
        .val(null)
2440
    $("#pickup_library_id").prop("disabled", true);
2404
        .trigger("change")
2405
        .empty()
2406
        .prop("disabled", true);
2441
2407
2442
    // Reset booking period picker
2408
    // Reset booking period picker
2443
    $("#period").get(0)._flatpickr.clear();
2409
    $("#period").get(0)._flatpickr.clear();
2444
- 

Return to bug 37707