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