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

(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js (-202 / +399 lines)
Lines 24-29 function containsAny(integers1, integers2) { Link Here
24
    return false; // No match found
24
    return false; // No match found
25
}
25
}
26
26
27
// Check if a specific item is available for the entire booking period
28
function isItemAvailableForPeriod(itemId, startDate, endDate) {
29
    for (let booking of bookings) {
30
        // Skip if we're editing this booking
31
        if (booking_id && booking_id == booking.booking_id) {
32
            continue;
33
        }
34
35
        if (booking.item_id !== itemId) {
36
            continue; // Different item, no conflict
37
        }
38
39
        let booking_start = dayjs(booking.start_date);
40
        let booking_end = dayjs(booking.end_date);
41
        let checkStartDate = dayjs(startDate);
42
        let checkEndDate = dayjs(endDate);
43
44
        // Check for any overlap with our booking period
45
        if (
46
            !(
47
                checkEndDate.isBefore(booking_start, "day") ||
48
                checkStartDate.isAfter(booking_end, "day")
49
            )
50
        ) {
51
            return false; // Overlap detected
52
        }
53
    }
54
    return true; // No conflicts found
55
}
56
27
$("#placeBookingModal").on("show.bs.modal", function (e) {
57
$("#placeBookingModal").on("show.bs.modal", function (e) {
28
    // Get context
58
    // Get context
29
    let button = $(e.relatedTarget);
59
    let button = $(e.relatedTarget);
Lines 440-454 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
440
                            // set local copy of selectedDates
470
                            // set local copy of selectedDates
441
                            let selectedDates = periodPicker.selectedDates;
471
                            let selectedDates = periodPicker.selectedDates;
442
472
443
                            // set booked counter
444
                            let booked = 0;
445
446
                            // reset the unavailable items array
447
                            let unavailable_items = [];
448
449
                            // reset the biblio level bookings array
450
                            let biblio_bookings = [];
451
452
                            // disable dates before selected date
473
                            // disable dates before selected date
453
                            if (
474
                            if (
454
                                !selectedDates[1] &&
475
                                !selectedDates[1] &&
Lines 458-630 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
458
                                return true;
479
                                return true;
459
                            }
480
                            }
460
481
461
                            // iterate existing bookings
482
                            // We should always have an itemtype selected and either specific item or "any item"
462
                            for (let booking of bookings) {
483
                            if (!booking_itemtype_id) {
463
                                // Skip if we're editing this booking
484
                                return true; // No itemtype selected, disable everything
464
                                if (
485
                            }
465
                                    booking_id &&
466
                                    booking_id == booking.booking_id
467
                                ) {
468
                                    continue;
469
                                }
470
486
471
                                let start_date = flatpickr.parseDate(
487
                            // If "any item of itemtype" is selected, use smart window maximization
472
                                    booking.start_date
488
                            if (!booking_item_id) {
489
                                return isDateDisabledForItemtype(
490
                                    date,
491
                                    selectedDates
473
                                );
492
                                );
474
                                let end_date = flatpickr.parseDate(
493
                            }
475
                                    booking.end_date
494
                            // If specific item is selected, use item-specific logic
495
                            else {
496
                                return isDateDisabledForSpecificItem(
497
                                    date,
498
                                    selectedDates
476
                                );
499
                                );
500
                            }
501
                        }
502
                    );
503
                }
477
504
478
                                // patron has selected a start date (end date checks)
505
                /**
479
                                if (selectedDates[0]) {
506
                 * SMART ITEMTYPE AVAILABILITY CALCULATION
480
                                    // new booking start date is between existing booking start and end dates
507
                 * For "any item of type X" bookings with dynamic item pool reduction
481
                                    if (
508
                 *
482
                                        selectedDates[0] >= start_date &&
509
                 * ALGORITHM OVERVIEW:
483
                                        selectedDates[0] <= end_date
510
                 * This function implements smart window maximization for itemtype bookings by using
484
                                    ) {
511
                 * dynamic item pool reduction. The core principle is "never re-add items to pool" -
485
                                        if (booking.item_id) {
512
                 * once an item is removed because it becomes unavailable, it's never re-added even
486
                                            if (
513
                 * if it becomes available again later. This ensures optimal resource allocation.
487
                                                unavailable_items.indexOf(
514
                 *
488
                                                    booking.item_id
515
                 * FLOW:
489
                                                ) === -1
516
                 * 1. For start date selection: Disable if ALL items of itemtype are booked
490
                                            ) {
517
                 * 2. For end date selection: Use smart window maximization algorithm
491
                                                unavailable_items.push(
518
                 *
492
                                                    booking.item_id
519
                 * SMART WINDOW MAXIMIZATION:
493
                                                );
520
                 * - Start with items available on the selected start date
494
                                            }
521
                 * - Walk through each day from start to target end date
495
                                        } else {
522
                 * - Remove items from pool when they become unavailable
496
                                            if (
523
                 * - NEVER re-add items even if they become available again later
497
                                                biblio_bookings.indexOf(
524
                 * - Disable date when no items remain in pool
498
                                                    booking.booking_id
525
                 *
499
                                                ) === -1
526
                 * EXAMPLE:
500
                                            ) {
527
                 * Items: A, B, C
501
                                                biblio_bookings.push(
528
                 * A available: days 1-5, booked 6-10, available again 11+
502
                                                    booking.booking_id
529
                 * B available: days 1-8, booked 9-15, available again 16+
503
                                                );
530
                 * C available: days 1-12, booked 13-20, available again 21+
504
                                            }
531
                 *
505
                                        }
532
                 * Start day 3:
506
                                    }
533
                 * - Initial pool: A, B, C
534
                 * - Days 3-5: Pool A, B, C (all available)
535
                 * - Day 6: Remove A (becomes booked), Pool now B, C
536
                 * - Day 9: Remove B (becomes booked), Pool now C
537
                 * - Day 13: Remove C (becomes booked), Pool now EMPTY → disable dates
538
                 * - Result: Can book days 3-12, day 13+ disabled
539
                 * - Note: A becomes available on day 11 but is NOT re-added to pool
540
                 *
541
                 * @param {Date} date - The date being checked for availability
542
                 * @param {Array} selectedDates - Array of selected dates from flatpickr [startDate, endDate?]
543
                 * @returns {boolean} - True if date should be disabled, false if available
544
                 */
545
                function isDateDisabledForItemtype(date, selectedDates) {
546
                    // Get items of the selected itemtype
547
                    let itemsOfType = bookable_items.filter(
548
                        item =>
549
                            item.effective_item_type_id === booking_itemtype_id
550
                    );
507
551
508
                                    // new booking end date would be between existing booking start and end dates
552
                    // For start date selection: disable if ALL items of itemtype are booked on this date
509
                                    else if (
553
                    if (!selectedDates[0]) {
510
                                        date >= start_date &&
554
                        return (
511
                                        date <= end_date
555
                            getAvailableItemsOnDate(date, itemsOfType)
512
                                    ) {
556
                                .length === 0
513
                                        if (booking.item_id) {
557
                        );
514
                                            if (
558
                    }
515
                                                unavailable_items.indexOf(
516
                                                    booking.item_id
517
                                                ) === -1
518
                                            ) {
519
                                                unavailable_items.push(
520
                                                    booking.item_id
521
                                                );
522
                                            }
523
                                        } else {
524
                                            if (
525
                                                biblio_bookings.indexOf(
526
                                                    booking.booking_id
527
                                                ) === -1
528
                                            ) {
529
                                                biblio_bookings.push(
530
                                                    booking.booking_id
531
                                                );
532
                                            }
533
                                        }
534
                                    }
535
559
536
                                    // new booking would span existing booking
560
                    // For end date selection: use smart window maximization
537
                                    else if (
561
                    if (selectedDates[0] && !selectedDates[1]) {
538
                                        selectedDates[0] <= start_date &&
562
                        let result = !isDateInMaximumWindow(
539
                                        date >= end_date
563
                            selectedDates[0],
540
                                    ) {
564
                            date,
541
                                        if (booking.item_id) {
565
                            itemsOfType
542
                                            if (
566
                        );
543
                                                unavailable_items.indexOf(
567
                        return result;
544
                                                    booking.item_id
568
                    }
545
                                                ) === -1
546
                                            ) {
547
                                                unavailable_items.push(
548
                                                    booking.item_id
549
                                                );
550
                                            }
551
                                        } else {
552
                                            if (
553
                                                biblio_bookings.indexOf(
554
                                                    booking.booking_id
555
                                                ) === -1
556
                                            ) {
557
                                                biblio_bookings.push(
558
                                                    booking.booking_id
559
                                                );
560
                                            }
561
                                        }
562
                                    }
563
569
564
                                    // new booking would not conflict
570
                    return false;
565
                                    else {
571
                }
566
                                        continue;
567
                                    }
568
572
569
                                    // check availability based on selection type
573
                /**
570
                                    if (
574
                 * MAXIMUM BOOKING WINDOW CALCULATION ALGORITHM
571
                                        booking_item_id &&
575
                 * Core Implementation of "Never Re-add Items to Pool" Principle
572
                                        booking_item_id != 0
576
                 *
573
                                    ) {
577
                 * PURPOSE:
574
                                        // Specific item selected - check if that item is unavailable
578
                 * Calculate the maximum possible booking window for "any item of itemtype X" bookings
575
                                        if (
579
                 * by dynamically reducing the available item pool as items become unavailable.
576
                                            unavailable_items.indexOf(
580
                 *
577
                                                parseInt(booking_item_id)
581
                 * CORE ALGORITHM: "Never Re-add Items to Pool"
578
                                            ) !== -1
582
                 * 1. Start with items available on the selected start date ONLY
579
                                        ) {
583
                 * 2. Walk through each day from start to target end date
580
                                            return true;
584
                 * 3. Remove items from pool when they become unavailable (booking starts)
581
                                        }
585
                 * 4. NEVER re-add items even if they become available again later (booking ends)
582
                                    } else {
586
                 * 5. Return false (disable date) when no items remain in pool
583
                                        // "Any item" selected - check if all items are unavailable
587
                 *
584
                                        let total_available =
588
                 * WHY THIS WORKS:
585
                                            bookable_items.length -
589
                 * - Maximizes booking windows by ensuring optimal resource allocation
586
                                            unavailable_items.length -
590
                 * - Prevents booking conflicts by being conservative about item availability
587
                                            biblio_bookings.length;
591
                 * - Ensures that if a booking can start on date X, there will always be an
588
                                        if (total_available === 0) {
592
                 *   item available for the entire duration (no conflicts)
589
                                            return true;
593
                 *
590
                                        }
594
                 * DETAILED EXAMPLE:
591
                                    }
595
                 * Items: TABLET001, TABLET002, TABLET003
592
                                }
596
                 * TABLET001: Available 1-9, Booked 10-15, Available 16+
597
                 * TABLET002: Available 1-12, Booked 13-20, Available 21+
598
                 * TABLET003: Available 1-17, Booked 18-25, Available 26+
599
                 *
600
                 * Testing: Can we book from day 5 to day 20?
601
                 *
602
                 * Step 1: Day 5 (start) - Initial pool: {TABLET001, TABLET002, TABLET003}
603
                 * Step 2: Day 6-9 - All items available, pool unchanged
604
                 * Step 3: Day 10 - TABLET001 becomes unavailable → Remove from pool
605
                 *         Pool now: {TABLET002, TABLET003}
606
                 * Step 4: Day 11-12 - Remaining items available, pool unchanged
607
                 * Step 5: Day 13 - TABLET002 becomes unavailable → Remove from pool
608
                 *         Pool now: {TABLET003}
609
                 * Step 6: Day 14-17 - TABLET003 available, pool unchanged
610
                 * Step 7: Day 18 - TABLET003 becomes unavailable → Remove from pool
611
                 *         Pool now: {} (empty)
612
                 * Step 8: Pool is empty → Return false (cannot book to day 20)
613
                 *
614
                 * Result: Can book from day 5 to day 17, but NOT to day 18+
615
                 *
616
                 * CRITICAL NOTE: Even though TABLET001 becomes available again on day 16,
617
                 * it is NOT re-added to the pool. This is the key principle that ensures
618
                 * booking reliability and optimal resource allocation.
619
                 *
620
                 * PERFORMANCE: O(n × d) where n = items of type, d = days in range
621
                 *
622
                 * @param {Date} startDate - Selected start date from flatpickr
623
                 * @param {Date} endDate - Target end date being checked for availability
624
                 * @param {Array} itemsOfType - Items of the selected itemtype
625
                 * @returns {boolean} - True if date is within maximum window, false if beyond
626
                 */
627
                function isDateInMaximumWindow(
628
                    startDate,
629
                    endDate,
630
                    itemsOfType
631
                ) {
632
                    // Start with only items available on the start date - never add items back
633
                    let availableOnStart = getAvailableItemsOnDate(
634
                        startDate,
635
                        itemsOfType
636
                    );
637
                    let availableItems = new Set(
638
                        availableOnStart.map(item => parseInt(item.item_id, 10))
639
                    );
593
640
594
                                // patron has not yet selected a start date (start date checks)
641
                    let currentDate = dayjs(startDate);
595
                                else if (
596
                                    date <= end_date &&
597
                                    date >= start_date
598
                                ) {
599
                                    // same item, disable date
600
                                    if (
601
                                        booking.item_id &&
602
                                        booking.item_id == booking_item_id
603
                                    ) {
604
                                        return true;
605
                                    }
606
642
607
                                    // count all clashes, both item and biblio level
643
                    // Walk through each day from start to end date
608
                                    booked++;
644
                    while (currentDate.isSameOrBefore(endDate, "day")) {
609
                                    if (booked == bookable) {
645
                        let availableToday = getAvailableItemsOnDate(
610
                                        return true;
646
                            currentDate,
611
                                    }
647
                            itemsOfType
648
                        );
649
                        let availableIds = new Set(
650
                            availableToday.map(item =>
651
                                parseInt(item.item_id, 10)
652
                            )
653
                        );
612
654
613
                                    // FIXME: The above is not intelligent enough to spot
655
                        // Remove items from our pool that are no longer available (never add back)
614
                                    // cases where an item must be used for a biblio level booking
656
                        // Only remove items that are unavailable today, don't re-add previously removed items
615
                                    // due to all other items being booking within the biblio level
657
                        let itemsToRemove = [];
616
                                    // booking period... we end up with a clash
658
                        for (let itemId of availableItems) {
617
                                    // To reproduce:
659
                            if (!availableIds.has(itemId)) {
618
                                    // * One bib with two bookable items.
660
                                itemsToRemove.push(itemId);
619
                                    // * Add item level booking
620
                                    // * Add biblio level booking that extends one day beyond the item level booking
621
                                    // * Try to book the item without an item level booking from the day before the biblio level
622
                                    //   booking is to be returned. Note this is a clash, the only item available for the biblio
623
                                    //   level booking is the item you just booked out overlapping the end date.
624
                                }
625
                            }
661
                            }
626
                        }
662
                        }
663
                        itemsToRemove.forEach(itemId =>
664
                            availableItems.delete(itemId)
665
                        );
666
667
                        // If no items left in the pool, this date is beyond the maximum window
668
                        if (availableItems.size === 0) {
669
                            return false;
670
                        }
671
672
                        // Move to next day
673
                        currentDate = currentDate.add(1, "day");
674
                    }
675
676
                    return true; // Date is within the maximum window
677
                }
