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