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

(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js (-202 / +399 lines)
Lines 24-29 function containsAny(integers1, integers2) { Link Here
24
    return false; // No match found
24
    return false; // No match found
25
}
25
}
26
26
27
// Check if a specific item is available for the entire booking period
28
function isItemAvailableForPeriod(itemId, startDate, endDate) {
29
    for (let booking of bookings) {
30
        // Skip if we're editing this booking
31
        if (booking_id && booking_id == booking.booking_id) {
32
            continue;
33
        }
34
35
        if (booking.item_id !== itemId) {
36
            continue; // Different item, no conflict
37
        }
38
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 (
46
            !(
47
                checkEndDate.isBefore(booking_start, "day") ||
48
                checkStartDate.isAfter(booking_end, "day")
49
            )
50
        ) {
51
            return false; // Overlap detected
52
        }
53
    }
54
    return true; // No conflicts found
55
}
56
27
$("#placeBookingModal").on("show.bs.modal", function (e) {
57
$("#placeBookingModal").on("show.bs.modal", function (e) {
28
    // Get context
58
    // Get context
29
    let button = $(e.relatedTarget);
59
    let button = $(e.relatedTarget);
Lines 440-454 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
440
                            // set local copy of selectedDates
470
                            // set local copy of selectedDates
441
                            let selectedDates = periodPicker.selectedDates;
471
                            let selectedDates = periodPicker.selectedDates;
442
472
443
                            // set booked counter
444
                            let booked = 0;
445
446
                            // reset the unavailable items array
447
                            let unavailable_items = [];
448
449
                            // reset the biblio level bookings array
450
                            let biblio_bookings = [];
451
452
                            // disable dates before selected date
473
                            // disable dates before selected date
453
                            if (
474
                            if (
454
                                !selectedDates[1] &&
475
                                !selectedDates[1] &&
Lines 458-630 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
458
                                return true;
479
                                return true;
459
                            }
480
                            }
460
481
461
                            // iterate existing bookings
482
                            // We should always have an itemtype selected and either specific item or "any item"
462
                            for (let booking of bookings) {
483
                            if (!booking_itemtype_id) {
463
                                // Skip if we're editing this booking
484
                                return true; // No itemtype selected, disable everything
464
                                if (
485
                            }
465
                                    booking_id &&
466
                                    booking_id == booking.booking_id
467
                                ) {
468
                                    continue;
469
                                }
470
486
471
                                let start_date = flatpickr.parseDate(
487
                            // If "any item of itemtype" is selected, use smart window maximization
472
                                    booking.start_date
488
                            if (!booking_item_id) {
489
                                return isDateDisabledForItemtype(
490
                                    date,
491
                                    selectedDates
473
                                );
492
                                );
474
                                let end_date = flatpickr.parseDate(
493
                            }
475
                                    booking.end_date
494
                            // If specific item is selected, use item-specific logic
495
                            else {
496
                                return isDateDisabledForSpecificItem(
497
                                    date,
498
                                    selectedDates
476
                                );
499
                                );
500
                            }
501
                        }
502
                    );
503
                }
477
504
478
                                // patron has selected a start date (end date checks)
505
                /**
479
                                if (selectedDates[0]) {
506
                 * SMART ITEMTYPE AVAILABILITY CALCULATION
480
                                    // new booking start date is between existing booking start and end dates
507
                 * For "any item of type X" bookings with dynamic item pool reduction
481
                                    if (
508
                 *
482
                                        selectedDates[0] >= start_date &&
509
                 * ALGORITHM OVERVIEW:
483
                                        selectedDates[0] <= end_date
510
                 * This function implements smart window maximization for itemtype bookings by using
484
                                    ) {
511
                 * dynamic item pool reduction. The core principle is "never re-add items to pool" -
485
                                        if (booking.item_id) {
512
                 * once an item is removed because it becomes unavailable, it's never re-added even
486
                                            if (
513
                 * if it becomes available again later. This ensures optimal resource allocation.
487
                                                unavailable_items.indexOf(
514
                 *
488
                                                    booking.item_id
515
                 * FLOW:
489
                                                ) === -1
516
                 * 1. For start date selection: Disable if ALL items of itemtype are booked
490
                                            ) {
517
                 * 2. For end date selection: Use smart window maximization algorithm
491
                                                unavailable_items.push(
518
                 *
492
                                                    booking.item_id
519
                 * SMART WINDOW MAXIMIZATION:
493
                                                );
520
                 * - Start with items available on the selected start date
494
                                            }
521
                 * - Walk through each day from start to target end date
495
                                        } else {
522
                 * - Remove items from pool when they become unavailable
496
                                            if (
523
                 * - NEVER re-add items even if they become available again later
497
                                                biblio_bookings.indexOf(
524
                 * - Disable date when no items remain in pool
498
                                                    booking.booking_id
525
                 *
499
                                                ) === -1
526
                 * EXAMPLE:
500
                                            ) {
527
                 * Items: A, B, C
501
                                                biblio_bookings.push(
528
                 * A available: days 1-5, booked 6-10, available again 11+
502
                                                    booking.booking_id
529
                 * B available: days 1-8, booked 9-15, available again 16+
503
                                                );
530
                 * C available: days 1-12, booked 13-20, available again 21+
504
                                            }
531
                 *
505
                                        }
532
                 * Start day 3:
506
                                    }
533
                 * - Initial pool: A, B, C
534
                 * - Days 3-5: Pool A, B, C (all available)
535
                 * - Day 6: Remove A (becomes booked), Pool now B, C
536
                 * - Day 9: Remove B (becomes booked), Pool now C
537
                 * - Day 13: Remove C (becomes booked), Pool now EMPTY → disable dates
538
                 * - Result: Can book days 3-12, day 13+ disabled
539
                 * - Note: A becomes available on day 11 but is NOT re-added to pool
540
                 *
541
                 * @param {Date} date - The date being checked for availability
542
                 * @param {Array} selectedDates - Array of selected dates from flatpickr [startDate, endDate?]
543
                 * @returns {boolean} - True if date should be disabled, false if available
544
                 */
545
                function isDateDisabledForItemtype(date, selectedDates) {
546
                    // Get items of the selected itemtype
547
                    let itemsOfType = bookable_items.filter(
548
                        item =>
549
                            item.effective_item_type_id === booking_itemtype_id
550
                    );
507
551
508
                                    // new booking end date would be between existing booking start and end dates
552
                    // For start date selection: disable if ALL items of itemtype are booked on this date
509
                                    else if (
553
                    if (!selectedDates[0]) {
510
                                        date >= start_date &&
554
                        return (
511
                                        date <= end_date
555
                            getAvailableItemsOnDate(date, itemsOfType)
512
                                    ) {
556
                                .length === 0
513
                                        if (booking.item_id) {
557
                        );
514
                                            if (
558
                    }
515
                                                unavailable_items.indexOf(
516
                                                    booking.item_id
517
                                                ) === -1
518
                                            ) {
519
                                                unavailable_items.push(
520
                                                    booking.item_id
521
                                                );
522
                                            }
523
                                        } else {
524
                                            if (
525
                                                biblio_bookings.indexOf(
526
                                                    booking.booking_id
527
                                                ) === -1
528
                                            ) {
529
                                                biblio_bookings.push(
530
                                                    booking.booking_id
531
                                                );
532
                                            }
533
                                        }
534
                                    }
535
559
536
                                    // new booking would span existing booking
560
                    // For end date selection: use smart window maximization
537
                                    else if (
561
                    if (selectedDates[0] && !selectedDates[1]) {
538
                                        selectedDates[0] <= start_date &&
562
                        let result = !isDateInMaximumWindow(
539
                                        date >= end_date
563
                            selectedDates[0],
540
                                    ) {
564
                            date,
541
                                        if (booking.item_id) {
565
                            itemsOfType
542
                                            if (
566
                        );
543
                                                unavailable_items.indexOf(
567
                        return result;
544
                                                    booking.item_id
568
                    }
545
                                                ) === -1
546
                                            ) {
547
                                                unavailable_items.push(
548
                                                    booking.item_id
549
                                                );
550
                                            }
551
                                        } else {
552
                                            if (
553
                                                biblio_bookings.indexOf(
554
                                                    booking.booking_id
555
                                                ) === -1
556
                                            ) {
557
                                                biblio_bookings.push(
558
                                                    booking.booking_id
559
                                                );
560
                                            }
561
                                        }
562
                                    }
563
569
564
                                    // new booking would not conflict
570
                    return false;
565
                                    else {
571
                }
566
                                        continue;
567
                                    }
568
572
569
                                    // check availability based on selection type
573
                /**
570
                                    if (
574
                 * MAXIMUM BOOKING WINDOW CALCULATION ALGORITHM
571
                                        booking_item_id &&
575
                 * Core Implementation of "Never Re-add Items to Pool" Principle
572
                                        booking_item_id != 0
576
                 *
573
                                    ) {
577
                 * PURPOSE:
574
                                        // Specific item selected - check if that item is unavailable
578
                 * Calculate the maximum possible booking window for "any item of itemtype X" bookings
575
                                        if (
579
                 * by dynamically reducing the available item pool as items become unavailable.
576
                                            unavailable_items.indexOf(
580
                 *
577
                                                parseInt(booking_item_id)
581
                 * CORE ALGORITHM: "Never Re-add Items to Pool"
578
                                            ) !== -1
582
                 * 1. Start with items available on the selected start date ONLY
579
                                        ) {
583
                 * 2. Walk through each day from start to target end date
580
                                            return true;
584
                 * 3. Remove items from pool when they become unavailable (booking starts)
581
                                        }
585
                 * 4. NEVER re-add items even if they become available again later (booking ends)
582
                                    } else {
586
                 * 5. Return false (disable date) when no items remain in pool
583
                                        // "Any item" selected - check if all items are unavailable
587
                 *
584
                                        let total_available =
588
                 * WHY THIS WORKS:
585
                                            bookable_items.length -
589
                 * - Maximizes booking windows by ensuring optimal resource allocation
586
                                            unavailable_items.length -
590
                 * - Prevents booking conflicts by being conservative about item availability
587
                                            biblio_bookings.length;
591
                 * - Ensures that if a booking can start on date X, there will always be an
588
                                        if (total_available === 0) {
592
                 *   item available for the entire duration (no conflicts)
589
                                            return true;
593
                 *
590
                                        }
594
                 * DETAILED EXAMPLE:
591
                                    }
595
                 * Items: TABLET001, TABLET002, TABLET003
592
                                }
596
                 * TABLET001: Available 1-9, Booked 10-15, Available 16+
597
                 * TABLET002: Available 1-12, Booked 13-20, Available 21+
598
                 * TABLET003: Available 1-17, Booked 18-25, Available 26+
599
                 *
600
                 * Testing: Can we book from day 5 to day 20?
601
                 *
602
                 * Step 1: Day 5 (start) - Initial pool: {TABLET001, TABLET002, TABLET003}
603
                 * Step 2: Day 6-9 - All items available, pool unchanged
604
                 * Step 3: Day 10 - TABLET001 becomes unavailable → Remove from pool
605
                 *         Pool now: {TABLET002, TABLET003}
606
                 * Step 4: Day 11-12 - Remaining items available, pool unchanged
607
                 * Step 5: Day 13 - TABLET002 becomes unavailable → Remove from pool
608
                 *         Pool now: {TABLET003}
609
                 * Step 6: Day 14-17 - TABLET003 available, pool unchanged
610
                 * Step 7: Day 18 - TABLET003 becomes unavailable → Remove from pool
611
                 *         Pool now: {} (empty)
612
                 * Step 8: Pool is empty → Return false (cannot book to day 20)
613
                 *
614
                 * Result: Can book from day 5 to day 17, but NOT to day 18+
615
                 *
616
                 * CRITICAL NOTE: Even though TABLET001 becomes available again on day 16,
617
                 * it is NOT re-added to the pool. This is the key principle that ensures
618
                 * booking reliability and optimal resource allocation.
619
                 *
620
                 * PERFORMANCE: O(n × d) where n = items of type, d = days in range
621
                 *
622
                 * @param {Date} startDate - Selected start date from flatpickr
623
                 * @param {Date} endDate - Target end date being checked for availability
624
                 * @param {Array} itemsOfType - Items of the selected itemtype
625
                 * @returns {boolean} - True if date is within maximum window, false if beyond
626
                 */
627
                function isDateInMaximumWindow(
628
                    startDate,
629
                    endDate,
630
                    itemsOfType
631
                ) {
632
                    // Start with only items available on the start date - never add items back
633
                    let availableOnStart = getAvailableItemsOnDate(
634
                        startDate,
635
                        itemsOfType
636
                    );
637
                    let availableItems = new Set(
638
                        availableOnStart.map(item => parseInt(item.item_id, 10))
639
                    );
593
640
594
                                // patron has not yet selected a start date (start date checks)
641
                    let currentDate = dayjs(startDate);
595
                                else if (
596
                                    date <= end_date &&
597
                                    date >= start_date
598
                                ) {
599
                                    // same item, disable date
600
                                    if (
601
                                        booking.item_id &&
602
                                        booking.item_id == booking_item_id
603
                                    ) {
604
                                        return true;
605
                                    }
606
642
607
                                    // count all clashes, both item and biblio level
643
                    // Walk through each day from start to end date
608
                                    booked++;
644
                    while (currentDate.isSameOrBefore(endDate, "day")) {
609
                                    if (booked == bookable) {
645
                        let availableToday = getAvailableItemsOnDate(
610
                                        return true;
646
                            currentDate,
611
                                    }
647
                            itemsOfType
648
                        );
649
                        let availableIds = new Set(
650
                            availableToday.map(item =>
651
                                parseInt(item.item_id, 10)
652
                            )
653
                        );
612
654
613
                                    // FIXME: The above is not intelligent enough to spot
655
                        // Remove items from our pool that are no longer available (never add back)
614
                                    // cases where an item must be used for a biblio level booking
656
                        // Only remove items that are unavailable today, don't re-add previously removed items
615
                                    // due to all other items being booking within the biblio level
657
                        let itemsToRemove = [];
616
                                    // booking period... we end up with a clash
658
                        for (let itemId of availableItems) {
617
                                    // To reproduce:
659
                            if (!availableIds.has(itemId)) {
618
                                    // * One bib with two bookable items.
660
                                itemsToRemove.push(itemId);
619
                                    // * Add item level booking
620
                                    // * Add biblio level booking that extends one day beyond the item level booking
621
                                    // * Try to book the item without an item level booking from the day before the biblio level
622
                                    //   booking is to be returned. Note this is a clash, the only item available for the biblio
623
                                    //   level booking is the item you just booked out overlapping the end date.
624
                                }
625
                            }
661
                            }
626
                        }
662
                        }
663
                        itemsToRemove.forEach(itemId =>
664
                            availableItems.delete(itemId)
665
                        );
666
667
                        // If no items left in the pool, this date is beyond the maximum window
668
                        if (availableItems.size === 0) {
669
                            return false;
670
                        }
671
672
                        // Move to next day
673
                        currentDate = currentDate.add(1, "day");
674
                    }
675
676
                    return true; // Date is within the maximum window
677
                }
