1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * Creates a left pane view for suggesting time/locations 26 * @constructor 27 * @class 28 * This class displays suggested free time/location for sending invites to attendees 29 * 30 * @author Sathishkumar Sugumaran 31 * 32 * @param parent [ZmApptComposeView] the appt compose view 33 * @param controller [ZmApptComposeController] the appt compose controller 34 */ 35 ZmScheduleAssistantView = function(parent, controller, apptEditView, closeCallback) { 36 this._kbMgr = appCtxt.getKeyboardMgr(); 37 this._attendees = []; 38 this._workingHours = {}; 39 this._fbStat = new AjxVector(); 40 this._fbStatMap = {}; 41 this._schedule = {}; 42 43 ZmApptAssistantView.call(this, parent, controller, apptEditView, closeCallback); 44 }; 45 46 ZmScheduleAssistantView.prototype = new ZmApptAssistantView; 47 ZmScheduleAssistantView.prototype.constructor = ZmScheduleAssistantView; 48 49 50 ZmScheduleAssistantView.prototype.toString = 51 function() { 52 return "ZmScheduleAssistantView"; 53 } 54 55 ZmScheduleAssistantView.prototype.cleanup = 56 function() { 57 this._attendees = []; 58 this._schedule = {}; 59 60 this._manualOverrideFlag = false; 61 if(this._currentSuggestions) this._currentSuggestions.removeAll(); 62 if(this._miniCalendar) this.clearMiniCal(); 63 64 }; 65 66 ZmScheduleAssistantView.prototype._createMiniCalendar = 67 function(date) { 68 date = date ? date : new Date(); 69 70 var firstDayOfWeek = appCtxt.get(ZmSetting.CAL_FIRST_DAY_OF_WEEK) || 0; 71 72 //todo: need to use server setting to decide the weekno standard 73 var serverId = AjxTimezone.getServerId(AjxTimezone.DEFAULT); 74 var useISO8601WeekNo = (serverId && serverId.indexOf("Europe")==0 && serverId != "Europe/London"); 75 76 this._miniCalendar = new ZmMiniCalendar({parent: this, posStyle:DwtControl.RELATIVE_STYLE, 77 firstDayOfWeek: firstDayOfWeek, showWeekNumber: appCtxt.get(ZmSetting.CAL_SHOW_CALENDAR_WEEK), 78 useISO8601WeekNo: useISO8601WeekNo}); 79 this._miniCalendar.setDate(date); 80 this._miniCalendar.setScrollStyle(Dwt.CLIP); 81 this._miniCalendar.addSelectionListener(new AjxListener(this, this._miniCalSelectionListener)); 82 this._miniCalendar.addDateRangeListener(new AjxListener(this, this._miniCalDateRangeListener)); 83 this._miniCalendar.setMouseOverDayCallback(new AjxCallback(this, this._miniCalMouseOverDayCallback)); 84 this._miniCalendar.setMouseOutDayCallback(new AjxCallback(this, this._miniCalMouseOutDayCallback)); 85 86 var workingWeek = []; 87 for (var i = 0; i < 7; i++) { 88 var d = (i + firstDayOfWeek) % 7; 89 workingWeek[i] = (d > 0 && d < 6); 90 } 91 this._miniCalendar.setWorkingWeek(workingWeek); 92 93 var app = appCtxt.getApp(ZmApp.CALENDAR); 94 var show = app._active || appCtxt.get(ZmSetting.CAL_ALWAYS_SHOW_MINI_CAL); 95 this._miniCalendar.setSkipNotifyOnPage(show && !app._active); 96 if (!app._active) { 97 this._miniCalendar.setSelectionMode(DwtCalendar.DAY); 98 } 99 100 this._miniCalendar.reparentHtmlElement(this._htmlElId + "_suggest_minical"); 101 }; 102 103 ZmScheduleAssistantView.prototype._configureSuggestionWidgets = 104 function() { 105 this._timeSuggestions = new ZmTimeSuggestionView(this, this._controller, this._apptView); 106 this._timeSuggestions.reparentHtmlElement(this._suggestionsView); 107 this._suggestTime = true; 108 this._currentSuggestions = this._timeSuggestions; 109 110 this._locationSuggestions = new ZmLocationSuggestionView(this, this._controller, this._apptView); 111 this._locationSuggestions.reparentHtmlElement(this._suggestionsView); 112 113 this._resetSize(); 114 } 115 116 ZmScheduleAssistantView.prototype.show = 117 function(suggestTime) { 118 this._enabled = true; 119 120 this._suggestTime = suggestTime; 121 if (this._suggestTime) { 122 this.updateTime(true, true); 123 Dwt.setInnerHtml(this._suggestionName, ZmMsg.suggestedTimes); 124 this._locationSuggestions.setVisible(false); 125 this._timeSuggestions.setVisible(true); 126 Dwt.setVisible(this._suggestMinical, true); 127 this._currentSuggestions = this._timeSuggestions; 128 } else { 129 Dwt.setInnerHtml(this._suggestionName, ZmMsg.suggestedLocations); 130 this._timeSuggestions.setVisible(false); 131 Dwt.setVisible(this._suggestMinical, false); 132 this._locationSuggestions.setVisible(true); 133 this._currentSuggestions = this._locationSuggestions; 134 } 135 136 this._resetSize(); 137 }; 138 139 ZmScheduleAssistantView.prototype.suggestAction = 140 function(focusOnSuggestion, showAllSuggestions) { 141 142 if(appCtxt.isOffline && !appCtxt.isZDOnline()) { return; } 143 144 var params = { 145 items: [], 146 itemIndex: {}, 147 focus: focusOnSuggestion, 148 showOnlyGreenSuggestions: !showAllSuggestions 149 }; 150 151 this._currentSuggestions.setLoadingHtml(); 152 // Location information is required even for a time search, since the time display indicates locations available 153 // at that time. Use isSuggestRooms to only do so when GAL_ENABLED is true. 154 if ((this._resources.length == 0) && this.isSuggestRooms()) { 155 this.searchCalendarResources(new AjxCallback(this, this._findFreeBusyInfo, [params])); 156 } else { 157 this._findFreeBusyInfo(params); 158 } 159 }; 160 161 162 ZmScheduleAssistantView.prototype.getLocationFBInfo = 163 function(fbCallback, fbCallbackObj, endTime) { 164 165 if(appCtxt.isOffline && !appCtxt.isZDOnline()) { return; } 166 167 var params = { 168 items: [], 169 itemIndex: {}, 170 focus: false, 171 fbEndTime: endTime, 172 showOnlyGreenSuggestions: true 173 }; 174 params.fbCallback = fbCallback.bind(fbCallbackObj, params); 175 176 if(this._resources.length == 0) { 177 this.searchCalendarResources(new AjxCallback(this, this._findFreeBusyInfo, [params])); 178 } else { 179 this._findFreeBusyInfo(params); 180 } 181 }; 182 183 184 185 ZmScheduleAssistantView.prototype._getTimeFrame = 186 function() { 187 var di = {}; 188 ZmApptViewHelper.getDateInfo(this._apptView, di); 189 var startDate = this._date; 190 if (!this._date || !this._suggestTime) { 191 startDate = AjxDateUtil.simpleParseDateStr(di.startDate); 192 } 193 var endDate = new Date(startDate); 194 startDate.setHours(0, 0, 0, 0); 195 endDate.setTime(startDate.getTime() + AjxDateUtil.MSEC_PER_DAY); 196 return {start:startDate, end:endDate}; 197 }; 198 199 ZmScheduleAssistantView.prototype._miniCalSelectionListener = 200 function(ev) { 201 if (ev.item instanceof ZmMiniCalendar) { 202 var date = ev.detail; 203 204 // *** Separate Suggestions pane, only invoked to show suggestions, so changing 205 // force refresh to True 206 this.reset(date, this._attendees, true); 207 208 //set edit view start/end date 209 var duration = this._apptView.getDurationInfo().duration; 210 var endDate = new Date(date.getTime() + duration); 211 this._apptView.setDate(date, endDate, true); 212 } 213 }; 214 215 ZmScheduleAssistantView.prototype.updateTime = 216 function(clearSelection, forceRefresh) { 217 if(clearSelection) this._date = null; 218 var tf = this._getTimeFrame(); 219 this._miniCalendar.setDate(tf.start, true); 220 this.reset(tf.start, this._attendees, forceRefresh); 221 appCtxt.notifyZimlets("onEditAppt_updateTime", [this._apptView, tf]);//notify Zimlets 222 }; 223 224 ZmScheduleAssistantView.prototype.getOrganizer = 225 function() { 226 return this._apptView._isProposeTime ? this._apptView.getCalItemOrganizer() : this._apptView.getOrganizer(); 227 }; 228 229 ZmScheduleAssistantView.prototype.addOrganizer = 230 function() { 231 //include organizer in the scheduler suggestions 232 var organizer = this._apptView.getOrganizer(); 233 this._attendees.push(organizer.getEmail()); 234 }; 235 236 ZmScheduleAssistantView.prototype.updateAttendees = 237 function(attendees) { 238 239 if(attendees instanceof AjxVector) attendees = attendees.getArray(); 240 241 this._attendees = []; 242 243 this.addOrganizer(); 244 245 var attendee; 246 for (var i = attendees.length; --i >= 0;) { 247 attendee = attendees[i].getEmail(); 248 if (attendee instanceof Array) { 249 attendee = attendee[i][0]; 250 } 251 this._attendees.push(attendee); 252 } 253 254 // *** Separate Suggestions pane, only invoked to show suggestions, so changing 255 // force refresh to True 256 this.reset(this._date, this._attendees, true); 257 }; 258 259 ZmScheduleAssistantView.prototype.updateAttendee = 260 function(attendee) { 261 262 var email = (typeof attendee == 'string') ? attendee : attendee.getEmail(); 263 if(this._attendees.length == 0) { 264 this.addOrganizer(); 265 this._attendees.push(email); 266 }else { 267 var found = false; 268 for (var i = this._attendees.length; --i >= 0;) { 269 if(email == this._attendees[i]) { 270 found = true; 271 break; 272 } 273 } 274 if(!found) this._attendees.push(email); 275 } 276 277 // *** Separate Suggestions pane, only invoked to show suggestions, so changing 278 // force refresh to True 279 this.reset(this._date, this._attendees, true); 280 }; 281 282 283 ZmScheduleAssistantView.prototype.reset = 284 function(date, attendees, forceRefresh) { 285 this._date = date || this._miniCalendar.getDate(); 286 if(!this._apptView.isSuggestionsNeeded() || !this.isSuggestionsEnabled()) { 287 var isGalEnabled = appCtxt.get(ZmSetting.GROUP_CALENDAR_ENABLED) && appCtxt.get(ZmSetting.GAL_ENABLED); 288 if(this._timeSuggestions && !isGalEnabled) this._timeSuggestions.removeAll(); 289 this.clearMiniCal(); 290 if(!this.isSuggestionsEnabled()) { 291 if(isGalEnabled) this._timeSuggestions.setShowSuggestionsHTML(this._date); 292 } 293 this._resetSize(); 294 return; 295 } 296 297 var newDuration = this._apptView.getDurationInfo().duration; 298 var newKey = this.getFormKey(this._date, attendees); 299 if(newKey != this._key || newDuration != this._duration) { 300 if(this._currentSuggestions){ 301 this._currentSuggestions.removeAll(); 302 this.clearMiniCal(); 303 } 304 if(forceRefresh) this.suggestAction(false, false); 305 } 306 307 this._resetSize(); 308 }; 309 310 ZmScheduleAssistantView.prototype._miniCalDateRangeListener = 311 function(ev) { 312 //clear current mini calendar suggestions 313 this._miniCalendar.setColor({}, true, {}); 314 if(!this._apptView.isSuggestionsNeeded()) return; 315 this.highlightMiniCal(); 316 }; 317 318 ZmScheduleAssistantView.prototype._miniCalMouseOverDayCallback = 319 function(control, day) { 320 this._currentMouseOverDay = day; 321 //todo: add code if tooltip needs to be supported 322 }; 323 324 ZmScheduleAssistantView.prototype._miniCalMouseOutDayCallback = 325 function(control) { 326 this._currentMouseOverDay = null; 327 }; 328 329 330 //smart scheduler suggestion modules 331 332 // This should only be called for time suggestions 333 ZmScheduleAssistantView.prototype._findFreeBusyInfo = 334 function(params) { 335 336 var currAcct = this._apptView.getCalendarAccount(); 337 // Bug: 48189 Don't send GetFreeBusyRequest for non-ZCS accounts. 338 if (appCtxt.isOffline && (!currAcct.isZimbraAccount || currAcct.isMain)) { 339 //todo: avoid showing smart scheduler button for non-ZCS accounts - offline client 340 return; 341 } 342 343 var tf = this._timeFrame = this._getTimeFrame(); 344 if (params.fbEndTime) { 345 // Override the time frame. Used for checking location 346 // recurrence collisions 347 tf.end = new Date(params.fbEndTime); 348 } 349 var emails = [], attendeeEmails = [], email; 350 351 params.itemIndex = {}; 352 params.items = []; 353 params.timeFrame = tf; 354 355 this._copyResourcesToParams(params, emails); 356 357 var attendees = this._apptView.getRequiredAttendeeEmails(); 358 this._attendees = []; 359 360 361 var attendee; 362 for (var i = attendees.length; --i >= 0;) { 363 this._addAttendee(attendees[i], params, emails, attendeeEmails); 364 } 365 params._nonOrganizerAttendeeEmails = attendeeEmails.slice(); 366 //include organizer in the scheduler suggestions 367 var organizer = this.getOrganizer(); 368 this._addAttendee(organizer.getEmail(), params, emails, attendeeEmails); 369 370 params.emails = emails; 371 params.attendeeEmails = attendeeEmails; 372 373 this._key = this.getFormKey(tf.start, this._attendees); 374 375 if((this._attendees.length == 0) && this._suggestTime) { 376 this._timeSuggestions.setNoAttendeesHtml(); 377 return; 378 } 379 380 if (this._freeBusyRequest) { 381 appCtxt.getRequestMgr().cancelRequest(this._freeBusyRequest, null, true); 382 } 383 384 var callback; 385 if (params.fbCallback) { 386 // Custom FB processing 387 callback = params.fbCallback; 388 } else { 389 if (this._suggestTime) { 390 callback = new AjxCallback(this, this.getWorkingHours, [params]); 391 } else { 392 callback = new AjxCallback(this, this.suggestLocations, [params]); 393 } 394 } 395 396 var acct = (appCtxt.multiAccounts) ? this._apptView.getCalendarAccount() : null; 397 var fbParams = { 398 startTime: tf.start.getTime(), 399 endTime: tf.end.getTime(), 400 emails: emails, 401 callback: callback, 402 errorCallback: callback, 403 noBusyOverlay: true, 404 account: acct 405 }; 406 407 this._freeBusyRequest = this._fbCache.getFreeBusyInfo(fbParams); 408 }; 409 410 ZmScheduleAssistantView.prototype._addAttendee = 411 function(attendee, params, emails, attendeeEmails) { 412 params.items.push(attendee); 413 params.itemIndex[attendee] = params.items.length-1; 414 emails.push(attendee); 415 attendeeEmails.push(attendee); 416 this._attendees.push(attendee); 417 }; 418 419 420 ZmScheduleAssistantView.prototype.getFormKey = 421 function(startDate, attendees) { 422 return startDate.getTime() + "-" + attendees.join(","); 423 }; 424 425 ZmScheduleAssistantView.prototype.clearCache = 426 function() { 427 this._organizerEmail = null; 428 this._workingHours = {}; 429 }; 430 431 ZmScheduleAssistantView.prototype.getFreeBusyKey = 432 function(timeFrame, id) { 433 return timeFrame.start.getTime() + "-" + timeFrame.end.getTime() + "-" + id; 434 }; 435 436 ZmScheduleAssistantView.prototype.getWorkingHours = 437 function(params) { 438 439 //clear fb request info 440 this._freeBusyRequest = null; 441 442 if (this._workingHoursRequest) { 443 appCtxt.getRequestMgr().cancelRequest(this._workingHoursRequest, null, true); 444 } 445 446 var onlyIncludeMyWorkingHours = params.onlyIncludeMyWorkingHours = this.isOnlyMyWorkingHoursIncluded(); 447 var onlyIncludeOthersWorkingHours = params.onlyIncludeOthersWorkingHours = this.isOnlyOthersWorkingHoursIncluded(); 448 449 if(!onlyIncludeMyWorkingHours && !onlyIncludeOthersWorkingHours) { 450 // Non-working hours can be used for the organizer and all attendees 451 this.suggestTimeSlots(params); 452 return; 453 } 454 455 var organizer = this.getOrganizer(); 456 this._organizerEmail = organizer.getEmail(); 457 458 var emails = []; 459 if (onlyIncludeOthersWorkingHours) { 460 emails = params._nonOrganizerAttendeeEmails; 461 } 462 if (onlyIncludeMyWorkingHours) { 463 emails = emails.concat([this._organizerEmail]); 464 } 465 466 var acct = (appCtxt.multiAccounts) ? this._apptView.getCalendarAccount() : null; 467 468 //optimization: fetch working hrs for a week - wrking hrs pattern repeat everyweek 469 var weekStartDate = new Date(params.timeFrame.start.getTime()); 470 var dow = weekStartDate.getDay(); 471 weekStartDate.setDate(weekStartDate.getDate()-((dow+7))%7); 472 473 474 var whrsParams = { 475 startTime: weekStartDate.getTime(), 476 endTime: weekStartDate.getTime() + 7*AjxDateUtil.MSEC_PER_DAY, 477 emails: emails, 478 callback: new AjxCallback(this, this._handleWorkingHoursResponse, [params]), 479 errorCallback: new AjxCallback(this, this._handleWorkingHoursError, [params]), 480 noBusyOverlay: true, 481 account: acct 482 }; 483 484 this._workingHoursRequest = this._fbCache.getWorkingHours(whrsParams); 485 }; 486 487 ZmScheduleAssistantView.prototype.isOnlyMyWorkingHoursIncluded = 488 function() { 489 return this._prefDialog ? 490 (this._prefDialog.getPreference(ZmTimeSuggestionPrefDialog.MY_WORKING_HOURS_FIELD) == "true") : false; 491 }; 492 ZmScheduleAssistantView.prototype.isOnlyOthersWorkingHoursIncluded = 493 function() { 494 return this._prefDialog ? 495 (this._prefDialog.getPreference(ZmTimeSuggestionPrefDialog.OTHERS_WORKING_HOURS_FIELD) == "true") : false; 496 }; 497 498 ZmScheduleAssistantView.prototype._handleWorkingHoursResponse = 499 function(params, result) { 500 501 this._workingHoursRequest = null; 502 this._workingHours = {}; 503 504 if(this._organizerEmail) { 505 this._workingHours[this._organizerEmail] = 506 this._fbCache.getWorkingHrsSlot(params.timeFrame.start.getTime(), 507 params.timeFrame.end.getTime(), this._organizerEmail); 508 } 509 if(this.isSuggestionsEnabled()) { 510 this.suggestTimeSlots(params); 511 } 512 }; 513 514 ZmScheduleAssistantView.prototype._handleWorkingHoursError = 515 function(params, result) { 516 517 this._workingHoursRequest = null; 518 this._workingHours = {}; 519 this.suggestTimeSlots(params); 520 521 }; 522 523 ZmScheduleAssistantView.prototype.suggestTimeSlots = 524 function(params) { 525 526 var startDate = this._timeFrame.start; 527 startDate.setHours(0, 0, 0, 0); 528 var startTime = startDate.getTime(); 529 530 var cDate = new Date(); 531 532 //ignore suggestions that are in past 533 if(startTime == cDate.setHours(0, 0, 0, 0)) { 534 startDate = new Date(); 535 startTime = startDate.setHours(startDate.getHours(), ((startDate.getMinutes() >=30) ? 60 : 30), 0, 0); 536 } 537 538 var endDate = new Date(startTime); 539 endDate.setHours(23, 59, 0, 0); 540 var endTime = endDate.getTime(); 541 var durationInfo = this._duration = this._apptView.getDurationInfo(); 542 543 params.duration = durationInfo.duration; 544 545 this._fbStat = new AjxVector(); 546 this._fbStatMap = {}; 547 this._totalUsers = this._attendees.length; 548 this._totalLocations = this._resources.length; 549 550 while(startTime < endTime) { 551 this.computeAvailability(startTime, startTime + durationInfo.duration, params); 552 startTime += AjxDateUtil.MSEC_PER_HALF_HOUR; 553 } 554 555 params.locationInfo = this.computeLocationAvailability(durationInfo, params); 556 557 this._fbStat.sort(ZmScheduleAssistantView._slotComparator); 558 //DBG.dumpObj(this._fbStat); 559 this.renderSuggestions(params); 560 561 //highlight minicalendar to mark suggested days in month 562 this.highlightMiniCal(); 563 }; 564 565 ZmScheduleAssistantView.prototype.isSuggestionsEnabled = 566 function() { 567 if(!this._suggestTime && (!appCtxt.get(ZmSetting.GROUP_CALENDAR_ENABLED) || !appCtxt.get(ZmSetting.GAL_ENABLED))) { 568 //disable suggest locations when GAL is disabled. 569 return false; 570 } 571 // Enabled when visible 572 return this._enabled; 573 }; 574 575 ZmScheduleAssistantView.prototype.overrideManualSuggestion = 576 function(enable) { 577 this._manualOverrideFlag = enable; 578 }; 579 580 ZmScheduleAssistantView.prototype.isSuggestRooms = 581 function() { 582 // Assume desire room checking if it is possible 583 return appCtxt.get(ZmSetting.GAL_ENABLED); 584 }; 585 586 ZmScheduleAssistantView.prototype.getAttendees = 587 function() { 588 return this._attendees; 589 }; 590 591 ZmScheduleAssistantView.prototype.computeAvailability = 592 function(startTime, endTime, params) { 593 594 var dayStartTime = (new Date(startTime)).setHours(0,0,0,0); 595 var dayEndTime = dayStartTime + AjxDateUtil.MSEC_PER_DAY; 596 597 var key = this.getKey(startTime, endTime); 598 var fbInfo; 599 600 if(!params.miniCalSuggestions && this._fbStatMap[key]) { 601 fbInfo = this._fbStatMap[key]; 602 }else { 603 fbInfo = { 604 startTime: startTime, 605 endTime: endTime, 606 availableUsers: 0, 607 availableLocations: 0, 608 attendees: [], 609 locations: [] 610 }; 611 } 612 613 var attendee, sched, isFree; 614 for(var i = this._attendees.length; --i >= 0;) { 615 attendee = this._attendees[i]; 616 617 var excludeTimeSlots = this._apptView.getFreeBusyExcludeInfo(attendee); 618 sched = this._fbCache.getFreeBusySlot(dayStartTime, dayEndTime, attendee, excludeTimeSlots); 619 620 // Last entry will be the organizer, all others are attendees 621 // Organizer and Attendees have separate checkboxes indicating whether to apply non-working hours to them. 622 var isOrganizer = (i == (this._attendees.length-1)); 623 var onlyUseWorkingHours = isOrganizer ? 624 params.onlyIncludeMyWorkingHours : params.onlyIncludeOthersWorkingHours; 625 isFree = onlyUseWorkingHours ? this.isWithinWorkingHour(attendee, startTime, endTime) : true; 626 627 //ignore time slots for non-working hours of this user 628 if(!isFree) continue; 629 630 if(sched.b) isFree = isFree && ZmApptAssistantView.isBooked(sched.b, startTime, endTime); 631 if(sched.t) isFree = isFree && ZmApptAssistantView.isBooked(sched.t, startTime, endTime); 632 if(sched.u) isFree = isFree && ZmApptAssistantView.isBooked(sched.u, startTime, endTime); 633 634 //collect all the item indexes of the attendees available at this slot 635 if(isFree) { 636 if(!params.miniCalSuggestions) fbInfo.attendees.push(params.itemIndex[attendee]); 637 fbInfo.availableUsers++; 638 } 639 } 640 641 if (this.isSuggestRooms()) { 642 643 var list = this._resources, resource; 644 for (var i = list.length; --i >= 0;) { 645 attendee = list[i]; 646 resource = attendee.getEmail(); 647 648 if (resource instanceof Array) { 649 resource = resource[0]; 650 } 651 652 var excludeTimeSlots = this._apptView.getFreeBusyExcludeInfo(resource); 653 sched = this._fbCache.getFreeBusySlot(dayStartTime, dayEndTime, resource, excludeTimeSlots); 654 isFree = true; 655 if(sched.b) isFree = isFree && ZmApptAssistantView.isBooked(sched.b, startTime, endTime); 656 if(sched.t) isFree = isFree && ZmApptAssistantView.isBooked(sched.t, startTime, endTime); 657 if(sched.u) isFree = isFree && ZmApptAssistantView.isBooked(sched.u, startTime, endTime); 658 659 //collect all the item indexes of the locations available at this slot 660 if(isFree) { 661 if(!params.miniCalSuggestions) fbInfo.locations.push(params.itemIndex[resource]); 662 fbInfo.availableLocations++; 663 } 664 } 665 } 666 667 //mini calendar suggestions should avoid collecting all computed information in array for optimiziation 668 if (!params.miniCalSuggestions) { 669 var showOnlyGreenSuggestions = params.showOnlyGreenSuggestions; 670 if(!showOnlyGreenSuggestions || (fbInfo.availableUsers == this._totalUsers)) { 671 this._fbStat.add(fbInfo); 672 this._fbStatMap[key] = fbInfo; 673 } 674 } 675 676 return fbInfo; 677 }; 678 679 //module to sort the computed time slots in order of 1)available users 2)time 680 ZmScheduleAssistantView._slotComparator = 681 function(slot1, slot2) { 682 if(slot1.availableUsers < slot2.availableUsers) { 683 return 1; 684 }else if(slot1.availableUsers > slot2.availableUsers) { 685 return -1; 686 }else { 687 return slot1.startTime < slot2.startTime ? -1 : (slot1.startTime > slot2.startTime ? 1 : 0); 688 } 689 }; 690 691 ZmScheduleAssistantView.prototype.getKey = 692 function(startTime, endTime) { 693 return startTime + "-" + endTime; 694 }; 695 696 //working hours pattern repeats every week - fetch it for just one week 697 ZmScheduleAssistantView.prototype.getWorkingHoursKey = 698 function() { 699 700 if(!this._timeFrame) return; 701 702 var weekStartDate = new Date(this._timeFrame.start.getTime()); 703 var dow = weekStartDate.getDay(); 704 weekStartDate.setDate(weekStartDate.getDate()-((dow+7))%7); 705 return [weekStartDate.getTime(), weekStartDate.getTime() + 7*AjxDateUtil.MSEC_PER_DAY, this._organizerEmail].join("-"); 706 }; 707 708 ZmScheduleAssistantView.prototype.isWithinWorkingHour = 709 function(attendee, startTime, endTime) { 710 711 var dayStartTime = (new Date(startTime)).setHours(0,0,0,0); 712 var dayEndTime = dayStartTime + AjxDateUtil.MSEC_PER_DAY; 713 714 var workingHours = this._fbCache.getWorkingHrsSlot(dayStartTime, dayEndTime, attendee); 715 716 //if working hours could not be retrieved consider all time slots for suggestion 717 if(workingHours && workingHours.n) { 718 workingHours = this._fbCache.getWorkingHrsSlot(dayStartTime, dayEndTime, this._organizerEmail); 719 if(workingHours && workingHours.n) return true; 720 } 721 722 if(!workingHours) return false; 723 724 var slots = workingHours.f; 725 726 //working hours are indicated as free slots 727 if(!slots) return false; 728 729 //convert working hrs relative to the searching time before comparing 730 var slotStartDate, slotEndDate, slotStartTime, slotEndTime; 731 for (var i = 0; i < slots.length; i++) { 732 slotStartDate = new Date(slots[i].s); 733 slotEndDate = new Date(slots[i].e); 734 slotStartTime = (new Date(startTime)).setHours(slotStartDate.getHours(), slotStartDate.getMinutes(), 0, 0); 735 slotEndTime = slotStartTime + (slots[i].e - slots[i].s); 736 if(startTime >= slotStartTime && endTime <= slotEndTime) { 737 return true; 738 } 739 }; 740 return false; 741 }; 742 743 ZmScheduleAssistantView.prototype.renderSuggestions = 744 function(params) { 745 746 if (this._suggestTime) { 747 params.list = this._fbStat; 748 } else { 749 params.list = params.locationInfo.locations; 750 var warning = false; 751 if (params.list.size() >= ZmContactsApp.SEARCHFOR_MAX) { 752 // Problem: the locations search returned the Limit, implying there may 753 // be even more - and the location suggestion pane does not have a 'Next' 754 // button to get the next dollop, since large numbers of suggestions are 755 // not useful. Include a warning that the user should set their location prefs. 756 warning = true; 757 } 758 this._locationSuggestions.setWarning(warning); 759 } 760 params.totalUsers = this._totalUsers; 761 params.totalLocations = this._totalLocations; 762 763 this._currentSuggestions.set(params); 764 if(params.focus) this._currentSuggestions.focus(); 765 this._resetSize(); 766 }; 767 768 //modules for handling mini calendar suggestions 769 770 ZmScheduleAssistantView.prototype.highlightMiniCal = 771 function() { 772 this.getMonthFreeBusyInfo(); 773 }; 774 775 ZmScheduleAssistantView.prototype.clearMiniCal = 776 function() { 777 this._miniCalendar.setColor({}, true, {}); 778 }; 779 780 ZmScheduleAssistantView.prototype.getMonthFreeBusyInfo = 781 function() { 782 var range = this._miniCalendar.getDateRange(); 783 var startDate = range.start; 784 var endDate = range.end; 785 786 var params = { 787 items: [], 788 itemIndex: {}, 789 focus: false, 790 timeFrame: { 791 start: startDate, 792 end: endDate 793 }, 794 miniCalSuggestions: true 795 }; 796 797 //avoid suggestions for past date 798 var currentDayTime = (new Date()).setHours(0,0,0,0); 799 if(currentDayTime >= startDate.getTime() && currentDayTime <= endDate.getTime()) { 800 //reset start date if the current date falls within the month date range - to ignore free busy info from the past 801 startDate = params.timeFrame.start = new Date(currentDayTime); 802 if(endDate.getTime() == currentDayTime) { 803 endDate = params.timeFrame.end = new Date(currentDayTime + AjxDateUtil.MSEC_PER_DAY); 804 } 805 }else if(endDate.getTime() < currentDayTime) { 806 //avoid fetching free busy info for dates in the past 807 return; 808 } 809 810 var list = this._resources; 811 var emails = [], attendeeEmails = []; 812 813 814 for (var i = list.length; --i >= 0;) { 815 var item = list[i]; 816 var email = item.getEmail(); 817 if (email instanceof Array) { 818 email = email[0]; 819 } 820 emails.push(email); 821 822 params.items.push(email); 823 params.itemIndex[email] = params.items.length -1; 824 825 } 826 827 var attendees = this._apptView.getRequiredAttendeeEmails(); 828 829 var attendee; 830 for (var i = attendees.length; --i >= 0;) { 831 attendee = attendees[i]; 832 params.items.push(attendee); 833 params.itemIndex[attendee] = params.items.length-1; 834 emails.push(attendee); 835 attendeeEmails.push(attendee); 836 } 837 838 params._nonOrganizerAttendeeEmails = attendeeEmails.slice(); 839 840 //include organizer in the scheduler suggestions 841 var organizer = this.getOrganizer(); 842 var organizerEmail = organizer.getEmail(); 843 params.items.push(organizerEmail); 844 params.itemIndex[organizerEmail] = params.items.length-1; 845 emails.push(organizerEmail); 846 attendeeEmails.push(organizerEmail); 847 848 params.emails = emails; 849 params.attendeeEmails = attendeeEmails; 850 851 var callback = new AjxCallback(this, this._handleMonthFreeBusyInfo, [params]); 852 var acct = (appCtxt.multiAccounts) 853 ? this._apptView.getCalendarAccount() : null; 854 855 856 var fbParams = { 857 startTime: startDate.getTime(), 858 endTime: endDate.getTime(), 859 emails: emails, 860 callback: callback, 861 errorCallback: callback, 862 noBusyOverlay: true, 863 account: acct 864 }; 865 866 this._monthFreeBusyRequest = this._fbCache.getFreeBusyInfo(fbParams); 867 }; 868 869 ZmScheduleAssistantView.prototype._handleMonthFreeBusyInfo = 870 function(params) { 871 872 //clear fb request info 873 this._monthFreeBusyRequest = null; 874 875 if (this._monthWorkingHrsReq) { 876 appCtxt.getRequestMgr().cancelRequest(this._monthWorkingHrsReq, null, true); 877 } 878 879 var onlyIncludeMyWorkingHours = this.isOnlyMyWorkingHoursIncluded(); 880 var onlyIncludeOthersWorkingHours = this.isOnlyOthersWorkingHoursIncluded(); 881 882 if(!onlyIncludeMyWorkingHours && !onlyIncludeOthersWorkingHours) { 883 this.suggestMonthTimeSlots(params); 884 return; 885 } 886 887 var organizer = this.getOrganizer(); 888 this._organizerEmail = organizer.getEmail(); 889 890 this._workingHoursKey = this.getWorkingHoursKey(); 891 892 var acct = (appCtxt.multiAccounts) ? this._apptView.getCalendarAccount() : null; 893 894 //optimization: fetch working hrs for a week - wrking hrs pattern repeat everyweek 895 var weekStartDate = new Date(params.timeFrame.start.getTime()); 896 var dow = weekStartDate.getDay(); 897 weekStartDate.setDate(weekStartDate.getDate()-((dow+7))%7); 898 899 var emails = onlyIncludeOthersWorkingHours ? params._nonOrganizerAttendeeEmails : null; 900 901 if (onlyIncludeMyWorkingHours) { 902 emails = emails && emails.concat([this._organizerEmail]); 903 } 904 905 var whrsParams = { 906 startTime: weekStartDate.getTime(), 907 endTime: weekStartDate.getTime() + 7*AjxDateUtil.MSEC_PER_DAY, 908 emails: emails, 909 callback: new AjxCallback(this, this._handleMonthWorkingHoursResponse, [params]), 910 errorCallback: new AjxCallback(this, this._handleMonthWorkingHoursError, [params]), 911 noBusyOverlay: true, 912 account: acct 913 }; 914 915 this._monthWorkingHrsReq = this._fbCache.getWorkingHours(whrsParams); 916 }; 917 918 919 ZmScheduleAssistantView.prototype._handleMonthWorkingHoursResponse = 920 function(params, result) { 921 922 this._monthWorkingHrsReq = null; 923 this.suggestMonthTimeSlots(params); 924 }; 925 926 ZmScheduleAssistantView.prototype._handleMonthWorkingHoursError = 927 function(params, result) { 928 929 this._monthWorkingHrsReq = null; 930 this.suggestMonthTimeSlots(params); 931 }; 932 933 934 ZmScheduleAssistantView.prototype.suggestMonthTimeSlots = 935 function(params) { 936 937 var startDate = params.timeFrame.start; 938 startDate.setHours(0, 0, 0, 0); 939 var startTime = startDate.getTime(); 940 var endTime = params.timeFrame.end.getTime(); 941 var duration = this._duration = this._apptView.getDurationInfo().duration; 942 943 params.duration = duration; 944 945 this._fbStat = new AjxVector(); 946 this._fbStatMap = {}; 947 this._totalUsers = this._attendees.length; 948 this._totalLocations = this._resources.length; 949 950 params.dates = {}; 951 params.colors = {}; 952 953 var key, fbStat, freeSlotFound = false, dayStartTime, dayEndTime; 954 955 //suggest for entire minicalendar range 956 while(startTime < endTime) { 957 958 dayStartTime = startTime; 959 dayEndTime = dayStartTime + AjxDateUtil.MSEC_PER_DAY; 960 961 freeSlotFound = false; 962 963 while(dayStartTime < dayEndTime) { 964 fbStat = this.computeAvailability(dayStartTime, dayStartTime + duration, params); 965 dayStartTime += AjxDateUtil.MSEC_PER_HALF_HOUR; 966 967 if(fbStat && fbStat.availableUsers == this._totalUsers) { 968 this._addColorCode(params, startTime, ZmMiniCalendar.COLOR_GREEN); 969 freeSlotFound = true; 970 //found atleast one free slot that can accomodate all attendees and atleast one recources 971 break; 972 } 973 } 974 975 if(!freeSlotFound) { 976 this._addColorCode(params, startTime, ZmMiniCalendar.COLOR_RED); 977 } 978 979 startTime += AjxDateUtil.MSEC_PER_DAY; 980 } 981 982 this._miniCalendar.setColor(params.dates, true, params.colors); 983 }; 984 985 ZmScheduleAssistantView.prototype._addColorCode = 986 function(params, startTime, code) { 987 var sd = new Date(startTime); 988 var str = AjxDateFormat.format("yyyyMMdd", sd); 989 params.dates[str] = sd; 990 params.colors[str] = code; 991 }; 992 993 ZmScheduleAssistantView.prototype._resetSize = function() { 994 ZmApptAssistantView.prototype._resetSize.call(this); 995 996 if (!this._currentSuggestions) { 997 return; 998 } 999 1000 var width = this.boundsForChild(this._currentSuggestions).width; 1001 width -= Dwt.getScrollbarSizes(this._suggestionsView).x; 1002 1003 if (AjxEnv.isIE || AjxEnv.isModernIE) { 1004 var insets = this._currentSuggestions.getInsets(); 1005 width -= insets.left + insets.right; 1006 } 1007 1008 this._currentSuggestions.setSize(width); 1009 }; 1010