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

(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js (-188 / +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-616 $("#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 that there are available items
573
                /**
570
                                    // available = all bookable items - booked items - booked biblios
574
                 * MAXIMUM BOOKING WINDOW CALCULATION ALGORITHM
571
                                    let total_available =
575
                 * Core Implementation of "Never Re-add Items to Pool" Principle
572
                                        bookable_items.length -
576
                 *
573
                                        unavailable_items.length -
577
                 * PURPOSE:
574
                                        biblio_bookings.length;
578
                 * Calculate the maximum possible booking window for "any item of itemtype X" bookings
575
                                    if (total_available === 0) {
579
                 * by dynamically reducing the available item pool as items become unavailable.
576
                                        return true;
580
                 *
577
                                    }
581
                 * CORE ALGORITHM: "Never Re-add Items to Pool"
578
                                }
582
                 * 1. Start with items available on the selected start date ONLY
583
                 * 2. Walk through each day from start to target end date
584
                 * 3. Remove items from pool when they become unavailable (booking starts)
585
                 * 4. NEVER re-add items even if they become available again later (booking ends)
586
                 * 5. Return false (disable date) when no items remain in pool
587
                 *
588
                 * WHY THIS WORKS:
589
                 * - Maximizes booking windows by ensuring optimal resource allocation
590
                 * - Prevents booking conflicts by being conservative about item availability
591
                 * - Ensures that if a booking can start on date X, there will always be an
592
                 *   item available for the entire duration (no conflicts)
593
                 *
594
                 * DETAILED EXAMPLE:
595
                 * Items: TABLET001, TABLET002, TABLET003
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
                    );
579
640
580
                                // patron has not yet selected a start date (start date checks)
641
                    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
642
593
                                    // count all clashes, both item and biblio level
643
                    // Walk through each day from start to end date
594
                                    booked++;
644
                    while (currentDate.isSameOrBefore(endDate, "day")) {
595
                                    if (booked == bookable) {
645
                        let availableToday = getAvailableItemsOnDate(
596
                                        return true;
646
                            currentDate,
597
                                    }
647
                            itemsOfType
648
                        );
649
                        let availableIds = new Set(
650
                            availableToday.map(item =>
651
                                parseInt(item.item_id, 10)
652
                            )
653
                        );
598
654
599
                                    // FIXME: The above is not intelligent enough to spot
655
                        // 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
656
                        // 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
657
                        let itemsToRemove = [];
602
                                    // booking period... we end up with a clash
658
                        for (let itemId of availableItems) {
603
                                    // To reproduce:
659
                            if (!availableIds.has(itemId)) {
604
                                    // * One bib with two bookable items.
660
                                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
                            }
661
                            }
612
                        }
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))
613
                    );
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;
614
                }
740
                }
615
741
616
                // Setup listener for itemtype select2
742
                // Setup listener for itemtype select2
Lines 649-657 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
649
775
650
                // Setup listener for item select2
776
                // Setup listener for item select2
651
                $("#booking_item_id").on("select2:select", function (e) {
777
                $("#booking_item_id").on("select2:select", function (e) {
652
                    booking_item_id = e.params.data.id
778
                    booking_item_id =
653
                        ? e.params.data.id
779
                        e.params.data.id !== undefined &&
654
                        : null;
780
                        e.params.data.id !== null
781
                            ? parseInt(e.params.data.id, 10)
782
                            : 0;
655
783
656
                    // Disable invalid pickup locations
784
                    // Disable invalid pickup locations
657
                    $("#pickup_library_id > option").each(function () {
785
                    $("#pickup_library_id > option").each(function () {
Lines 665-671 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
665
                                .split(",")
793
                                .split(",")
666
                                .map(Number);
794
                                .map(Number);
667
                            if (
795
                            if (
668
                                valid_items.includes(parseInt(booking_item_id))
796
                                valid_items.includes(
797
                                    parseInt(booking_item_id, 10)
798
                                )
669
                            ) {
799
                            ) {
670
                                option.prop("disabled", false);
800
                                option.prop("disabled", false);
671
                            } else {
801
                            } else {
Lines 864-879 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
864
                let bookingsByDate = {};
994
                let bookingsByDate = {};
865
                // Iterate through the bookings array
995
                // Iterate through the bookings array
866
                bookings.forEach(booking => {
996
                bookings.forEach(booking => {
867
                    const start_date = flatpickr.parseDate(booking.start_date);
997
                    const start_date = dayjs(booking.start_date);
868
                    const end_date = flatpickr.parseDate(booking.end_date);
998
                    const end_date = dayjs(booking.end_date);
869
                    const item_id = booking.item_id;
999
                    const item_id = booking.item_id;
870
1000
871
                    // Iterate through each date within the range of start_date and end_date
1001
                    // Iterate through each date within the range of start_date and end_date
872
                    let currentDate = new Date(start_date);
1002
                    let currentDate = dayjs(start_date);
873
                    while (currentDate <= end_date) {
1003
                    while (currentDate.isSameOrBefore(end_date, "day")) {
874
                        const currentDateStr = currentDate
1004
                        const currentDateStr = currentDate.format("YYYY-MM-DD");
875
                            .toISOString()
876
                            .split("T")[0];
877
1005
878
                        // If the date key doesn't exist in the hash, create an empty array for it
1006
                        // If the date key doesn't exist in the hash, create an empty array for it
879
                        if (!bookingsByDate[currentDateStr]) {
1007
                        if (!bookingsByDate[currentDateStr]) {
Lines 884-890 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
884
                        bookingsByDate[currentDateStr].push(item_id);
1012
                        bookingsByDate[currentDateStr].push(item_id);
885
1013
886
                        // Move to the next day
1014
                        // Move to the next day
887
                        currentDate.setDate(currentDate.getDate() + 1);
1015
                        currentDate = currentDate.add(1, "day");
888
                    }
1016
                    }
889
                });
1017
                });
890
1018
Lines 1055-1060 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1055
                setFormValues(
1183
                setFormValues(
1056
                    patron_id,
1184
                    patron_id,
1057
                    booking_item_id,
1185
                    booking_item_id,
1186
                    item_type_id,
1058
                    start_date,
1187
                    start_date,
1059
                    end_date,
1188
                    end_date,
1060
                    periodPicker
1189
                    periodPicker
Lines 1068-1073 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1068
        setFormValues(
1197
        setFormValues(
1069
            patron_id,
1198
            patron_id,
1070
            booking_item_id,
1199
            booking_item_id,
1200
            item_type_id,
1071
            start_date,
1201
            start_date,
1072
            end_date,
1202
            end_date,
1073
            periodPicker
1203
            periodPicker
Lines 1078-1087 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1078
function setFormValues(
1208
function setFormValues(
1079
    patron_id,
1209
    patron_id,
1080
    booking_item_id,
1210
    booking_item_id,
1211
    item_type_id,
1081
    start_date,
1212
    start_date,
1082
    end_date,
1213
    end_date,
1083
    periodPicker
1214
    periodPicker
1084
) {
1215
) {
1216
    // Set itemtype first if provided (needed for edit mode before setting dates)
1217
    if (item_type_id) {
1218
        booking_itemtype_id = item_type_id;
1219
    }
1085
    // If passed patron, pre-select
1220
    // If passed patron, pre-select
1086
    if (patron_id) {
1221
    if (patron_id) {
1087
        let patronSelect = $("#booking_patron_id");
1222
        let patronSelect = $("#booking_patron_id");
Lines 1131-1144 function setFormValues( Link Here
1131
                    },
1266
                    },
1132
                });
1267
                });
1133
            }
1268
            }
1269
1270
            // IMPORTANT: Set dates AFTER item selection completes
1271
            // This ensures booking_itemtype_id is set before dates are validated
1272
            if (start_date) {
1273
                // Allow invalid pre-load so setDate can set date range
1274
                // periodPicker.set('allowInvalidPreload', true);
1275
                // FIXME: Why is this the case.. we're passing two valid Date objects
1276
                let start = new Date(start_date);
1277
                let end = new Date(end_date);
1278
1279
                let dates = [new Date(start_date), new Date(end_date)];
1280
                periodPicker.setDate(dates, true);
1281
            }
1134
        }, 100);
1282
        }, 100);
1135
    }
1283
    }
1136
1284
    // If no item selected but dates provided, set them now
1137
    // Set booking start & end if this is an edit
1285
    else if (start_date) {
1138
    if (start_date) {
1139
        // Allow invalid pre-load so setDate can set date range
1140
        // periodPicker.set('allowInvalidPreload', true);
1141
        // FIXME: Why is this the case.. we're passing two valid Date objects
1142
        let start = new Date(start_date);
1286
        let start = new Date(start_date);
1143
        let end = new Date(end_date);
1287
        let end = new Date(end_date);
1144
1288
Lines 1162-1180 $("#placeBookingForm").on("submit", function (e) { Link Here
1162
    let biblio_id = $("#booking_biblio_id").val();
1306
    let biblio_id = $("#booking_biblio_id").val();
1163
    let item_id = $("#booking_item_id").val();
1307
    let item_id = $("#booking_item_id").val();
1164
1308
1165
    if (!booking_id) {
1309
    // Prepare booking payload
1166
        let posting = $.post(
1310
    let booking_payload = {
1167
            url,
1311
        start_date: start_date,
1168
            JSON.stringify({
1312
        end_date: end_date,
1169
                start_date: start_date,
1313
        pickup_library_id: pickup_library_id,
1170
                end_date: end_date,
1314
        biblio_id: biblio_id,
1171
                pickup_library_id: pickup_library_id,
1315
        patron_id: $("#booking_patron_id").find(":selected").val(),
1172
                biblio_id: biblio_id,
1316
    };
1173
                item_id: item_id != 0 ? item_id : null,
1317
1174
                patron_id: $("#booking_patron_id").find(":selected").val(),
1318
    // If "any item" is selected, determine whether to send item_id or itemtype_id
1175
            })
1319
    if (item_id == 0) {
1320
        // Get items of the selected itemtype that are available for the period
1321
        let itemsOfType = bookable_items.filter(
1322
            item => item.effective_item_type_id === booking_itemtype_id
1176
        );
1323
        );
1177
1324
1325
        let availableItems = itemsOfType.filter(item => {
1326
            return isItemAvailableForPeriod(
1327
                item.item_id,
1328
                new Date(start_date),
1329
                new Date(end_date)
1330
            );
1331
        });
1332
1333
        if (availableItems.length === 0) {
1334
            $("#booking_result").replaceWith(
1335
                '<div id="booking_result" class="alert alert-danger">' +
1336
                    __("No suitable item found for booking") +
1337
                    "</div>"
1338
            );
1339
            return;
1340
        } else if (availableItems.length === 1) {
1341
            // Only one item available - optimization: send specific item_id
1342
            booking_payload.item_id = availableItems[0].item_id;
1343
        } else {
1344
            // Multiple items available - let server choose optimal item
1345
            booking_payload.itemtype_id = booking_itemtype_id;
1346
        }
1347
    } else {
1348
        // Specific item selected
1349
        booking_payload.item_id = item_id;
1350
    }
1351
1352
    if (!booking_id) {
1353
        let posting = $.post(url, JSON.stringify(booking_payload));
1354
1178
        posting.done(function (data) {
1355
        posting.done(function (data) {
1179
            // Update bookings store for subsequent bookings
1356
            // Update bookings store for subsequent bookings
1180
            bookings.push(data);
1357
            bookings.push(data);
Lines 1228-1247 $("#placeBookingForm").on("submit", function (e) { Link Here
1228
            );
1405
            );
1229
        });
1406
        });
1230
    } else {
1407
    } else {
1408
        // For edits with "any item" (item_id == 0), use same hybrid approach as new bookings
1409
        let edit_payload = {
1410
            booking_id: booking_id,
1411
            start_date: start_date,
1412
            end_date: end_date,
1413
            pickup_library_id: pickup_library_id,
1414
            biblio_id: biblio_id,
1415
            patron_id: $("#booking_patron_id").find(":selected").val(),
1416
        };
1417
1418
        if (item_id == 0) {
1419
            // Get items of the selected itemtype that are available for the period
1420
            let itemsOfType = bookable_items.filter(
1421
                item => item.effective_item_type_id === booking_itemtype_id
1422
            );
1423
1424
            let availableItems = itemsOfType.filter(item => {
1425
                return isItemAvailableForPeriod(
1426
                    item.item_id,
1427
                    new Date(start_date),
1428
                    new Date(end_date)
1429
                );
1430
            });
1431
1432
            if (availableItems.length === 0) {
1433
                $("#booking_result").replaceWith(
1434
                    '<div id="booking_result" class="alert alert-danger">' +
1435
                        __("No suitable item found for booking") +
1436
                        "</div>"
1437
                );
1438
                return;
1439
            } else if (availableItems.length === 1) {
1440
                // Only one item available - send specific item_id
1441
                edit_payload.item_id = availableItems[0].item_id;
1442
            } else {
1443
                // Multiple items available - let server choose optimal item
1444
                edit_payload.itemtype_id = booking_itemtype_id;
1445
            }
1446
        } else {
1447
            // Specific item selected
1448
            edit_payload.item_id = item_id;
1449
        }
1450
1231
        url += "/" + booking_id;
1451
        url += "/" + booking_id;
1232
        let putting = $.ajax({
1452
        let putting = $.ajax({
1233
            method: "PUT",
1453
            method: "PUT",
1234
            url: url,
1454
            url: url,
1235
            contentType: "application/json",
1455
            contentType: "application/json",
1236
            data: JSON.stringify({
1456
            data: JSON.stringify(edit_payload),
1237
                booking_id: booking_id,
1238
                start_date: start_date,
1239
                end_date: end_date,
1240
                pickup_library_id: pickup_library_id,
1241
                biblio_id: biblio_id,
1242
                item_id: item_id != 0 ? item_id : null,
1243
                patron_id: $("#booking_patron_id").find(":selected").val(),
1244
            }),
1245
        });
1457
        });
1246
1458
1247
        putting.done(function (data) {
1459
        putting.done(function (data) {
Lines 1307-1313 $("#placeBookingModal").on("hidden.bs.modal", function (e) { Link Here
1307
    booking_patron = undefined;
1519
    booking_patron = undefined;
1308
1520
1309
    // Reset item select
1521
    // Reset item select
1310
    $("#booking_item_id").val(0).trigger("change");
1522
    $("#booking_item_id").val(parseInt(0)).trigger("change");
1311
    $("#booking_item_id").prop("disabled", true);
1523
    $("#booking_item_id").prop("disabled", true);
1312
1524
1313
    // Reset itemtype select
1525
    // Reset itemtype select
1314
- 

Return to bug 40134