678
679
                // Get items of itemtype that are available on a specific date
680
                function getAvailableItemsOnDate(date, itemsOfType) {
681
                    let unavailableItems = new Set();
682
683
                    // Check all existing bookings for conflicts on this date
684
                    for (let booking of bookings) {
685
                        // Skip if we're editing this booking
686
                        if (booking_id && booking_id == booking.booking_id) {
687
                            continue;
688
                        }
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
695
                        if (
696
                            checkDate.isSameOrAfter(start_date, "day") &&
697
                            checkDate.isSameOrBefore(end_date, "day")
698
                        ) {
699
                            // All bookings have item_id, so mark this specific item as unavailable
700
                            // Ensure integer comparison consistency
701
                            unavailableItems.add(parseInt(booking.item_id, 10));
702
                        }
703
                    }
704
705
                    // Return items of our type that are not unavailable
706
                    let available = itemsOfType.filter(
707
                        item =>
708
                            !unavailableItems.has(parseInt(item.item_id, 10))
627
                    );
709
                    );
710
                    return available;
711
                }
712
713
                // Item-specific availability logic for specific item bookings
714
                function isDateDisabledForSpecificItem(date, selectedDates) {
715
                    for (let booking of bookings) {
716
                        // Skip if we're editing this booking
717
                        if (booking_id && booking_id == booking.booking_id) {
718
                            continue;
719
                        }
720
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 (
727
                            checkDate.isSameOrAfter(start_date, "day") &&
728
                            checkDate.isSameOrBefore(end_date, "day")
729
                        ) {
730
                            // Same item, disable date (ensure integer comparison)
731
                            if (
732
                                parseInt(booking.item_id, 10) ===
733
                                parseInt(booking_item_id, 10)
734
                            ) {
735
                                return true;
736
                            }
737
                        }
738
                    }
739
                    return false;
628
                }
