|
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 |
- |
|
|