1/** 2 * Widget for Daily Pricing Date Selection 3 * @author Ryan Schingeck 4 * @created 2017-12-18 5 */ 6 7(function ($, undefined) { 8 "use strict"; 9 10 $.widget("rec1.catalogItemRegistrationDailyPricingDateSelection", { 11 12 options: { 13 selectedValues: null, 14 item: null, 15 requireOne: false, 16 }, 17 18 _create: function () { 19 var _this = this; 20 this.$modal = this.element; 21 this.$contents = this.$modal.find(".modal-body"); 22 this.components = {}; 23 this.registrations = []; 24 this.registrationsByDay = []; 25 this.selectedUserIds = []; 26 this.selectedUsers = {}; 27 this.scheduleAvailability = {}; 28 this.participantCounts = {}; 29 this.dailyPriceRates = []; 30 31 this.element.on("shown.bs.modal", function () { 32 _this.components.calendar.$element.fullCalendar('render'); 33 _this.components.calendar.rendered = true; 34 }); 35 36 }, 37 38 load: function () { 39 var _this = this, 40 $modal = this.$modal; 41 $modal.loadingmask(); 42 var promise = this.options.item.dailyPricingDateSelection(null, null, null, this.options.selectedValues); 43 promise 44 .done(function (data) { 45 _this.setData(data); 46 }) 47 .always(function () { 48 $modal.loadingmask('remove'); 49 }); 50 }, 51 52 show: function () { 53 this.$modal.modal('show'); 54 }, 55 56 setData: function (data) { 57 this.setUsers(data.users); 58 this.setDefaultSelectedUsers(data.users); 59 this.render(data.html); 60 this.components.usersPanel.refresh(); 61 this.refresh(); 62 }, 63 64 setDefaultSelectedUsers: function (users) { 65 var selectedUserIds = Object.keys(users); 66 for (var i in selectedUserIds) { 67 this.setSelectedUser(selectedUserIds[i], true); 68 } 69 }, 70 71 setUsers: function (users) { 72 this.users = users; 73 }, 74 75 render: function (html) { 76 this.$contents.html(html); 77 this.initComponents(); 78 }, 79 80 initComponents: function () { 81 var element = this.element, 82 _this = this, 83 $footer = element.find(".modal-footer"); 84 85 this.components = { 86 calendar: { 87 $element: element.find(".dps-calendar"), 88 initialized: false, 89 dirty: true, 90 initialize: this.initCalendar.bind(this), 91 refresh: this.refreshCalendar.bind(this), 92 availabilityDirty: true 93 }, 94 95 usersPanel: { 96 $element: element.find(".dps-users-panel"), 97 initialized: false, 98 dirty: false, 99 initialize: this.initUsersPanel.bind(this), 100 refresh: this.refreshUsersPanel.bind(this) 101 }, 102 103 selectionsPanel: { 104 $element: element.find(".dps-selections-panel"), 105 initialized: false, 106 dirty: true, 107 initialize: this.initSelectionsPanel.bind(this), 108 refresh: this.refreshSelectionsPanel.bind(this) 109 }, 110 111 controlButtons: { 112 $element: $footer, 113 $addToCart: $footer.find(".add-to-cart"), 114 initialized: false, 115 dirty: true, 116 initialize: this.initControlButtons.bind(this), 117 refresh: this.refreshControlButtons.bind(this) 118 } 119 }; 120 }, 121 122 initCalendar: function () { 123 this.initEventSources(); 124 125 this.components.calendar.$element.fullCalendar({ 126 selectable: true, 127 selectHelper: true, 128 eventLimit: true, 129 displayEventEnd: true, 130 select: this._selectedDateRange.bind(this), 131 loading: this._calendarLoading.bind(this), 132 viewRender: this._calendarViewRender.bind(this), 133 eventClick: this._calendarEventClicked.bind(this), 134 eventRender: this._calendarEventRender.bind(this), 135 header: { 136 left: 'title', 137 center: '', 138 right: 'today prev,next' 139 }, 140 eventSources: this.calendarEventSources, 141 selectConstraint: null, 142 height: 600, 143 longPressDelay: 0 144 }); 145 }, 146 147 initEventSources: function () { 148 this.calendarEventSources = [ 149 { 150 name: 'dateSelections', 151 events: this.getDateSelectionEvents.bind(this), 152 editable: false, 153 className: '' 154 } 155 ]; 156 }, 157 158 _calendarLoading: function () { 159 160 }, 161 162 getDateSelectionEvents: function (start, end, timezone, callback) { 163 var registrations = this.registrations, 164 events = []; 165 166 $.each(registrations, function (index, event) { 167 if (event.start > end) { 168 return true; 169 } 170 events.push(event); 171 }); 172 173 callback(events); 174 }, 175 176 _selectedDateRange: function (start, end, jsEvent, view) { 177 var selectedUsers = this.selectedUsers, 178 selectedDates = this._createDateListFromRange(start, end), 179 registrations = [], 180 unavailableDateCount = 0; 181 182 if (this.selectedUserIds.length === 0) { 183 alert('No registrants are selected. Click the checkbox next to one or more registrant(s) on the left-hand panel.'); 184 return; 185 } 186 187 for (var d in selectedDates) { 188 var eventStart, eventEnd, 189 selectedDate = selectedDates[d]; 190 191 192 eventStart = $.fullCalendar.moment(selectedDate, 'YYYY-MM-DD'); 193 eventEnd = $.fullCalendar.moment(selectedDate, 'YYYY-MM-DD'); 194 195 if (eventStart > eventEnd) { 196 eventEnd.add(1, 'day'); 197 } 198 199 eventStart.stripZone(); 200 eventEnd.stripZone(); 201 202 203 if (!this.isAvailableDate(eventStart)) { 204 unavailableDateCount++; 205 continue; 206 } 207 208 209 for (var u in selectedUsers) { 210 var user = selectedUsers[u]; 211 212 var weekDescription = this.getWeekDescription(eventStart); 213 214 var registration = { 215 user: user, 216 date: selectedDate, 217 allDay: true, 218 start: eventStart, 219 end: eventEnd, 220 title: user.fullName, 221 weekDescription: weekDescription, 222 weekKey: eventStart.clone().week() + '-' + user.id, 223 description: selectedDate, 224 className: [ 225 'bg-color-' + user.colorCode, 226 'border-color-' + user.colorCode 227 ], 228 registrations: [], 229 weekdays: [], 230 warning: null, 231 error: null 232 }; 233 registrations.push(registration); 234 } 235 } 236 237 // Hide FullCalendar's selection helper 238 this.components.calendar.$element.fullCalendar('unselect'); 239 this.addRegistrations(registrations); 240 this.detectAllRegistrationWarnings(); 241 this.components.calendar.dirty = true; 242 this.components.selectionsPanel.dirty = true;
243 this.components.controlButtons.dirty = true; 244 this.refresh(); 245 246 if (unavailableDateCount === 1) { 247 alert("There was an unavailable date selected. Only the days highlighted in green/yellow are available."); 248 } else if (unavailableDateCount > 1) { 249 alert("There were " + unavailableDateCount + " unavailable dates selected. Only the days highlighted in green/yellow are available."); 250 } 251 }, 252 253 _createDateListFromRange: function (start, end) { 254 var dateList = []; 255 for (var currentDate = moment(start); currentDate.isBefore(end); currentDate.add(1, 'days')) { 256 dateList.push(currentDate.format('YYYY-MM-DD')); 257 } 258 return dateList; 259 }, 260 261 isAvailableDate: function (date) { 262 var year = date.year(), 263 month = date.month() + 1, 264 mDate = date.date(); 265 266 return this.scheduleAvailability[year] 267 && this.scheduleAvailability[year][month] 268 && this.scheduleAvailability[year][month][mDate]; 269 270 }, 271 272 getParticipantCountForDate: function (date) { 273 var dateFormat = date.format("MM/DD/YYYY"); 274 if (this.participantCounts.hasOwnProperty(dateFormat)) { 275 return this.participantCounts[dateFormat]; 276 } 277 return null; 278 }, 279 280 getWeekDescription: function (momentInstance) { 281 var firstDay = momentInstance.clone(), 282 lastDay = momentInstance.clone(); 283 return 'Week of ' + firstDay.day(0).format("M/D") + ' - ' 284 + lastDay.day(6).format("M/D"); 285 }, 286 287 addRegistrations: function (registrations) { 288 for (var i = 0; i < registrations.length; i++) { 289 this._addRegistration(registrations[i]); 290 } 291 }, 292 293 _calendarEventClicked: function () { 294 295 }, 296 297 _findRegistrationByDateAndUser: function (date, userId) { 298 var registrations = this.registrations, 299 registrationFound = null; 300 301 $.each(registrations, function (index, registration) { 302 if (registration.date === date && registration.user.id === userId) { 303 registrationFound = registration; 304 return true; 305 } 306 }); 307 308 return registrationFound; 309 }, 310 311 _addRegistration: function (registration) { 312 313 314 // already found for date, do not add again 315 if (this._findRegistrationByDateAndUser(registration.date, registration.user.id)) { 316 return; 317 } 318 319 this.registrations.push(registration); 320 321 var value = { 322 description: moment(registration.date).format("MM/DD/YYYY"), 323 date: registration.date, 324 rate: this._getRateByDate(moment(registration.date)), 325 registrationsByUser: {} 326 }; 327 value.registrationsByUser[registration.user.id] = registration; 328 if (value.rate !== null) { 329 value.description += " - " + value.rate.title; 330 } 331 332 var index = -1; 333 $.each(this.registrationsByDay, function (i, registrationByDay) { 334 if (registrationByDay.date === registration.date) { 335 index = i; 336 return; 337 } 338 }); 339 340 if (index === -1) { 341 this.registrationsByDay.push(value); 342 } else { 343 var day = this.registrationsByDay[index]; 344 // User is already in the date 345 if (!day.registrationsByUser[registration.user.id]) { 346 day.registrationsByUser[registration.user.id] = registration; 347 } 348 } 349 }, 350 351 getDaysDescription: function (days) { 352 var dayDescriptions = []; 353 days.sort(); 354 for (var i in days) { 355 dayDescriptions.push(moment.weekdaysMin(days[i])); 356 } 357 return dayDescriptions.join('/'); 358 }, 359 360 361 detectAllRegistrationWarnings: function () { 362 // Detect if restrictions apply on the # of days selected 363 var _this = this, 364 hasWarnings = false;
365 this._iterateRegistrationsByUser(function (day, userId, registration) { 366 var thisHasWarnings = _this.detectRegistrationWarning(registration); 367 hasWarnings = hasWarnings || thisHasWarnings; 368 }); 369 this.hasWarnings = hasWarnings; 370 }, 371 372 detectRegistrationWarning: function (registration) { 373 // rate = this._getRate(dayCount), 374 // validRate = rate || this._getValidRate(dayCount); 375 376 // No rate 377 // if(!validRate) { 378 // registration.warning = this._getWarningForInvalidDayCount(registration, dayCount); 379 //} 380 // else if(this._registrationHasRestrictedWeekdays(registration, validRate)) { 381 // registration.warning = this._getWarningForRestrictedWeekdays(registration, validRate); 382 //} 383 // else { 384 // registration.warning = null; 385 // } 386 387 return !!registration.warning; // bool 388 }, 389 390 _iterateRegistrationsByUser: function (callback) { 391 $.each(this.registrationsByDay, function (dayIndex, day) { 392 $.each(day.registrationsByUser, function (userId, registration) { 393 callback.apply(this, [day, userId, registration]); 394 }); 395 }); 396 }, 397 398 _calendarViewRender: function () { 399 // Render cached availability 400 this._renderCalendarAvailability(); 401 this._renderCalendarParticipantCounts(); 402 this.refreshCalendarAvailability(); 403 }, 404 405 _calendarEventRender: function (event, element, view) { 406 var _this = this, 407 $close = $('<div class="dps-remove-registration anchor-right-xs"><i class="flaticon-close"></i></div>'), 408 $content = element.children('.fc-content'); 409 $close.on('click', function (e) { 410 _this.removeRegistration(event); 411 _this.detectAllRegistrationWarnings(); 412 _this.components.calendar.dirty = true; 413 _this.components.selectionsPanel.dirty = true; 414 _this.components.controlButtons.dirty = true; 415 _this.refresh(); 416 }); 417 $content.append($close); 418 }, 419 420 removeRegistration: function (registration) { 421 if (registration === null) { 422 return; 423 } 424 var _this = this; 425 426 var reservationToRemoveIndex = -1; 427 $.each(this.registrations, function (index, saved) { 428 429 if (saved.date === registration.date 430 && saved.user.id === registration.user.id) { 431 _this.registrations.splice(index, 1); 432 return false; 433 } 434 }); 435 436 $.each(this.registrationsByDay, function (index, registrationDay) { 437 if (registrationDay.date === registration.date 438 && registrationDay.registrationsByUser.hasOwnProperty(registration.user.id)) { 439 delete _this.registrationsByDay[index].registrationsByUser[registration.user.id]; 440 441 if (Object.keys(registrationDay.registrationsByUser).length === 0) { 442 _this.registrationsByDay.splice(index, 1); 443 } 444 return false 445 } 446 }); 447 448 }, 449 450 initUsersPanel: function () { 451 var _this = this, 452 component = this.components.usersPanel, 453 $element = component.$element; 454 455 $element.on("change", "input", function (event) { 456 _this.setSelectedUser( 457 this.value, 458 this.checked 459 ); 460 }); 461 }, 462 463 setSelectedUser: function (userId, selected) { 464 if (selected) { 465 this.selectedUserIds.push(userId); 466 this.selectedUsers[userId] = this.users[userId]; 467 } else { 468 this.selectedUserIds.splice( 469 this.selectedUserIds.indexOf(userId), 470 1 471 ); 472 delete this.selectedUsers[userId]; 473 } 474 }, 475 476 initSelectionsPanel: function () { 477 var component = this.components.selectionsPanel, 478 $element = component.$element; 479 480 this._on($element, { 481 "click .dps-remove-registration": function (event) { 482 var $target = $(event.currentTarget), 483 $li = $target.closest('li'), 484 userId = $li.data('user'), 485 date = $li.data('date'); 486 487 this.removeRegistration(this._findRegistrationByDateAndUser(date, userId.toString())); 488 this.detectAllRegistrationWarnings(); 489 this.components.selectionsPanel.dirty = true; 490 this.components.calendar.dirty = true;
491 this.components.controlButtons.dirty = true; 492 this.refresh(); 493 } 494 }); 495 }, 496 497 initControlButtons: function () { 498 this._on(this.components.controlButtons.$addToCart, { 499 "click": "addToCart" 500 }); 501 }, 502 503 addToCart: function (event) { 504 var _this = this, 505 $modal = this.$modal, 506 $contents = this.$contents, 507 users = {}; 508 for (var i in this.users) { 509 users[i] = { 510 dates: [], 511 override: false 512 }; 513 } 514 515 $.each(this.registrations, function (index, registration) { 516 users[registration.user.id].dates.push(registration.start.format('M/D/YYYY')); 517 users[registration.user.id].override = $.inArray(registration.user.id.toString(), _this.options.selectedValues.override) !== -1; 518 }); 519 520 var userDateMapping = { 521 userDateMapping: users 522 }; 523 524 525 $contents.loadingmask(); 526 if (!_this.options.requireOne) { 527 this.options.item.registerDailyPriceDateSelections(userDateMapping).done(function (data) { 528 $modal.modal('hide'); 529 530 531 if (data.warnings && data.warnings.length > 0 && data.newItemCount > 0) { 532 alert('Only ' + data.newItemCount + ' ' + (data.newItemCount == 1 ? 'item was' : 'items were') + 533 ' added to your cart.\n\nThe following warning(s) occurred:\n - ' + 534 data.warnings.join('\n - ')); 535 } 536 537 }).always(function () { 538 $contents.loadingmask('remove'); 539 }); 540 } else { 541 this.options.item.selectSessionPrerequisites(null, null, userDateMapping, _this.options.selectedValues); 542 $modal.modal('hide'); 543 } 544 }, 545 546 refreshCalendar: function () { 547 if (this.components.calendar.rendered) { 548 this.components.calendar.$element.fullCalendar('refetchEvents'); 549 this.components.calendar.availabilityDirty && this.refreshCalendarAvailability(); 550 } 551 }, 552 553 refreshCalendarAvailability: function () { 554 555 var _this = this, 556 $calendar = this.components.calendar.$element, 557 view = $calendar.fullCalendar('getView'), 558 start = view.start.format('YYYY-MM-DD'), 559 end = view.end.format('YYYY-MM-DD'); 560 561 this.components.calendar.availabilityDirty = false; 562 563 var promise = this.options.item.getScheduleAvailability(start, end); 564 565 promise.done(function (data) { 566 _this.setScheduleAvailability(data.availability); 567 _this.setParticipantCounts(data.participantCounts); 568 _this.setDailyPriceRates(data.dailyPriceRates); 569 _this._renderCalendarAvailability(); 570 _this._renderCalendarParticipantCounts(); 571 }); 572 }, 573 574 setScheduleAvailability: function (availability) { 575 $.extend(true, this.scheduleAvailability, availability); 576 }, 577 578 setParticipantCounts: function (participantCounts) { 579 $.extend(true, this.participantCounts, participantCounts); 580 }, 581 582 setDailyPriceRates: function (dailyPriceRates) { 583 var _this = this; 584 $.each(dailyPriceRates, function (index, rate) { 585 var found = false; 586 $.each(_this.dailyPriceRates, function (savedIndex, savedRate) { 587 if (rate.date === savedRate.date) { 588 found = true; 589 return; 590 } 591 }); 592 if (!found) { 593 _this.dailyPriceRates.push(rate); 594 } 595 }); 596 }, 597 598 _renderCalendarAvailability: function () { 599 600 if (typeof this.scheduleAvailability !== "object") { 601 return; 602 } 603 604 var _this = this, 605 $calendar = this.components.calendar.$element, 606 $days = $calendar.find(".fc-day"); 607 608 var availability = this.scheduleAvailability, 609 classNames = "day-partial day-available day-unavailable bg-warning bg-success bg-muted"; 610 611 $days.each(function (index) { 612 var $day = $(this), 613 date = moment($day.data('date')),
614 month = date.month() + 1, 615 mDate = date.date(), 616 year = date.year(), 617 className = 'day-unavailable bg-muted'; 618 619 if (_this.isAvailableDate(date)) { 620 var thisAvailability = availability[year][month][mDate]; 621 switch (thisAvailability) { 622 case 1: 623 className = "day-partial bg-warning"; 624 break; 625 case 2: 626 className = "day-available bg-success"; 627 break; 628 } 629 630 $day.closest("td").attr("data-container", ".dps-calendar-panel"); 631 var rate = _this._getRateByDate(date); 632 if (rate !== null) { 633 if (!$day.find(".day-label").length) { 634 var $tdCell = $day.closest("td"); 635 636 $tdCell 637 .attr("title", rate.title) 638 .attr("tabindex", 0) 639 .attr("role", "button") 640 .attr("aria-label", rate.title) 641 .on('keydown', function (event) { 642 if (event.key === "Enter" || event.keyCode === 13) { 643 event.preventDefault(); 644 645 // Create the start and end moment objects for selection 646 var cellDate = moment($day.data('date')); 647 var endDate = cellDate.clone().add(1, 'days'); 648 649 // Use the public select method to trigger the selection 650 $calendar.fullCalendar('select', cellDate, endDate); 651 652 // Focus back on the cell for continued navigation 653 setTimeout(function () { 654 $tdCell.focus(); 655 }, 100); 656 } 657 }); 658 659 $day.closest("td").tooltip(); 660 661 var $title = $('<div class="day-label"></div>'); 662 $title.text(rate.title); 663 $day.attr("title", rate.title).prepend($title); 664 } 665 } 666 } 667 668 $day.removeClass(classNames).addClass(className); 669 }); 670 }, 671 672 _renderCalendarParticipantCounts: function () { 673 674 if (typeof this.participantCounts !== "object") { 675 return; 676 } 677 678 var _this = this, 679 $calendar = this.components.calendar.$element, 680 $days = $calendar.find(".fc-day"); 681 682 $days.each(function (index) { 683 var $day = $(this), 684 date = moment($day.data('date')); 685 686 if (_this.getParticipantCountForDate(date)) { 687 var count = _this.getParticipantCountForDate(date); 688 689 var $countLabel = $('<div></div>'); 690 $countLabel[0].className = "dropin-participant-count-label fc-event"; 691 $countLabel.html(count + " Enrolled"); 692 $countLabel.attr("title", $countLabel.text()); 693 694 $day.closest("td.fc-day").append($countLabel); 695 } 696 }); 697 }, 698 699 _getRateByDate: function (date) { 700 for (var i in this.dailyPriceRates) { 701 if (this.dailyPriceRates[i].date === date.format("YYYY-MM-DD")) { 702 return this.dailyPriceRates[i]; 703 } 704 } 705 return null; 706 }, 707 708 refreshUsersPanel: function () { 709 var selectedUsers = this.selectedUserIds; 710 711 this.components.usersPanel.$element.find("input[type='checkbox']").each(function () { 712 if (selectedUsers.indexOf($(this).val()) != -1) { 713 $(this).prop("checked", "checked"); 714 } 715 }); 716 }, 717 718 refreshSelectionsPanel: function () { 719 // We're going to just re-render the selections panel 720 var _this = this, 721 html = ''; 722 723 html = '<ul class="ui-listview"><li class="ui-listview-heading">Selected Dates</li>'; 724 if (this.registrationsByDay.length === 0) { 725 html += '<p class="ui-voffset-lg text-center text-muted">No dates selected</p>'; 726 } 727 728 var registrations = this.registrationsByDay; 729 // sort by days 730 registrations.sort(function (reg1, reg2) { 731 var reg1Date = moment(reg1.date), 732 reg2Date = moment(reg2.date); 733 734 if (reg1Date < reg2Date) { 735 return -1; 736 } else if (reg1Date > reg2Date) { 737 return 1; 738 } 739 return 0; 740 }); 741 742 $.each(registrations, function (index, registrationDay) { 743 html += _this._renderRegistrationGroup(registrationDay.date, 744 registrationDay.description, 745 registrationDay.registrationsByUser); 746 }); 747 html += '</ul>'; 748 749 this.components.selectionsPanel.$element.html(html); 750 }, 751 752 refreshControlButtons: function () {
753 var $addToCart = this.components.controlButtons.$addToCart, 754 disabled = this.hasWarnings || this.registrations.length === 0; 755 $addToCart.prop('disabled', disabled); 756 }, 757 758 _renderRegistrationGroup: function (date, description, registrations) { 759 760 var _this = this, 761 rate = _this._getRateByDate(moment(date)); 762 763 var html = '<li class="heading-muted ui-voffset-lg"><div class="pull-left">' + description + '</div>' + 764 '<div class="pull-right"><span class="badge">' + Object.keys(registrations).length + ' </span></div>' + 765 '</li>'; 766 767 $.each(registrations, function (userId, registration) { 768 var warnings = _this._renderRegistrationWarning(registration); 769 html += '<li class="color-' + registration.user.colorCode + '" ' + 770 'data-user="' + userId + '" data-date="' + date + '"> $' + rate.price + " - " + 771 registration.user.fullName + 772 '<div class="pull-right">' + 773 ' <a href="javascript:void(0);" class="flaticon-close dps-remove-registration ui-offset-sm text-muted"></a>' + 774 '</div>' + 775 warnings + 776 '</li>'; 777 }); 778 return html; 779 }, 780 781 _renderRegistrationWarning: function (registration) { 782 if (registration.warning) { 783 var html = '<div class="text-muted">' + 784 '<i class="flaticon-warning2 text-warning"></i> ' + registration.warning + 785 '</div>'; 786 return html; 787 } 788 return ''; 789 }, 790 791 refresh: function () { 792 $.each(this.components, function (name, component) { 793 component.initialized || component.initialize(); 794 component.dirty && component.refresh(); 795 796 component.initialized = true; 797 component.dirty = false; 798 }); 799 } 800 801 }); 802 803 804})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.