740
                }
629
741
630
                // Setup listener for itemtype select2
742
                // Setup listener for itemtype select2
Lines 663-671 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
663
775
664
                // Setup listener for item select2
776
                // Setup listener for item select2
665
                $("#booking_item_id").on("select2:select", function (e) {
777
                $("#booking_item_id").on("select2:select", function (e) {
666
                    booking_item_id = e.params.data.id
778
                    booking_item_id =
667
                        ? e.params.data.id
779
                        e.params.data.id !== undefined &&
668
                        : null;
780
                        e.params.data.id !== null
781
                            ? parseInt(e.params.data.id, 10)
782
                            : 0;
669
783
670
                    // Disable invalid pickup locations
784
                    // Disable invalid pickup locations
671
                    $("#pickup_library_id > option").each(function () {
785
                    $("#pickup_library_id > option").each(function () {
Lines 679-685 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
679
                                .split(",")
793
                                .split(",")
680
                                .map(Number);
794
                                .map(Number);
681
                            if (
795
                            if (
682
                                valid_items.includes(parseInt(booking_item_id))
796
                                valid_items.includes(
797
                                    parseInt(booking_item_id, 10)
798
                                )
683
                            ) {
799
                            ) {
684
                                option.prop("disabled", false);
800
                                option.prop("disabled", false);
685
                            } else {
801
                            } else {
Lines 900-915 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
900
                let bookingsByDate = {};
1016
                let bookingsByDate = {};
901
                // Iterate through the bookings array
1017
                // Iterate through the bookings array
902
                bookings.forEach(booking => {
1018
                bookings.forEach(booking => {
903
                    const start_date = flatpickr.parseDate(booking.start_date);
1019
                    const start_date = dayjs(booking.start_date);
904
                    const end_date = flatpickr.parseDate(booking.end_date);
1020
                    const end_date = dayjs(booking.end_date);
905
                    const item_id = booking.item_id;
1021
                    const item_id = booking.item_id;
906
1022
907
                    // Iterate through each date within the range of start_date and end_date
1023
                    // Iterate through each date within the range of start_date and end_date
908
                    let currentDate = new Date(start_date);
1024
                    let currentDate = dayjs(start_date);
909
                    while (currentDate <= end_date) {
1025
                    while (currentDate.isSameOrBefore(end_date, "day")) {
910
                        const currentDateStr = currentDate
1026
                        const currentDateStr = currentDate.format("YYYY-MM-DD");
911
                            .toISOString()
912
                            .split("T")[0];
913
1027
914
                        // If the date key doesn't exist in the hash, create an empty array for it
1028
                        // If the date key doesn't exist in the hash, create an empty array for it
915
                        if (!bookingsByDate[currentDateStr]) {
1029
                        if (!bookingsByDate[currentDateStr]) {
Lines 920-926 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
920
                        bookingsByDate[currentDateStr].push(item_id);
1034
                        bookingsByDate[currentDateStr].push(item_id);
921
1035
922
                        // Move to the next day
1036
                        // Move to the next day
923
                        currentDate.setDate(currentDate.getDate() + 1);
1037
                        currentDate = currentDate.add(1, "day");
924
                    }
1038
                    }
925
                });
1039
                });
926
1040
Lines 1105-1110 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1105
                setFormValues(
1219
                setFormValues(
1106
                    patron_id,
1220
                    patron_id,
1107
                    booking_item_id,
1221
                    booking_item_id,
1222
                    item_type_id,
1108
                    start_date,
1223
                    start_date,
1109
                    end_date,
1224
                    end_date,
1110
                    periodPicker
1225
                    periodPicker
Lines 1118-1123 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1118
        setFormValues(
1233
        setFormValues(
1119
            patron_id,
1234
            patron_id,
1120
            booking_item_id,
1235
            booking_item_id,
1236
            item_type_id,
1121
            start_date,
1237
            start_date,
1122
            end_date,
1238
            end_date,
1123
            periodPicker
1239
            periodPicker
Lines 1128-1137 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1128
function setFormValues(
1244
function setFormValues(
1129
    patron_id,
1245
    patron_id,
1130
    booking_item_id,
1246
    booking_item_id,
1247
    item_type_id,
1131
    start_date,
1248
    start_date,
1132
    end_date,
1249
    end_date,
1133
    periodPicker
1250
    periodPicker
1134
) {
1251
) {
1252
    // Set itemtype first if provided (needed for edit mode before setting dates)
1253
    if (item_type_id) {
1254
        booking_itemtype_id = item_type_id;
1255
    }
1135
    // If passed patron, pre-select
1256
    // If passed patron, pre-select
1136
    if (patron_id) {
1257
    if (patron_id) {
1137
        let patronSelect = $("#booking_patron_id");
1258
        let patronSelect = $("#booking_patron_id");
Lines 1181-1194 function setFormValues( Link Here
1181
                    },
1302
                    },
1182
                });
1303
                });
1183
            }
1304
            }
1305
1306
            // IMPORTANT: Set dates AFTER item selection completes
1307
            // This ensures booking_itemtype_id is set before dates are validated
1308
            if (start_date) {
1309
                // Allow invalid pre-load so setDate can set date range
1310
                // periodPicker.set('allowInvalidPreload', true);
1311
                // FIXME: Why is this the case.. we're passing two valid Date objects
1312
                let start = new Date(start_date);
1313
                let end = new Date(end_date);
1314
1315
                let dates = [new Date(start_date), new Date(end_date)];
1316
                periodPicker.setDate(dates, true);
1317
            }
1184
        }, 100);
1318
        }, 100);
1185
    }
1319
    }
1186
1320
    // If no item selected but dates provided, set them now
1187
    // Set booking start & end if this is an edit
1321
    else if (start_date) {
1188
    if (start_date) {
1189
        // Allow invalid pre-load so setDate can set date range
1190
        // periodPicker.set('allowInvalidPreload', true);
1191
        // FIXME: Why is this the case.. we're passing two valid Date objects
1192
        let start = new Date(start_date);
1322
        let start = new Date(start_date);
1193
        let end = new Date(end_date);
1323
        let end = new Date(end_date);
1194
1324
Lines 1212-1230 $("#placeBookingForm").on("submit", function (e) { Link Here
1212
    let biblio_id = $("#booking_biblio_id").val();
1342
    let biblio_id = $("#booking_biblio_id").val();
1213
    let item_id = $("#booking_item_id").val();
1343
    let item_id = $("#booking_item_id").val();
1214
1344
1215
    if (!booking_id) {
1345
    // Prepare booking payload
1216
        let posting = $.post(
1346
    let booking_payload = {
1217
            url,
1347
        start_date: start_date,
1218
            JSON.stringify({
1348
        end_date: end_date,
1219
                start_date: start_date,
1349
        pickup_library_id: pickup_library_id,
1220
                end_date: end_date,
1350
        biblio_id: biblio_id,
1221
                pickup_library_id: pickup_library_id,
1351
        patron_id: $("#booking_patron_id").find(":selected").val(),
1222
                biblio_id: biblio_id,
1352
    };
1223
                item_id: item_id != 0 ? item_id : null,
1353
1224
                patron_id: $("#booking_patron_id").find(":selected").val(),
1354
    // If "any item" is selected, determine whether to send item_id or itemtype_id
1225
            })
1355
    if (item_id == 0) {
1356
        // Get items of the selected itemtype that are available for the period
1357
        let itemsOfType = bookable_items.filter(
1358
            item => item.effective_item_type_id === booking_itemtype_id
1226
        );
1359
        );
1227
1360
1361
        let availableItems = itemsOfType.filter(item => {
1362
            return isItemAvailableForPeriod(
1363
                item.item_id,
1364
                new Date(start_date),
1365
                new Date(end_date)
1366
            );
1367
        });
1368
1369
        if (availableItems.length === 0) {
1370
            $("#booking_result").replaceWith(
1371
                '<div id="booking_result" class="alert alert-danger">' +
1372
                    __("No suitable item found for booking") +
1373
                    "</div>"
1374
            );
1375
            return;
1376
        } else if (availableItems.length === 1) {
1377
            // Only one item available - optimization: send specific item_id
1378
            booking_payload.item_id = availableItems[0].item_id;
1379
        } else {
1380
            // Multiple items available - let server choose optimal item
1381
            booking_payload.itemtype_id = booking_itemtype_id;
1382
        }
1383
    } else {
1384
        // Specific item selected
1385
        booking_payload.item_id = item_id;
1386
    }
1387
1388
    if (!booking_id) {
1389
        let posting = $.post(url, JSON.stringify(booking_payload));
1390
1228
        posting.done(function (data) {
1391
        posting.done(function (data) {
1229
            // Update bookings store for subsequent bookings
1392
            // Update bookings store for subsequent bookings
1230
            bookings.push(data);
1393
            bookings.push(data);
Lines 1278-1297 $("#placeBookingForm").on("submit", function (e) { Link Here
1278
            );
1441
            );
1279
        });
1442
        });
1280
    } else {
1443
    } else {
1444
        // For edits with "any item" (item_id == 0), use same hybrid approach as new bookings
1445
        let edit_payload = {
1446
            booking_id: booking_id,
1447
            start_date: start_date,
1448
            end_date: end_date,
1449
            pickup_library_id: pickup_library_id,
1450
            biblio_id: biblio_id,
1451
            patron_id: $("#booking_patron_id").find(":selected").val(),
1452
        };
1453
1454
        if (item_id == 0) {
1455
            // Get items of the selected itemtype that are available for the period
1456
            let itemsOfType = bookable_items.filter(
1457
                item => item.effective_item_type_id === booking_itemtype_id
1458
            );
1459
1460
            let availableItems = itemsOfType.filter(item => {
1461
                return isItemAvailableForPeriod(
1462
                    item.item_id,
1463
                    new Date(start_date),
1464
                    new Date(end_date)
1465
                );
1466
            });
1467
1468
            if (availableItems.length === 0) {
1469
                $("#booking_result").replaceWith(
1470
                    '<div id="booking_result" class="alert alert-danger">' +
1471
                        __("No suitable item found for booking") +
1472
                        "</div>"
1473
                );
1474
                return;
1475
            } else if (availableItems.length === 1) {
1476
                // Only one item available - send specific item_id
1477
                edit_payload.item_id = availableItems[0].item_id;
1478
            } else {
1479
                // Multiple items available - let server choose optimal item
1480
                edit_payload.itemtype_id = booking_itemtype_id;
1481
            }
1482
        } else {
1483
            // Specific item selected
1484
            edit_payload.item_id = item_id;
1485
        }
1486
1281
        url += "/" + booking_id;
1487
        url += "/" + booking_id;
1282
        let putting = $.ajax({
1488
        let putting = $.ajax({
1283
            method: "PUT",
1489
            method: "PUT",
1284
            url: url,
1490
            url: url,
1285
            contentType: "application/json",
1491
            contentType: "application/json",
1286
            data: JSON.stringify({
1492
            data: JSON.stringify(edit_payload),
1287
                booking_id: booking_id,
1288
                start_date: start_date,
1289
                end_date: end_date,
1290
                pickup_library_id: pickup_library_id,
1291
                biblio_id: biblio_id,
1292
                item_id: item_id != 0 ? item_id : null,
1293
                patron_id: $("#booking_patron_id").find(":selected").val(),
1294
            }),
1295
        });
1493
        });
1296
1494
1297
        putting.done(function (data) {
1495
        putting.done(function (data) {
Lines 1357-1363 $("#placeBookingModal").on("hidden.bs.modal", function (e) { Link Here
1357
    booking_patron = undefined;
1555
    booking_patron = undefined;
1358
1556
1359
    // Reset item select
1557
    // Reset item select
1360
    $("#booking_item_id").val(0).trigger("change");
1558
    $("#booking_item_id").val(parseInt(0)).trigger("change");
1361
    $("#booking_item_id").prop("disabled", true);
1559
    $("#booking_item_id").prop("disabled", true);
1362
1560
1363
    // Reset itemtype select
1561
    // Reset itemtype select
1364
- 

Return to bug 40134