Lines 421-599
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
421 |
f => f.name === "dateDisable" |
421 |
f => f.name === "dateDisable" |
422 |
); |
422 |
); |
423 |
if (disableExists.length === 0) { |
423 |
if (disableExists.length === 0) { |
424 |
periodPicker.config.disable.push(function dateDisable( |
424 |
periodPicker.config.disable.push( |
425 |
date |
425 |
function dateDisable(date) { |
426 |
) { |
426 |
// set local copy of selectedDates |
427 |
// set local copy of selectedDates |
427 |
let selectedDates = periodPicker.selectedDates; |
428 |
let selectedDates = periodPicker.selectedDates; |
|
|
429 |
|
430 |
// set booked counter |
431 |
let booked = 0; |
432 |
|
428 |
|
433 |
// reset the unavailable items array |
429 |
// set booked counter |
434 |
let unavailable_items = []; |
430 |
let booked = 0; |
435 |
|
431 |
|
436 |
// reset the biblio level bookings array |
432 |
// reset the unavailable items array |
437 |
let biblio_bookings = []; |
433 |
let unavailable_items = []; |
438 |
|
434 |
|
439 |
// disable dates before selected date |
435 |
// reset the biblio level bookings array |
440 |
if ( |
436 |
let biblio_bookings = []; |
441 |
!selectedDates[1] && |
|
|
442 |
selectedDates[0] && |
443 |
selectedDates[0] > date |
444 |
) { |
445 |
return true; |
446 |
} |
447 |
|
437 |
|
448 |
// iterate existing bookings |
438 |
// disable dates before selected date |
449 |
for (booking of bookings) { |
|
|
450 |
// Skip if we're editing this booking |
451 |
if ( |
439 |
if ( |
452 |
booking_id && |
440 |
!selectedDates[1] && |
453 |
booking_id == booking.booking_id |
441 |
selectedDates[0] && |
|
|
442 |
selectedDates[0] > date |
454 |
) { |
443 |
) { |
455 |
continue; |
444 |
return true; |
456 |
} |
445 |
} |
457 |
|
446 |
|
458 |
let start_date = flatpickr.parseDate( |
447 |
// iterate existing bookings |
459 |
booking.start_date |
448 |
for (booking of bookings) { |
460 |
); |
449 |
// Skip if we're editing this booking |
461 |
let end_date = flatpickr.parseDate( |
|
|
462 |
booking.end_date |
463 |
); |
464 |
|
465 |
// patron has selected a start date (end date checks) |
466 |
if (selectedDates[0]) { |
467 |
// new booking start date is between existing booking start and end dates |
468 |
if ( |
450 |
if ( |
469 |
selectedDates[0] >= start_date && |
451 |
booking_id && |
470 |
selectedDates[0] <= end_date |
452 |
booking_id == booking.booking_id |
471 |
) { |
453 |
) { |
472 |
if (booking.item_id) { |
454 |
continue; |
473 |
if ( |
|
|
474 |
unavailable_items.indexOf( |
475 |
booking.item_id |
476 |
) === -1 |
477 |
) { |
478 |
unavailable_items.push( |
479 |
booking.item_id |
480 |
); |
481 |
} |
482 |
} else { |
483 |
if ( |
484 |
biblio_bookings.indexOf( |
485 |
booking.booking_id |
486 |
) === -1 |
487 |
) { |
488 |
biblio_bookings.push( |
489 |
booking.booking_id |
490 |
); |
491 |
} |
492 |
} |
493 |
} |
455 |
} |
494 |
|
456 |
|
495 |
// new booking end date would be between existing booking start and end dates |
457 |
let start_date = flatpickr.parseDate( |
496 |
else if ( |
458 |
booking.start_date |
497 |
date >= start_date && |
459 |
); |
498 |
date <= end_date |
460 |
let end_date = flatpickr.parseDate( |
499 |
) { |
461 |
booking.end_date |
500 |
if (booking.item_id) { |
462 |
); |
501 |
if ( |
463 |
|
502 |
unavailable_items.indexOf( |
464 |
// patron has selected a start date (end date checks) |
503 |
booking.item_id |
465 |
if (selectedDates[0]) { |
504 |
) === -1 |
466 |
// new booking start date is between existing booking start and end dates |
505 |
) { |
467 |
if ( |
506 |
unavailable_items.push( |
468 |
selectedDates[0] >= start_date && |
507 |
booking.item_id |
469 |
selectedDates[0] <= end_date |
508 |
); |
470 |
) { |
509 |
} |
471 |
if (booking.item_id) { |
510 |
} else { |
472 |
if ( |
511 |
if ( |
473 |
unavailable_items.indexOf( |
512 |
biblio_bookings.indexOf( |
474 |
booking.item_id |
513 |
booking.booking_id |
475 |
) === -1 |
514 |
) === -1 |
476 |
) { |
515 |
) { |
477 |
unavailable_items.push( |
516 |
biblio_bookings.push( |
478 |
booking.item_id |
517 |
booking.booking_id |
479 |
); |
518 |
); |
480 |
} |
|
|
481 |
} else { |
482 |
if ( |
483 |
biblio_bookings.indexOf( |
484 |
booking.booking_id |
485 |
) === -1 |
486 |
) { |
487 |
biblio_bookings.push( |
488 |
booking.booking_id |
489 |
); |
490 |
} |
519 |
} |
491 |
} |
520 |
} |
492 |
} |
521 |
} |
|
|
522 |
|
493 |
|
523 |
// new booking would span existing booking |
494 |
// new booking end date would be between existing booking start and end dates |
524 |
else if ( |
495 |
else if ( |
525 |
selectedDates[0] <= start_date && |
496 |
date >= start_date && |
526 |
date >= end_date |
497 |
date <= end_date |
527 |
) { |
498 |
) { |
528 |
if (booking.item_id) { |
499 |
if (booking.item_id) { |
529 |
if ( |
500 |
if ( |
530 |
unavailable_items.indexOf( |
501 |
unavailable_items.indexOf( |
531 |
booking.item_id |
502 |
booking.item_id |
532 |
) === -1 |
503 |
) === -1 |
533 |
) { |
504 |
) { |
534 |
unavailable_items.push( |
505 |
unavailable_items.push( |
535 |
booking.item_id |
506 |
booking.item_id |
536 |
); |
507 |
); |
|
|
508 |
} |
509 |
} else { |
510 |
if ( |
511 |
biblio_bookings.indexOf( |
512 |
booking.booking_id |
513 |
) === -1 |
514 |
) { |
515 |
biblio_bookings.push( |
516 |
booking.booking_id |
517 |
); |
518 |
} |
537 |
} |
519 |
} |
538 |
} else { |
520 |
} |
539 |
if ( |
521 |
|
540 |
biblio_bookings.indexOf( |
522 |
// new booking would span existing booking |
541 |
booking.booking_id |
523 |
else if ( |
542 |
) === -1 |
524 |
selectedDates[0] <= start_date && |
543 |
) { |
525 |
date >= end_date |
544 |
biblio_bookings.push( |
526 |
) { |
545 |
booking.booking_id |
527 |
if (booking.item_id) { |
546 |
); |
528 |
if ( |
|
|
529 |
unavailable_items.indexOf( |
530 |
booking.item_id |
531 |
) === -1 |
532 |
) { |
533 |
unavailable_items.push( |
534 |
booking.item_id |
535 |
); |
536 |
} |
537 |
} else { |
538 |
if ( |
539 |
biblio_bookings.indexOf( |
540 |
booking.booking_id |
541 |
) === -1 |
542 |
) { |
543 |
biblio_bookings.push( |
544 |
booking.booking_id |
545 |
); |
546 |
} |
547 |
} |
547 |
} |
548 |
} |
548 |
} |
549 |
} |
|
|
550 |
|
549 |
|
551 |
// new booking would not conflict |
550 |
// new booking would not conflict |
552 |
else { |
551 |
else { |
553 |
continue; |
552 |
continue; |
554 |
} |
553 |
} |
555 |
|
554 |
|
556 |
// check that there are available items |
555 |
// check that there are available items |
557 |
// available = all bookable items - booked items - booked biblios |
556 |
// available = all bookable items - booked items - booked biblios |
558 |
let total_available = |
557 |
let total_available = |
559 |
bookable_items.length - |
558 |
bookable_items.length - |
560 |
unavailable_items.length - |
559 |
unavailable_items.length - |
561 |
biblio_bookings.length; |
560 |
biblio_bookings.length; |
562 |
if (total_available === 0) { |
561 |
if (total_available === 0) { |
563 |
return true; |
562 |
return true; |
|
|
563 |
} |
564 |
} |
564 |
} |
565 |
} |
|
|
566 |
|
565 |
|
567 |
// patron has not yet selected a start date (start date checks) |
566 |
// patron has not yet selected a start date (start date checks) |
568 |
else if (date <= end_date && date >= start_date) { |
567 |
else if ( |
569 |
// same item, disable date |
568 |
date <= end_date && |
570 |
if ( |
569 |
date >= start_date |
571 |
booking.item_id && |
|
|
572 |
booking.item_id == booking_item_id |
573 |
) { |
570 |
) { |
574 |
return true; |
571 |
// same item, disable date |
575 |
} |
572 |
if ( |
|
|
573 |
booking.item_id && |
574 |
booking.item_id == booking_item_id |
575 |
) { |
576 |
return true; |
577 |
} |
576 |
|
578 |
|
577 |
// count all clashes, both item and biblio level |
579 |
// count all clashes, both item and biblio level |
578 |
booked++; |
580 |
booked++; |
579 |
if (booked == bookable) { |
581 |
if (booked == bookable) { |
580 |
return true; |
582 |
return true; |
581 |
} |
583 |
} |
582 |
|
584 |
|
583 |
// FIXME: The above is not intelligent enough to spot |
585 |
// FIXME: The above is not intelligent enough to spot |
584 |
// cases where an item must be used for a biblio level booking |
586 |
// cases where an item must be used for a biblio level booking |
585 |
// due to all other items being booking within the biblio level |
587 |
// due to all other items being booking within the biblio level |
586 |
// booking period... we end up with a clash |
588 |
// booking period... we end up with a clash |
587 |
// To reproduce: |
589 |
// To reproduce: |
588 |
// * One bib with two bookable items. |
590 |
// * One bib with two bookable items. |
589 |
// * Add item level booking |
591 |
// * Add item level booking |
590 |
// * Add biblio level booking that extends one day beyond the item level booking |
592 |
// * Add biblio level booking that extends one day beyond the item level booking |
591 |
// * Try to book the item without an item level booking from the day before the biblio level |
593 |
// * Try to book the item without an item level booking from the day before the biblio level |
592 |
// booking is to be returned. Note this is a clash, the only item available for the biblio |
594 |
// booking is to be returned. Note this is a clash, the only item available for the biblio |
593 |
// level booking is the item you just booked out overlapping the end date. |
595 |
// level booking is the item you just booked out overlapping the end date. |
|
|
596 |
} |
594 |
} |
597 |
} |
595 |
} |
598 |
} |
596 |
}); |
599 |
); |
597 |
} |
600 |
} |
598 |
|
601 |
|
599 |
// Setup listener for itemtype select2 |
602 |
// Setup listener for itemtype select2 |
Lines 713-833
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
713 |
f => f.name === "periodChange" |
716 |
f => f.name === "periodChange" |
714 |
); |
717 |
); |
715 |
if (changeExists.length === 0) { |
718 |
if (changeExists.length === 0) { |
716 |
periodPicker.config.onChange.push(function periodChange( |
719 |
periodPicker.config.onChange.push( |
717 |
selectedDates, |
720 |
function periodChange( |
718 |
dateStr, |
721 |
selectedDates, |
719 |
instance |
722 |
dateStr, |
720 |
) { |
723 |
instance |
721 |
// Start date selected |
724 |
) { |
722 |
if (selectedDates[0] && !selectedDates[1]) { |
725 |
// Start date selected |
723 |
const startDate = new Date(selectedDates[0]); |
726 |
if (selectedDates[0] && !selectedDates[1]) { |
724 |
|
727 |
const startDate = new Date(selectedDates[0]); |
725 |
// Custom format function to make specific dates bold |
728 |
|
726 |
boldDates = [new Date(startDate)]; |
729 |
// Custom format function to make specific dates bold |
727 |
// Add issueLength days after the startDate |
730 |
boldDates = [new Date(startDate)]; |
728 |
const nextDate = new Date(startDate); |
731 |
// Add issueLength days after the startDate |
729 |
nextDate.setDate( |
732 |
const nextDate = new Date(startDate); |
730 |
nextDate.getDate() + parseInt(issueLength) |
|
|
731 |
); |
732 |
boldDates.push(new Date(nextDate)); |
733 |
|
734 |
// Add subsequent dates based on renewalsAllowed and renewalLength |
735 |
for (let i = 0; i < renewalsAllowed; i++) { |
736 |
nextDate.setDate( |
733 |
nextDate.setDate( |
737 |
nextDate.getDate() + parseInt(renewalLength) |
734 |
nextDate.getDate() + parseInt(issueLength) |
738 |
); |
735 |
); |
739 |
boldDates.push(new Date(nextDate)); |
736 |
boldDates.push(new Date(nextDate)); |
740 |
} |
|
|
741 |
|
737 |
|
742 |
// Calculate the maximum date based on the selected start date |
738 |
// Add subsequent dates based on renewalsAllowed and renewalLength |
743 |
let totalRenewalLength = |
739 |
for (let i = 0; i < renewalsAllowed; i++) { |
744 |
parseInt(renewalsAllowed) * |
740 |
nextDate.setDate( |
745 |
parseInt(renewalLength); |
741 |
nextDate.getDate() + |
746 |
let totalIssueLength = |
742 |
parseInt(renewalLength) |
747 |
parseInt(issueLength) + |
743 |
); |
748 |
parseInt(totalRenewalLength); |
744 |
boldDates.push(new Date(nextDate)); |
749 |
|
745 |
} |
750 |
const maxDate = new Date(startDate.getTime()); |
|
|
751 |
maxDate.setDate( |
752 |
maxDate.getDate() + totalIssueLength |
753 |
); |
754 |
|
746 |
|
755 |
// Update the maxDate option of the flatpickr instance |
747 |
// Calculate the maximum date based on the selected start date |
756 |
instance.set("maxDate", maxDate); |
748 |
let totalRenewalLength = |
757 |
} |
749 |
parseInt(renewalsAllowed) * |
758 |
// Range set, update hidden fields and set available items |
750 |
parseInt(renewalLength); |
759 |
else if (selectedDates[0] && selectedDates[1]) { |
751 |
let totalIssueLength = |
760 |
// set form fields from picker |
752 |
parseInt(issueLength) + |
761 |
let picker_start = dayjs(selectedDates[0]); |
753 |
parseInt(totalRenewalLength); |
762 |
let picker_end = dayjs(selectedDates[1]).endOf( |
754 |
|
763 |
"day" |
755 |
const maxDate = new Date(startDate.getTime()); |
764 |
); |
756 |
maxDate.setDate( |
765 |
$("#booking_start_date").val( |
757 |
maxDate.getDate() + totalIssueLength |
766 |
picker_start.toISOString() |
758 |
); |
767 |
); |
|
|
768 |
$("#booking_end_date").val( |
769 |
picker_end.toISOString() |
770 |
); |
771 |
|
759 |
|
772 |
// set available items in select2 |
760 |
// Update the maxDate option of the flatpickr instance |
773 |
let booked_items = bookings.filter(function ( |
761 |
instance.set("maxDate", maxDate); |
774 |
booking |
762 |
} |
775 |
) { |
763 |
// Range set, update hidden fields and set available items |
776 |
let start_date = flatpickr.parseDate( |
764 |
else if (selectedDates[0] && selectedDates[1]) { |
777 |
booking.start_date |
765 |
// set form fields from picker |
|
|
766 |
let picker_start = dayjs(selectedDates[0]); |
767 |
let picker_end = dayjs(selectedDates[1]).endOf( |
768 |
"day" |
778 |
); |
769 |
); |
779 |
let end_date = flatpickr.parseDate( |
770 |
$("#booking_start_date").val( |
780 |
booking.end_date |
771 |
picker_start.toISOString() |
781 |
); |
772 |
); |
782 |
// This booking ends before the start of the new booking |
773 |
$("#booking_end_date").val( |
783 |
if (end_date <= selectedDates[0]) { |
774 |
picker_end.toISOString() |
784 |
return false; |
775 |
); |
785 |
} |
776 |
|
786 |
// This booking starts after then end of the new booking |
777 |
// set available items in select2 |
787 |
if (start_date >= selectedDates[1]) { |
778 |
let booked_items = bookings.filter( |
788 |
return false; |
779 |
function (booking) { |
789 |
} |
780 |
let start_date = flatpickr.parseDate( |
790 |
// This booking overlaps |
781 |
booking.start_date |
791 |
return true; |
|
|
792 |
}); |
793 |
$("#booking_item_id > option").each(function () { |
794 |
let option = $(this); |
795 |
if ( |
796 |
booking_item_id && |
797 |
booking_item_id == option.val() |
798 |
) { |
799 |
option.prop("disabled", false); |
800 |
} else if ( |
801 |
booked_items.some(function (booked_item) { |
802 |
return ( |
803 |
option.val() == booked_item.item_id |
804 |
); |
782 |
); |
805 |
}) |
783 |
let end_date = flatpickr.parseDate( |
806 |
) { |
784 |
booking.end_date |
807 |
option.attr("data-available", false); |
785 |
); |
808 |
option.prop("disabled", true); |
786 |
// This booking ends before the start of the new booking |
809 |
} else { |
787 |
if (end_date <= selectedDates[0]) { |
810 |
option.attr("data-available", true); |
788 |
return false; |
811 |
if (option.data("pickup")) { |
789 |
} |
812 |
option.prop("disabled", false); |
790 |
// This booking starts after then end of the new booking |
|
|
791 |
if (start_date >= selectedDates[1]) { |
792 |
return false; |
793 |
} |
794 |
// This booking overlaps |
795 |
return true; |
813 |
} |
796 |
} |
814 |
} |
797 |
); |
815 |
}); |
798 |
$("#booking_item_id > option").each( |
816 |
$("#booking_item_id").trigger("change.select2"); |
799 |
function () { |
817 |
} |
800 |
let option = $(this); |
818 |
// Range not set, reset field options and flatPickr state |
801 |
if ( |
819 |
else { |
802 |
booking_item_id && |
820 |
boldDates = []; |
803 |
booking_item_id == option.val() |
821 |
instance.set("maxDate", null); |
804 |
) { |
822 |
$("#booking_item_id > option").each(function () { |
805 |
option.prop("disabled", false); |
823 |
let option = $(this); |
806 |
} else if ( |
824 |
if (option.data("pickup")) { |
807 |
booked_items.some( |
825 |
option.prop("disabled", false); |
808 |
function (booked_item) { |
826 |
} |
809 |
return ( |
827 |
}); |
810 |
option.val() == |
828 |
$("#booking_item_id").trigger("change.select2"); |
811 |
booked_item.item_id |
|
|
812 |
); |
813 |
} |
814 |
) |
815 |
) { |
816 |
option.attr( |
817 |
"data-available", |
818 |
false |
819 |
); |
820 |
option.prop("disabled", true); |
821 |
} else { |
822 |
option.attr("data-available", true); |
823 |
if (option.data("pickup")) { |
824 |
option.prop("disabled", false); |
825 |
} |
826 |
} |
827 |
} |
828 |
); |
829 |
$("#booking_item_id").trigger("change.select2"); |
830 |
} |
831 |
// Range not set, reset field options and flatPickr state |
832 |
else { |
833 |
boldDates = []; |
834 |
instance.set("maxDate", null); |
835 |
$("#booking_item_id > option").each( |
836 |
function () { |
837 |
let option = $(this); |
838 |
if (option.data("pickup")) { |
839 |
option.prop("disabled", false); |
840 |
} |
841 |
} |
842 |
); |
843 |
$("#booking_item_id").trigger("change.select2"); |
844 |
} |
829 |
} |
845 |
} |
830 |
}); |
846 |
); |
831 |
} |
847 |
} |
832 |
|
848 |
|
833 |
// Create a bookings store keyed on date |
849 |
// Create a bookings store keyed on date |
Lines 863-898
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
863 |
f => f.name === "dayCreate" |
879 |
f => f.name === "dayCreate" |
864 |
); |
880 |
); |
865 |
if (dayCreateExists.length === 0) { |
881 |
if (dayCreateExists.length === 0) { |
866 |
periodPicker.config.onDayCreate.push(function dayCreate( |
882 |
periodPicker.config.onDayCreate.push( |
867 |
dObj, |
883 |
function dayCreate(dObj, dStr, instance, dayElem) { |
868 |
dStr, |
884 |
const currentDate = dayElem.dateObj; |
869 |
instance, |
885 |
const dateString = currentDate |
870 |
dayElem |
886 |
.toISOString() |
871 |
) { |
887 |
.split("T")[0]; |
872 |
const currentDate = dayElem.dateObj; |
888 |
|
873 |
const dateString = currentDate |
889 |
const isBold = boldDates.some( |
874 |
.toISOString() |
890 |
boldDate => |
875 |
.split("T")[0]; |
891 |
boldDate.getTime() === currentDate.getTime() |
876 |
|
892 |
); |
877 |
const isBold = boldDates.some( |
893 |
if (isBold) { |
878 |
boldDate => |
894 |
dayElem.classList.add("title"); |
879 |
boldDate.getTime() === currentDate.getTime() |
895 |
} |
880 |
); |
|
|
881 |
if (isBold) { |
882 |
dayElem.classList.add("title"); |
883 |
} |
884 |
|
896 |
|
885 |
if (bookingsByDate[dateString]) { |
897 |
if (bookingsByDate[dateString]) { |
886 |
const dots = document.createElement("span"); |
898 |
const dots = document.createElement("span"); |
887 |
dots.className = "event-dots"; |
899 |
dots.className = "event-dots"; |
888 |
dayElem.appendChild(dots); |
900 |
dayElem.appendChild(dots); |
889 |
bookingsByDate[dateString].forEach(item => { |
901 |
bookingsByDate[dateString].forEach(item => { |
890 |
const dot = document.createElement("span"); |
902 |
const dot = document.createElement("span"); |
891 |
dot.className = "event item_" + item; |
903 |
dot.className = "event item_" + item; |
892 |
dots.appendChild(dot); |
904 |
dots.appendChild(dot); |
893 |
}); |
905 |
}); |
|
|
906 |
} |
894 |
} |
907 |
} |
895 |
}); |
908 |
); |
896 |
} |
909 |
} |
897 |
|
910 |
|
898 |
// Add hints for days before the start range and after the end range |
911 |
// Add hints for days before the start range and after the end range |