678
679
                // Get items of itemtype that are available on a specific date
680
                function getAvailableItemsOnDate(date, itemsOfType) {
681
                    let unavailableItems = new Set();
682
683
                    // Check all existing bookings for conflicts on this date
684
                    for (let booking of bookings) {
685
                        // Skip if we're editing this booking
686
                        if (booking_id && booking_id == booking.booking_id) {
687
                            continue;
688
                        }
689
690
                        let start_date = dayjs(booking.start_date);
691
                        let end_date = dayjs(booking.end_date);
692
                        let checkDate = dayjs(date);
693
694
                        // Check if this date falls within this booking period
695
                        if (
696
                            checkDate.isSameOrAfter(start_date, "day") &&
697
                            checkDate.isSameOrBefore(end_date, "day")
698
                        ) {
699
                            // All bookings have item_id, so mark this specific item as unavailable
700
                            // Ensure integer comparison consistency
701
                            unavailableItems.add(parseInt(booking.item_id, 10));
702
                        }
703
                    }
704
705
                    // Return items of our type that are not unavailable
706
                    let available = itemsOfType.filter(
707
                        item =>
708
                            !unavailableItems.has(parseInt(item.item_id, 10))
627
                    );
709
                    );
710
                    return available;
711
                }
712
713
                // Item-specific availability logic for specific item bookings
