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