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