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

(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js (-210 / +502 lines)
Lines 93-121 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
93
            let $patron = $("<span></span>")
93
            let $patron = $("<span></span>")
94
                .append(
94
                .append(
95
                    "" +
95
                    "" +
96
                    (patron.surname
96
                        (patron.surname
97
                        ? escape_str(patron.surname) + ", "
97
                            ? escape_str(patron.surname) + ", "
98
                        : "") +
98
                            : "") +
99
                    (patron.firstname
99
                        (patron.firstname
100
                        ? escape_str(patron.firstname) + " "
100
                            ? escape_str(patron.firstname) + " "
101
                        : "") +
101
                            : "") +
102
                    (patron.cardnumber
102
                        (patron.cardnumber
103
                        ? " (" + escape_str(patron.cardnumber) + ")"
103
                            ? " (" + escape_str(patron.cardnumber) + ")"
104
                        : "") +
104
                            : "") +
105
                    "<small>" +
105
                        "<small>" +
106
                    (patron.date_of_birth
106
                        (patron.date_of_birth
107
                        ? ' <span class="age_years">' +
107
                            ? ' <span class="age_years">' +
108
                        $get_age(patron.date_of_birth) +
108
                              $get_age(patron.date_of_birth) +
109
                        " " +
109
                              " " +
110
                        __("years") +
110
                              __("years") +
111
                        "</span>"
111
                              "</span>"
112
                        : "") +
112
                            : "") +
113
                    (patron.library
113
                        (patron.library
114
                        ? ' <span class="ac-library">' +
114
                            ? ' <span class="ac-library">' +
115
                        escape_str(patron.library.name) +
115
                              escape_str(patron.library.name) +
116
                        "</span>"
116
                              "</span>"
117
                        : "") +
117
                            : "") +
118
                    "</small>"
118
                        "</small>"
119
                )
119
                )
120
                .addClass(loggedInClass);
120
                .addClass(loggedInClass);
121
            return $patron;
121
            return $patron;
Lines 212-221 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
212
            ) {
212
            ) {
213
                let option = $(
213
                let option = $(
214
                    '<option value="' +
214
                    '<option value="' +
215
                    pickup_location.library_id +
215
                        pickup_location.library_id +
216
                    '">' +
216
                        '">' +
217
                    pickup_location.name +
217
                        pickup_location.name +
218
                    "</option>"
218
                        "</option>"
219
                );
219
                );
220
220
221
                option.attr(
221
                option.attr(
Lines 414-421 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
414
                    if (
414
                    if (
415
                        !$("#booking_itemtype").find(
415
                        !$("#booking_itemtype").find(
416
                            "option[value='" +
416
                            "option[value='" +
417
                            item.item_type.item_type_id +
417
                                item.item_type.item_type_id +
418
                            "']"
418
                                "']"
419
                        ).length
419
                        ).length
420
                    ) {
420
                    ) {
421
                        // Create a DOM Option and de-select by default
421
                        // Create a DOM Option and de-select by default
Lines 440-454 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
440
                            // set local copy of selectedDates
440
                            // set local copy of selectedDates
441
                            let selectedDates = periodPicker.selectedDates;
441
                            let selectedDates = periodPicker.selectedDates;
442
442
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
443
                            // disable dates before selected date
453
                            if (
444
                            if (
454
                                !selectedDates[1] &&
445
                                !selectedDates[1] &&
Lines 458-616 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
458
                                return true;
449
                                return true;
459
                            }
450
                            }
460
451
461
                            // iterate existing bookings
452
                            // We should always have an itemtype selected and either specific item or "any item"
462
                            for (let booking of bookings) {
453
                            if (!booking_itemtype_id) {
463
                                // Skip if we're editing this booking
454
                                return true; // No itemtype selected, disable everything
464
                                if (
455
                            }
465
                                    booking_id &&
466
                                    booking_id == booking.booking_id
467
                                ) {
468
                                    continue;
469
                                }
470
456
471
                                let start_date = flatpickr.parseDate(
457
                            // If "any item of itemtype" is selected, use smart window maximization
472
                                    booking.start_date
458
                            if (!booking_item_id) {
459
                                return isDateDisabledForItemtype(
460
                                    date,
461
                                    selectedDates
473
                                );
462
                                );
474
                                let end_date = flatpickr.parseDate(
463
                            }
475
                                    booking.end_date
464
                            // If specific item is selected, use item-specific logic
465
                            else {
466
                                return isDateDisabledForSpecificItem(
467
                                    date,
468
                                    selectedDates
476
                                );
469
                                );
470
                            }
471
                        }
472
                    );
473
                }
477
474
478
                                // patron has selected a start date (end date checks)
475
                /**
479
                                if (selectedDates[0]) {
476
                 * SMART ITEMTYPE AVAILABILITY CALCULATION
480
                                    // new booking start date is between existing booking start and end dates
477
                 * For "any item of type X" bookings with dynamic item pool reduction
481
                                    if (
478
                 *
482
                                        selectedDates[0] >= start_date &&
479
                 * ALGORITHM OVERVIEW:
483
                                        selectedDates[0] <= end_date
480
                 * This function implements smart window maximization for itemtype bookings by using
484
                                    ) {
481
                 * dynamic item pool reduction. The core principle is "never re-add items to pool" -
485
                                        if (booking.item_id) {
482
                 * once an item is removed because it becomes unavailable, it's never re-added even
486
                                            if (
483
                 * if it becomes available again later. This ensures optimal resource allocation.
487
                                                unavailable_items.indexOf(
484
                 *
488
                                                    booking.item_id
485
                 * FLOW:
489
                                                ) === -1
486
                 * 1. For start date selection: Disable if ALL items of itemtype are booked
490
                                            ) {
487
                 * 2. For end date selection: Use smart window maximization algorithm
491
                                                unavailable_items.push(
488
                 *
492
                                                    booking.item_id
489
                 * SMART WINDOW MAXIMIZATION:
493
                                                );
490
                 * - Start with items available on the selected start date
494
                                            }
491
                 * - Walk through each day from start to target end date
495
                                        } else {
492
                 * - Remove items from pool when they become unavailable
496
                                            if (
493
                 * - NEVER re-add items even if they become available again later
497
                                                biblio_bookings.indexOf(
494
                 * - Disable date when no items remain in pool
498
                                                    booking.booking_id
495
                 *
499
                                                ) === -1
496
                 * EXAMPLE:
500
                                            ) {
497
                 * Items: A, B, C
501
                                                biblio_bookings.push(
498
                 * A available: days 1-5, booked 6-10, available again 11+
502
                                                    booking.booking_id
499
                 * B available: days 1-8, booked 9-15, available again 16+
503
                                                );
500
                 * C available: days 1-12, booked 13-20, available again 21+
504
                                            }
501
                 *
505
                                        }
502
                 * Start day 3:
506
                                    }
503
                 * - Initial pool: A, B, C
504
                 * - Days 3-5: Pool A, B, C (all available)
505
                 * - Day 6: Remove A (becomes booked), Pool now B, C
506
                 * - Day 9: Remove B (becomes booked), Pool now C
507
                 * - Day 13: Remove C (becomes booked), Pool now EMPTY → disable dates
508
                 * - Result: Can book days 3-12, day 13+ disabled
509
                 * - Note: A becomes available on day 11 but is NOT re-added to pool
510
                 *
511
                 * @param {Date} date - The date being checked for availability
512
                 * @param {Array} selectedDates - Array of selected dates from flatpickr [startDate, endDate?]
513
                 * @returns {boolean} - True if date should be disabled, false if available
514
                 */
515
                function isDateDisabledForItemtype(date, selectedDates) {
516
                    // Get items of the selected itemtype
517
                    let itemsOfType = bookable_items.filter(
518
                        item =>
519
                            item.effective_item_type_id === booking_itemtype_id
520
                    );
507
521
508
                                    // new booking end date would be between existing booking start and end dates
522
                    // For start date selection: disable if ALL items of itemtype are booked on this date
509
                                    else if (
523
                    if (!selectedDates[0]) {
510
                                        date >= start_date &&
524
                        return (
511
                                        date <= end_date
525
                            getAvailableItemsOnDate(date, itemsOfType)
512
                                    ) {
526
                                .length === 0
513
                                        if (booking.item_id) {
527
                        );
514
                                            if (
528
                    }
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
529
536
                                    // new booking would span existing booking
530
                    // For end date selection: use smart window maximization
537
                                    else if (
531
                    if (selectedDates[0] && !selectedDates[1]) {
538
                                        selectedDates[0] <= start_date &&
532
                        let result = !isDateInMaximumWindow(
539
                                        date >= end_date
533
                            selectedDates[0],
540
                                    ) {
534
                            date,
541
                                        if (booking.item_id) {
535
                            itemsOfType
542
                                            if (
536
                        );
543
                                                unavailable_items.indexOf(
537
                        return result;
544
                                                    booking.item_id
538
                    }
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
539
564
                                    // new booking would not conflict
540
                    return false;
565
                                    else {
541
                }
566
                                        continue;
567
                                    }
568
542
569
                                    // check that there are available items
543
                /**
570
                                    // available = all bookable items - booked items - booked biblios
544
                 * MAXIMUM BOOKING WINDOW CALCULATION ALGORITHM
571
                                    let total_available =
545
                 * Core Implementation of "Never Re-add Items to Pool" Principle
572
                                        bookable_items.length -
546
                 *
573
                                        unavailable_items.length -
547
                 * PURPOSE:
574
                                        biblio_bookings.length;
548
                 * Calculate the maximum possible booking window for "any item of itemtype X" bookings
575
                                    if (total_available === 0) {
549
                 * by dynamically reducing the available item pool as items become unavailable.
576
                                        return true;
550
                 *
577
                                    }
551
                 * CORE ALGORITHM: "Never Re-add Items to Pool"
578
                                }
552
                 * 1. Start with items available on the selected start date ONLY
553
                 * 2. Walk through each day from start to target end date
554
                 * 3. Remove items from pool when they become unavailable (booking starts)
555
                 * 4. NEVER re-add items even if they become available again later (booking ends)
556
                 * 5. Return false (disable date) when no items remain in pool
557
                 *
558
                 * WHY THIS WORKS:
559
                 * - Maximizes booking windows by ensuring optimal resource allocation
560
                 * - Prevents booking conflicts by being conservative about item availability
561
                 * - Ensures that if a booking can start on date X, there will always be an
562
                 *   item available for the entire duration (no conflicts)
563
                 *
564
                 * DETAILED EXAMPLE:
565
                 * Items: TABLET001, TABLET002, TABLET003
566
                 * TABLET001: Available 1-9, Booked 10-15, Available 16+
567
                 * TABLET002: Available 1-12, Booked 13-20, Available 21+
568
                 * TABLET003: Available 1-17, Booked 18-25, Available 26+
569
                 *
570
                 * Testing: Can we book from day 5 to day 20?
571
                 *
572
                 * Step 1: Day 5 (start) - Initial pool: {TABLET001, TABLET002, TABLET003}
573
                 * Step 2: Day 6-9 - All items available, pool unchanged
574
                 * Step 3: Day 10 - TABLET001 becomes unavailable → Remove from pool
575
                 *         Pool now: {TABLET002, TABLET003}
576
                 * Step 4: Day 11-12 - Remaining items available, pool unchanged
577
                 * Step 5: Day 13 - TABLET002 becomes unavailable → Remove from pool
578
                 *         Pool now: {TABLET003}
579
                 * Step 6: Day 14-17 - TABLET003 available, pool unchanged
580
                 * Step 7: Day 18 - TABLET003 becomes unavailable → Remove from pool
581
                 *         Pool now: {} (empty)
582
                 * Step 8: Pool is empty → Return false (cannot book to day 20)
583
                 *
584
                 * Result: Can book from day 5 to day 17, but NOT to day 18+
585
                 *
586
                 * CRITICAL NOTE: Even though TABLET001 becomes available again on day 16,
587
                 * it is NOT re-added to the pool. This is the key principle that ensures
588
                 * booking reliability and optimal resource allocation.
589
                 *
590
                 * PERFORMANCE: O(n × d) where n = items of type, d = days in range
591
                 *
592
                 * @param {Date} startDate - Selected start date from flatpickr
593
                 * @param {Date} endDate - Target end date being checked for availability
594
                 * @param {Array} itemsOfType - Items of the selected itemtype
595
                 * @returns {boolean} - True if date is within maximum window, false if beyond
596
                 */
597
                function isDateInMaximumWindow(
598
                    startDate,
599
                    endDate,
600
                    itemsOfType
601
                ) {
602
                    // Start with only items available on the start date - never add items back
603
                    let availableOnStart = getAvailableItemsOnDate(
604
                        startDate,
605
                        itemsOfType
606
                    );
607
                    let availableItems = new Set(
608
                        availableOnStart.map(item => parseInt(item.item_id, 10))
609
                    );
579
610
580
                                // patron has not yet selected a start date (start date checks)
611
                    let currentDate = dayjs(startDate);
581
                                else if (
582
                                    date <= end_date &&
583
                                    date >= start_date
584
                                ) {
585
                                    // same item, disable date
586
                                    if (
587
                                        booking.item_id &&
588
                                        booking.item_id == booking_item_id
589
                                    ) {
590
                                        return true;
591
                                    }
592
612
593
                                    // count all clashes, both item and biblio level
613
                    // Walk through each day from start to end date
594
                                    booked++;
614
                    while (currentDate.isSameOrBefore(endDate, "day")) {
595
                                    if (booked == bookable) {
615
                        let availableToday = getAvailableItemsOnDate(
596
                                        return true;
616
                            currentDate,
597
                                    }
617
                            itemsOfType
618
                        );
619
                        let availableIds = new Set(
620
                            availableToday.map(item =>
621
                                parseInt(item.item_id, 10)
622
                            )
623
                        );
598
624
599
                                    // FIXME: The above is not intelligent enough to spot
625
                        // Remove items from our pool that are no longer available (never add back)
600
                                    // cases where an item must be used for a biblio level booking
626
                        // Only remove items that are unavailable today, don't re-add previously removed items
601
                                    // due to all other items being booking within the biblio level
627
                        let itemsToRemove = [];
602
                                    // booking period... we end up with a clash
628
                        for (let itemId of availableItems) {
603
                                    // To reproduce:
629
                            if (!availableIds.has(itemId)) {
604
                                    // * One bib with two bookable items.
630
                                itemsToRemove.push(itemId);
605
                                    // * Add item level booking
606
                                    // * Add biblio level booking that extends one day beyond the item level booking
607
                                    // * Try to book the item without an item level booking from the day before the biblio level
608
                                    //   booking is to be returned. Note this is a clash, the only item available for the biblio
609
                                    //   level booking is the item you just booked out overlapping the end date.
610
                                }
611
                            }
631
                            }
612
                        }
632
                        }
633
                        itemsToRemove.forEach(itemId =>
634
                            availableItems.delete(itemId)
635
                        );
636
637
                        // If no items left in the pool, this date is beyond the maximum window
638
                        if (availableItems.size === 0) {
639
                            return false;
640
                        }
641
642
                        // Move to next day
643
                        currentDate = currentDate.add(1, "day");
644
                    }
645
646
                    return true; // Date is within the maximum window
647
                }
648
649
                // Get items of itemtype that are available on a specific date
650
                function getAvailableItemsOnDate(date, itemsOfType) {
651
                    let unavailableItems = new Set();
652
653
                    // Check all existing bookings for conflicts on this date
654
                    for (let booking of bookings) {
655
                        // Skip if we're editing this booking
656
                        if (booking_id && booking_id == booking.booking_id) {
657
                            continue;
658
                        }
659
660
                        let start_date = dayjs(booking.start_date);
661
                        let end_date = dayjs(booking.end_date);
662
                        let checkDate = dayjs(date);
663
664
                        // Check if this date falls within this booking period
665
                        if (
666
                            checkDate.isSameOrAfter(start_date, "day") &&
667
                            checkDate.isSameOrBefore(end_date, "day")
668
                        ) {
669
                            // All bookings have item_id, so mark this specific item as unavailable
670
                            // Ensure integer comparison consistency
671
                            unavailableItems.add(parseInt(booking.item_id, 10));
672
                        }
673
                    }
674
675
                    // Return items of our type that are not unavailable
676
                    let available = itemsOfType.filter(
677
                        item =>
678
                            !unavailableItems.has(parseInt(item.item_id, 10))
613
                    );
679
                    );
680
                    return available;
681
                }
682
683
                // Item-specific availability logic for specific item bookings
684
                function isDateDisabledForSpecificItem(date, selectedDates) {
685
                    for (let booking of bookings) {
686
                        // Skip if we're editing this booking
687
                        if (booking_id && booking_id == booking.booking_id) {
688
                            continue;
689
                        }
690
691
                        let start_date = dayjs(booking.start_date);
692
                        let end_date = dayjs(booking.end_date);
693
                        let checkDate = dayjs(date);
694
695
                        // Check if this booking conflicts with our selected item and date
696
                        if (
697
                            checkDate.isSameOrAfter(start_date, "day") &&
698
                            checkDate.isSameOrBefore(end_date, "day")
699
                        ) {
700
                            // Same item, disable date (ensure integer comparison)
701
                            if (
702
                                parseInt(booking.item_id, 10) ===
703
                                parseInt(booking_item_id, 10)
704
                            ) {
705
                                return true;
706
                            }
707
                        }
708
                    }
709
                    return false;
710
                }
711
712
                /**
713
                 * OPTIMAL ITEM SELECTION FOR "ANY ITEM" BOOKINGS
714
                 *
715
                 * PURPOSE:
716
                 * When submitting an "any item of itemtype X" booking, select the specific item
717
                 * that provides the best future availability for subsequent bookings.
718
                 *
719
                 * SELECTION CRITERIA:
720
                 * 1. Item must be available for the entire requested booking period
721
                 * 2. Among available items, choose the one with longest future availability
722
                 * 3. This maximizes opportunities for future bookings by preserving
723
                 *    items with shorter future availability for those bookings
724
                 *
725
                 * ALGORITHM:
726
                 * 1. Filter items of the specified type
727
                 * 2. Find items available for the entire booking period
728
                 * 3. For each available item, calculate future availability after booking ends
729
                 * 4. Select item with longest future availability
730
                 * 5. If tie, select first item (arbitrary but consistent)
731
                 *
732
                 * EXAMPLE:
733
                 * Booking period: Days 5-10
734
                 * TABLET001: Available 5-10, then booked 11-15, then free 16+
735
                 * TABLET002: Available 5-10, then free until 25, then booked 26+
736
                 * TABLET003: Available 5-10, then booked 11-30
737
                 *
738
                 * Future availability calculations (after day 10):
739
                 * TABLET001: 0 days (booked immediately after)
740
                 * TABLET002: 15 days (free until day 25)
741
                 * TABLET003: 0 days (booked immediately after)
742
                 *
743
                 * Result: Select TABLET002 (longest future availability)
744
                 * Benefit: Preserves TABLET001 for bookings that might need days 11-15
745
                 *
746
                 * @param {string} itemTypeId - The itemtype ID to select from
747
                 * @param {Date} startDate - Booking start date
748
                 * @param {Date} endDate - Booking end date
749
                 * @returns {string|null} - Selected item ID, or null if none available
750
                 */
751
                function selectOptimalItem(itemTypeId, startDate, endDate) {
752
                    // Get items of the selected itemtype
753
                    let itemsOfType = bookable_items.filter(
754
                        item => item.effective_item_type_id === itemTypeId
755
                    );
756
757
                    // Find items available for the entire booking period
758
                    let availableItems = itemsOfType.filter(item => {
759
                        return isItemAvailableForPeriod(
760
                            item.item_id,
761
                            startDate,
762
                            endDate
763
                        );
764
                    });
765
766
                    if (availableItems.length === 0) {
767
                        return null; // No items available
768
                    }
769
770
                    if (availableItems.length === 1) {
771
                        return availableItems[0].item_id; // Only one choice
772
                    }
773
774
                    // Calculate future availability for each item and choose the best one
775
                    let bestItem = null;
776
                    let longestFutureAvailability = -1;
777
778
                    for (let item of availableItems) {
779
                        let futureAvailability = calculateFutureAvailability(
780
                            item.item_id,
781
                            endDate
782
                        );
783
784
                        if (futureAvailability > longestFutureAvailability) {
785
                            longestFutureAvailability = futureAvailability;
786
                            bestItem = item;
787
                        }
788
                    }
789
790
                    return bestItem
791
                        ? bestItem.item_id
792
                        : availableItems[0].item_id;
793
                }
794
795
                // Check if a specific item is available for the entire booking period
796
                function isItemAvailableForPeriod(itemId, startDate, endDate) {
797
                    for (let booking of bookings) {
798
                        // Skip if we're editing this booking
799
                        if (booking_id && booking_id == booking.booking_id) {
800
                            continue;
801
                        }
802
803
                        if (booking.item_id !== itemId) {
804
                            continue; // Different item, no conflict
805
                        }
806
807
                        let booking_start = dayjs(booking.start_date);
808
                        let booking_end = dayjs(booking.end_date);
809
                        let checkStartDate = dayjs(startDate);
810
                        let checkEndDate = dayjs(endDate);
811
812
                        // Check for any overlap with our booking period
813
                        if (
814
                            !(
815
                                checkEndDate.isBefore(booking_start, "day") ||
816
                                checkStartDate.isAfter(booking_end, "day")
817
                            )
818
                        ) {
819
                            return false; // Overlap detected
820
                        }
821
                    }
822
                    return true; // No conflicts found
823
                }
824
825
                // Calculate how many days an item is available after the booking end date
826
                function calculateFutureAvailability(itemId, bookingEndDate) {
827
                    let daysAvailable = 0;
828
                    let checkDate = dayjs(bookingEndDate).add(1, "day"); // Start day after booking ends
829
830
                    // Check availability for next 365 days (or until we hit a booking)
831
                    for (let i = 0; i < 365; i++) {
832
                        let isAvailable = true;
833
834
                        for (let booking of bookings) {
835
                            // Skip if we're editing this booking
836
                            if (
837
                                booking_id &&
838
                                booking_id == booking.booking_id
839
                            ) {
840
                                continue;
841
                            }
842
843
                            if (booking.item_id !== itemId) {
844
                                continue; // Different item
845
                            }
846
847
                            let booking_start = dayjs(booking.start_date);
848
                            let booking_end = dayjs(booking.end_date);
849
850
                            // Check if this date is within an existing booking
851
                            if (
852
                                checkDate.isSameOrAfter(booking_start, "day") &&
853
                                checkDate.isSameOrBefore(booking_end, "day")
854
                            ) {
855
                                isAvailable = false;
856
                                break;
857
                            }
858
                        }
859
860
                        if (!isAvailable) {
861
                            break; // Hit a booking, stop counting
862
                        }
863
864
                        daysAvailable++;
865
                        checkDate = checkDate.add(1, "day");
866
                    }
867
868
                    return daysAvailable;
614
                }
869
                }
615
870
616
                // Setup listener for itemtype select2
871
                // Setup listener for itemtype select2
Lines 649-657 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
649
904
650
                // Setup listener for item select2
905
                // Setup listener for item select2
651
                $("#booking_item_id").on("select2:select", function (e) {
906
                $("#booking_item_id").on("select2:select", function (e) {
652
                    booking_item_id = e.params.data.id
907
                    booking_item_id =
653
                        ? e.params.data.id
908
                        e.params.data.id !== undefined &&
654
                        : null;
909
                        e.params.data.id !== null
910
                            ? parseInt(e.params.data.id, 10)
911
                            : 0;
655
912
656
                    // Disable invalid pickup locations
913
                    // Disable invalid pickup locations
657
                    $("#pickup_library_id > option").each(function () {
914
                    $("#pickup_library_id > option").each(function () {
Lines 665-671 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
665
                                .split(",")
922
                                .split(",")
666
                                .map(Number);
923
                                .map(Number);
667
                            if (
924
                            if (
668
                                valid_items.includes(parseInt(booking_item_id))
925
                                valid_items.includes(
926
                                    parseInt(booking_item_id, 10)
927
                                )
669
                            ) {
928
                            ) {
670
                                option.prop("disabled", false);
929
                                option.prop("disabled", false);
671
                            } else {
930
                            } else {
Lines 753-759 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
753
                                for (let i = 0; i < renewalsAllowed; i++) {
1012
                                for (let i = 0; i < renewalsAllowed; i++) {
754
                                    nextDate.setDate(
1013
                                    nextDate.setDate(
755
                                        nextDate.getDate() +
1014
                                        nextDate.getDate() +
756
                                        parseInt(renewalLength)
1015
                                            parseInt(renewalLength)
757
                                    );
1016
                                    );
758
                                    boldDates.push(new Date(nextDate));
1017
                                    boldDates.push(new Date(nextDate));
759
                                }
1018
                                }
Lines 864-879 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
864
                let bookingsByDate = {};
1123
                let bookingsByDate = {};
865
                // Iterate through the bookings array
1124
                // Iterate through the bookings array
866
                bookings.forEach(booking => {
1125
                bookings.forEach(booking => {
867
                    const start_date = flatpickr.parseDate(booking.start_date);
1126
                    const start_date = dayjs(booking.start_date);
868
                    const end_date = flatpickr.parseDate(booking.end_date);
1127
                    const end_date = dayjs(booking.end_date);
869
                    const item_id = booking.item_id;
1128
                    const item_id = booking.item_id;
870
1129
871
                    // Iterate through each date within the range of start_date and end_date
1130
                    // Iterate through each date within the range of start_date and end_date
872
                    let currentDate = new Date(start_date);
1131
                    let currentDate = dayjs(start_date);
873
                    while (currentDate <= end_date) {
1132
                    while (currentDate.isSameOrBefore(end_date, "day")) {
874
                        const currentDateStr = currentDate
1133
                        const currentDateStr = currentDate.format("YYYY-MM-DD");
875
                            .toISOString()
876
                            .split("T")[0];
877
1134
878
                        // If the date key doesn't exist in the hash, create an empty array for it
1135
                        // If the date key doesn't exist in the hash, create an empty array for it
879
                        if (!bookingsByDate[currentDateStr]) {
1136
                        if (!bookingsByDate[currentDateStr]) {
Lines 884-890 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
884
                        bookingsByDate[currentDateStr].push(item_id);
1141
                        bookingsByDate[currentDateStr].push(item_id);
885
1142
886
                        // Move to the next day
1143
                        // Move to the next day
887
                        currentDate.setDate(currentDate.getDate() + 1);
1144
                        currentDate = currentDate.add(1, "day");
888
                    }
1145
                    }
889
                });
1146
                });
890
1147
Lines 933-940 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
933
                            );
1190
                            );
934
                            const startDate = periodPicker.selectedDates[0]
1191
                            const startDate = periodPicker.selectedDates[0]
935
                                ? dayjs(periodPicker.selectedDates[0]).startOf(
1192
                                ? dayjs(periodPicker.selectedDates[0]).startOf(
936
                                    "day"
1193
                                      "day"
937
                                )
1194
                                  )
938
                                : null;
1195
                                : null;
939
1196
940
                            const leadStart = startDate
1197
                            const leadStart = startDate
Lines 960-966 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
960
                                    dayElem.classList.toggle(
1217
                                    dayElem.classList.toggle(
961
                                        "leadRange",
1218
                                        "leadRange",
962
                                        elemDate.isSameOrAfter(leadStart) &&
1219
                                        elemDate.isSameOrAfter(leadStart) &&
963
                                        elemDate.isBefore(leadEnd)
1220
                                            elemDate.isBefore(leadEnd)
964
                                    );
1221
                                    );
965
                                    dayElem.classList.toggle(
1222
                                    dayElem.classList.toggle(
966
                                        "leadRangeEnd",
1223
                                        "leadRangeEnd",
Lines 973-979 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
973
                                    dayElem.classList.toggle(
1230
                                    dayElem.classList.toggle(
974
                                        "trailRange",
1231
                                        "trailRange",
975
                                        elemDate.isAfter(trailStart) &&
1232
                                        elemDate.isAfter(trailStart) &&
976
                                        elemDate.isSameOrBefore(trailEnd)
1233
                                            elemDate.isSameOrBefore(trailEnd)
977
                                    );
1234
                                    );
978
                                    dayElem.classList.toggle(
1235
                                    dayElem.classList.toggle(
979
                                        "trailRangeEnd",
1236
                                        "trailRangeEnd",
Lines 1116-1122 function setFormValues( Link Here
1116
    // If passed an itemnumber, pre-select
1373
    // If passed an itemnumber, pre-select
1117
    if (booking_item_id) {
1374
    if (booking_item_id) {
1118
        // Wait a bit for the item options to be fully created with data attributes
1375
        // Wait a bit for the item options to be fully created with data attributes
1119
        setTimeout(function() {
1376
        setTimeout(function () {
1120
            $("#booking_item_id").val(booking_item_id).trigger("change");
1377
            $("#booking_item_id").val(booking_item_id).trigger("change");
1121
            // Also trigger the select2:select event with proper data
1378
            // Also trigger the select2:select event with proper data
1122
            let selectedOption = $("#booking_item_id option:selected")[0];
1379
            let selectedOption = $("#booking_item_id option:selected")[0];
Lines 1126-1134 function setFormValues( Link Here
1126
                    params: {
1383
                    params: {
1127
                        data: {
1384
                        data: {
1128
                            id: booking_item_id,
1385
                            id: booking_item_id,
1129
                            element: selectedOption
1386
                            element: selectedOption,
1130
                        }
1387
                        },
1131
                    }
1388
                    },
1132
                });
1389
                });
1133
            }
1390
            }
1134
        }, 100);
1391
        }, 100);
Lines 1162-1167 $("#placeBookingForm").on("submit", function (e) { Link Here
1162
    let biblio_id = $("#booking_biblio_id").val();
1419
    let biblio_id = $("#booking_biblio_id").val();
1163
    let item_id = $("#booking_item_id").val();
1420
    let item_id = $("#booking_item_id").val();
1164
1421
1422
    // If "any item" is selected, find the optimal item
1423
    if (item_id == 0) {
1424
        item_id = selectOptimalItem(
1425
            booking_itemtype_id,
1426
            new Date(start_date),
1427
            new Date(end_date)
1428
        );
1429
1430
        if (!item_id) {
1431
            $("#booking_result").replaceWith(
1432
                '<div id="booking_result" class="alert alert-danger">' +
1433
                    __("No suitable item found for booking") +
1434
                    "</div>"
1435
            );
1436
            return;
1437
        }
1438
    }
1439
1165
    if (!booking_id) {
1440
    if (!booking_id) {
1166
        let posting = $.post(
1441
        let posting = $.post(
1167
            url,
1442
            url,
Lines 1170-1176 $("#placeBookingForm").on("submit", function (e) { Link Here
1170
                end_date: end_date,
1445
                end_date: end_date,
1171
                pickup_library_id: pickup_library_id,
1446
                pickup_library_id: pickup_library_id,
1172
                biblio_id: biblio_id,
1447
                biblio_id: biblio_id,
1173
                item_id: item_id != 0 ? item_id : null,
1448
                item_id: item_id,
1174
                patron_id: $("#booking_patron_id").find(":selected").val(),
1449
                patron_id: $("#booking_patron_id").find(":selected").val(),
1175
            })
1450
            })
1176
        );
1451
        );
Lines 1212-1219 $("#placeBookingForm").on("submit", function (e) { Link Here
1212
            // Set feedback
1487
            // Set feedback
1213
            $("#transient_result").replaceWith(
1488
            $("#transient_result").replaceWith(
1214
                '<div id="transient_result" class="alert alert-info">' +
1489
                '<div id="transient_result" class="alert alert-info">' +
1215
                __("Booking successfully placed") +
1490
                    __("Booking successfully placed") +
1216
                "</div>"
1491
                    "</div>"
1217
            );
1492
            );
1218
1493
1219
            // Close modal
1494
            // Close modal
Lines 1223-1233 $("#placeBookingForm").on("submit", function (e) { Link Here
1223
        posting.fail(function (data) {
1498
        posting.fail(function (data) {
1224
            $("#booking_result").replaceWith(
1499
            $("#booking_result").replaceWith(
1225
                '<div id="booking_result" class="alert alert-danger">' +
1500
                '<div id="booking_result" class="alert alert-danger">' +
1226
                __("Failure") +
1501
                    __("Failure") +
1227
                "</div>"
1502
                    "</div>"
1228
            );
1503
            );
1229
        });
1504
        });
1230
    } else {
1505
    } else {
1506
        // For edits, also handle optimal item selection
1507
        if (item_id == 0) {
1508
            item_id = selectOptimalItem(
1509
                booking_itemtype_id,
1510
                new Date(start_date),
1511
                new Date(end_date)
1512
            );
1513
1514
            if (!item_id) {
1515
                $("#booking_result").replaceWith(
1516
                    '<div id="booking_result" class="alert alert-danger">' +
1517
                        __("No suitable item found for booking") +
1518
                        "</div>"
1519
                );
1520
                return;
1521
            }
1522
        }
1523
1231
        url += "/" + booking_id;
1524
        url += "/" + booking_id;
1232
        let putting = $.ajax({
1525
        let putting = $.ajax({
1233
            method: "PUT",
1526
            method: "PUT",
Lines 1239-1245 $("#placeBookingForm").on("submit", function (e) { Link Here
1239
                end_date: end_date,
1532
                end_date: end_date,
1240
                pickup_library_id: pickup_library_id,
1533
                pickup_library_id: pickup_library_id,
1241
                biblio_id: biblio_id,
1534
                biblio_id: biblio_id,
1242
                item_id: item_id != 0 ? item_id : null,
1535
                item_id: item_id,
1243
                patron_id: $("#booking_patron_id").find(":selected").val(),
1536
                patron_id: $("#booking_patron_id").find(":selected").val(),
1244
            }),
1537
            }),
1245
        });
1538
        });
Lines 1281-1288 $("#placeBookingForm").on("submit", function (e) { Link Here
1281
            // Set feedback
1574
            // Set feedback
1282
            $("#transient_result").replaceWith(
1575
            $("#transient_result").replaceWith(
1283
                '<div id="transient_result" class="alert alert-info">' +
1576
                '<div id="transient_result" class="alert alert-info">' +
1284
                __("Booking successfully updated") +
1577
                    __("Booking successfully updated") +
1285
                "</div>"
1578
                    "</div>"
1286
            );
1579
            );
1287
1580
1288
            // Close modal
1581
            // Close modal
Lines 1292-1299 $("#placeBookingForm").on("submit", function (e) { Link Here
1292
        putting.fail(function (data) {
1585
        putting.fail(function (data) {
1293
            $("#booking_result").replaceWith(
1586
            $("#booking_result").replaceWith(
1294
                '<div id="booking_result" class="alert alert-danger">' +
1587
                '<div id="booking_result" class="alert alert-danger">' +
1295
                __("Failure") +
1588
                    __("Failure") +
1296
                "</div>"
1589
                    "</div>"
1297
            );
1590
            );
1298
        });
1591
        });
1299
    }
1592
    }
Lines 1307-1313 $("#placeBookingModal").on("hidden.bs.modal", function (e) { Link Here
1307
    booking_patron = undefined;
1600
    booking_patron = undefined;
1308
1601
1309
    // Reset item select
1602
    // Reset item select
1310
    $("#booking_item_id").val(0).trigger("change");
1603
    $("#booking_item_id").val(parseInt(0)).trigger("change");
1311
    $("#booking_item_id").prop("disabled", true);
1604
    $("#booking_item_id").prop("disabled", true);
1312
1605
1313
    // Reset itemtype select
1606
    // Reset itemtype select
1314
- 

Return to bug 40134