714
                function isDateDisabledForSpecificItem(date, selectedDates) {
715
                    for (let booking of bookings) {
716
                        // Skip if we're editing this booking
717
                        if (booking_id && booking_id == booking.booking_id) {
718
                            continue;
719
                        }
720
721
                        let start_date = dayjs(booking.start_date);
722
                        let end_date = dayjs(booking.end_date);
723
                        let checkDate = dayjs(date);
724
725
                        // Check if this booking conflicts with our selected item and date
726
                        if (
727
                            checkDate.isSameOrAfter(start_date, "day") &&
728
                            checkDate.isSameOrBefore(end_date, "day")
729
                        ) {
730
                            // Same item, disable date (ensure integer comparison)
731
                            if (
732
                                parseInt(booking.item_id, 10) ===
733
                                parseInt(booking_item_id, 10)
734
                            ) {
735
                                return true;
736
                            }
737
                        }
738
                    }
739
                    return false;
628
                }
740
                }
629
741
630
                // Setup listener for itemtype select2
742
                // Setup listener for itemtype select2
Lines 663-671 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
663
775
664
                // Setup listener for item select2
776
                // Setup listener for item select2
665
                $("#booking_item_id").on("select2:select", function (e) {
777
                $("#booking_item_id").on("select2:select", function (e) {
666
                    booking_item_id = e.params.data.id
778
                    booking_item_id =
667
                        ? e.params.data.id
779
                        e.params.data.id !== undefined &&
668
                        : null;
780
                        e.params.data.id !== null
781
                            ? parseInt(e.params.data.id, 10)
782
                            : 0;
669
783
670
                    // Disable invalid pickup locations
784
                    // Disable invalid pickup locations
671
                    $("#pickup_library_id > option").each(function () {
785
                    $("#pickup_library_id > option").each(function () {
Lines 679-685 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
679
                                .split(",")
793
                                .split(",")
680
                                .map(Number);
794
                                .map(Number);
681
                            if (
795
                            if (
682
                                valid_items.includes(parseInt(booking_item_id))
796
                                valid_items.includes(
797
                                    parseInt(booking_item_id, 10)
798
                                )
683
                            ) {
799
                            ) {
684
                                option.prop("disabled", false);
800
                                option.prop("disabled", false);
685
                            } else {
801
                            } else {
Lines 878-893 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
878
                let bookingsByDate = {};
994
                let bookingsByDate = {};
879
                // Iterate through the bookings array
995
                // Iterate through the bookings array
880
                bookings.forEach(booking => {
996
                bookings.forEach(booking => {
881
                    const start_date = flatpickr.parseDate(booking.start_date);
997
                    const start_date = dayjs(booking.start_date);
882
                    const end_date = flatpickr.parseDate(booking.end_date);
998
                    const end_date = dayjs(booking.end_date);
883
                    const item_id = booking.item_id;
999
                    const item_id = booking.item_id;
884
1000
885
                    // 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
886
                    let currentDate = new Date(start_date);
1002
                    let currentDate = dayjs(start_date);
887
                    while (currentDate <= end_date) {
1003
                    while (currentDate.isSameOrBefore(end_date, "day")) {
888
                        const currentDateStr = currentDate
1004
                        const currentDateStr = currentDate.format("YYYY-MM-DD");
889
                            .toISOString()
890
                            .split("T")[0];
891
1005
892
                        // 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
893
                        if (!bookingsByDate[currentDateStr]) {
1007
                        if (!bookingsByDate[currentDateStr]) {
Lines 898-904 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
898
                        bookingsByDate[currentDateStr].push(item_id);
1012
                        bookingsByDate[currentDateStr].push(item_id);
899
1013
900
                        // Move to the next day
1014
                        // Move to the next day
901
                        currentDate.setDate(currentDate.getDate() + 1);
1015
                        currentDate = currentDate.add(1, "day");
902
                    }
1016
                    }
903
                });
1017
                });
904
1018
Lines 1083-1088 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1083
                setFormValues(
1197
                setFormValues(
1084
                    patron_id,
1198
                    patron_id,
1085
                    booking_item_id,
1199
                    booking_item_id,
1200
                    item_type_id,
1086
                    start_date,
1201
                    start_date,
1087
                    end_date,
1202
                    end_date,
1088
                    periodPicker
1203
                    periodPicker
Lines 1096-1101 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1096
        setFormValues(
1211
        setFormValues(
1097
            patron_id,
1212
            patron_id,
1098
            booking_item_id,
1213
            booking_item_id,
1214
            item_type_id,
1099
            start_date,
1215
            start_date,
1100
            end_date,
1216
            end_date,
1101
            periodPicker
1217
            periodPicker
Lines 1106-1115 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1106
function setFormValues(
1222
function setFormValues(
1107
    patron_id,
1223
    patron_id,
1108
    booking_item_id,
1224
    booking_item_id,
1225
    item_type_id,
1109
    start_date,
1226
    start_date,
1110
    end_date,
1227
    end_date,
1111
    periodPicker
1228
    periodPicker
1112
) {
1229
) {
1230
    // Set itemtype first if provided (needed for edit mode before setting dates)
1231
    if (item_type_id) {
1232
        booking_itemtype_id = item_type_id;
1233
    }
1113
    // If passed patron, pre-select
1234
    // If passed patron, pre-select
1114
    if (patron_id) {
1235
    if (patron_id) {
1115
        let patronSelect = $("#booking_patron_id");
1236
        let patronSelect = $("#booking_patron_id");
Lines 1159-1172 function setFormValues( Link Here
1159
                    },
1280
                    },
1160
                });
1281
                });
1161
            }
1282
            }
1283
1284
            // IMPORTANT: Set dates AFTER item selection completes
1285
            // This ensures booking_itemtype_id is set before dates are validated
1286
            if (start_date) {
1287
                // Allow invalid pre-load so setDate can set date range
1288
                // periodPicker.set('allowInvalidPreload', true);
1289
                // FIXME: Why is this the case.. we're passing two valid Date objects
1290
                let start = new Date(start_date);
1291
                let end = new Date(end_date);
1292
1293
                let dates = [new Date(start_date), new Date(end_date)];
1294
                periodPicker.setDate(dates, true);
1295
            }
1162
        }, 100);
1296
        }, 100);
1163
    }
1297
    }
1164
1298
    // If no item selected but dates provided, set them now
1165
    // Set booking start & end if this is an edit
1299
    else if (start_date) {
1166
    if (start_date) {
1167
        // Allow invalid pre-load so setDate can set date range
1168
        // periodPicker.set('allowInvalidPreload', true);
1169
        // FIXME: Why is this the case.. we're passing two valid Date objects
1170
        let start = new Date(start_date);
1300
        let start = new Date(start_date);
1171
        let end = new Date(end_date);
1301
        let end = new Date(end_date);
1172
1302
Lines 1190-1208 $("#placeBookingForm").on("submit", function (e) { Link Here
1190
    let biblio_id = $("#booking_biblio_id").val();
1320
    let biblio_id = $("#booking_biblio_id").val();
1191
    let item_id = $("#booking_item_id").val();
1321
    let item_id = $("#booking_item_id").val();
1192
1322
1193
    if (!booking_id) {
1323
    // Prepare booking payload
1194
        let posting = $.post(
1324
    let booking_payload = {
1195
            url,
1325
        start_date: start_date,
1196
            JSON.stringify({
1326
        end_date: end_date,
1197
                start_date: start_date,
1327
        pickup_library_id: pickup_library_id,
1198
                end_date: end_date,
1328
        biblio_id: biblio_id,
1199
                pickup_library_id: pickup_library_id,
1329
        patron_id: $("#booking_patron_id").find(":selected").val(),
1200
                biblio_id: biblio_id,
1330
    };
1201
                item_id: item_id != 0 ? item_id : null,
1331
1202
                patron_id: $("#booking_patron_id").find(":selected").val(),
1332
    // If "any item" is selected, determine whether to send item_id or itemtype_id
1203
            })
1333
    if (item_id == 0) {
1334
        // Get items of the selected itemtype that are available for the period
1335
        let itemsOfType = bookable_items.filter(
1336
            item => item.effective_item_type_id === booking_itemtype_id
1204
        );
1337
        );
1205
1338
1339
        let availableItems = itemsOfType.filter(item => {
1340
            return isItemAvailableForPeriod(
1341
                item.item_id,
1342
                new Date(start_date),
1343
                new Date(end_date)
1344
            );
1345
        });
1346
1347
        if (availableItems.length === 0) {
1348
            $("#booking_result").replaceWith(
1349
                '<div id="booking_result" class="alert alert-danger">' +
1350
                    __("No suitable item found for booking") +
1351
                    "</div>"
1352
            );
1353
            return;
1354
        } else if (availableItems.length === 1) {
1355
            // Only one item available - optimization: send specific item_id
1356
            booking_payload.item_id = availableItems[0].item_id;
1357
        } else {
1358
            // Multiple items available - let server choose optimal item
1359
            booking_payload.itemtype_id = booking_itemtype_id;
1360
        }
1361
    } else {
1362
        // Specific item selected
1363
        booking_payload.item_id = item_id;
1364
    }
1365
1366
    if (!booking_id) {
1367
        let posting = $.post(url, JSON.stringify(booking_payload));
1368
1206
        posting.done(function (data) {
1369
        posting.done(function (data) {
1207
            // Update bookings store for subsequent bookings
1370
            // Update bookings store for subsequent bookings
1208
            bookings.push(data);
1371
            bookings.push(data);
Lines 1256-1275 $("#placeBookingForm").on("submit", function (e) { Link Here
1256
            );
1419
            );
1257
        });
1420
        });
1258
    } else {
1421
    } else {
1422
        // For edits with "any item" (item_id == 0), use same hybrid approach as new bookings
1423
        let edit_payload = {
1424
            booking_id: booking_id,
1425
            start_date: start_date,
1426
            end_date: end_date,
1427
            pickup_library_id: pickup_library_id,
1428
            biblio_id: biblio_id,
1429
            patron_id: $("#booking_patron_id").find(":selected").val(),
1430
        };
1431
1432
        if (item_id == 0) {
1433
            // Get items of the selected itemtype that are available for the period
1434
            let itemsOfType = bookable_items.filter(
1435
                item => item.effective_item_type_id === booking_itemtype_id
1436
            );
1437
1438
            let availableItems = itemsOfType.filter(item => {
1439
                return isItemAvailableForPeriod(
1440
                    item.item_id,
1441
                    new Date(start_date),
1442
                    new Date(end_date)
1443
                );
1444
            });
1445
1446
            if (availableItems.length === 0) {
1447
                $("#booking_result").replaceWith(
1448
                    '<div id="booking_result" class="alert alert-danger">' +
1449
                        __("No suitable item found for booking") +
1450
                        "</div>"
1451
                );
1452
                return;
1453
            } else if (availableItems.length === 1) {
1454
                // Only one item available - send specific item_id
1455
                edit_payload.item_id = availableItems[0].item_id;
1456
            } else {
1457
                // Multiple items available - let server choose optimal item
1458
                edit_payload.itemtype_id = booking_itemtype_id;
1459
            }
1460
        } else {
1461
            // Specific item selected
1462
            edit_payload.item_id = item_id;
1463
        }
1464
1259
        url += "/" + booking_id;
1465
        url += "/" + booking_id;
1260
        let putting = $.ajax({
1466
        let putting = $.ajax({
1261
            method: "PUT",
1467
            method: "PUT",
1262
            url: url,
1468
            url: url,
1263
            contentType: "application/json",
1469
            contentType: "application/json",
1264
            data: JSON.stringify({
1470
            data: JSON.stringify(edit_payload),
1265
                booking_id: booking_id,
1266
                start_date: start_date,
1267
                end_date: end_date,
1268
                pickup_library_id: pickup_library_id,
1269
                biblio_id: biblio_id,
1270
                item_id: item_id != 0 ? item_id : null,
1271
                patron_id: $("#booking_patron_id").find(":selected").val(),
1272
            }),
1273
        });
1471
        });
1274
1472
1275
        putting.done(function (data) {
1473
        putting.done(function (data) {
Lines 1335-1341 $("#placeBookingModal").on("hidden.bs.modal", function (e) { Link Here
1335
    booking_patron = undefined;
1533
    booking_patron = undefined;
1336
1534
1337
    // Reset item select
1535
    // Reset item select
1338
    $("#booking_item_id").val(0).trigger("change");
1536
    $("#booking_item_id").val(parseInt(0)).trigger("change");
1339
    $("#booking_item_id").prop("disabled", true);
1537
    $("#booking_item_id").prop("disabled", true);
1340
1538
1341
    // Reset itemtype select
1539
    // Reset itemtype select
1342
- 

Return to bug 40134