1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015, 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, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a new tab view for scheduling appointment attendees.
 26  * @constructor
 27  * @class
 28  * This class displays free/busy information for an appointment's attendees. An
 29  * attendee may be a person, a location, or equipment.
 30  *
 31  *  @author Sathishkumar Sugumaran
 32  *
 33  * @param parent			[ZmApptComposeView]			the appt compose view
 34  * @param attendees			[hash]						attendees/locations/equipment
 35  * @param controller		[ZmApptComposeController]	the appt compose controller
 36  * @param dateInfo			[object]					hash of date info
 37  */
 38 ZmFreeBusySchedulerView = function(parent, attendees, controller, dateInfo, appt, fbParentCallback) {
 39 
 40 	DwtComposite.call(this, {
 41 		parent: parent,
 42 		posStyle: DwtControl.RELATIVE_STYLE,
 43 		className: 'ZmFreeBusySchedulerView'
 44 	});
 45 
 46 	this._attendees  = attendees;
 47 	this._controller = controller;
 48 	this._dateInfo   = dateInfo;
 49 	this._appt       = appt;
 50 	this._fbParentCallback = fbParentCallback;
 51 
 52 	this._editView = parent;
 53 
 54 	this._rendered = false;
 55 	this._emailToIdx = {};
 56 	this._schedTable = [];
 57 	this._autoCompleteHandled = {};
 58 	this._allAttendees = [];
 59 	this._allAttendeesStatus = [];
 60 	this._allAttendeesSlot = null;
 61     this._sharedCalIds = {};
 62     
 63 	this._attTypes = [ZmCalBaseItem.PERSON];
 64 	if (appCtxt.get(ZmSetting.GAL_ENABLED)) {
 65 		this._attTypes.push(ZmCalBaseItem.LOCATION);
 66 		this._attTypes.push(ZmCalBaseItem.EQUIPMENT);
 67 	}
 68 
 69 	this._fbCallback = new AjxCallback(this, this._handleResponseFreeBusy);
 70 	this._workCallback = new AjxCallback(this, this._handleResponseWorking);
 71 	this._kbMgr = appCtxt.getKeyboardMgr();
 72     this._emailAliasMap = {};
 73 
 74     this.addListener(DwtEvent.ONMOUSEDOWN, parent._listenerMouseDown);
 75 
 76     this.isComposeMode = true;
 77     this._resultsPaginated = true;
 78     this._isPageless = false;
 79 
 80     this._fbConflict = {};
 81 
 82     //this._fbCache = controller.getApp().getFreeBusyCache();
 83     this._fbCache = parent.getFreeBusyCache();
 84 };
 85 
 86 ZmFreeBusySchedulerView.prototype = new DwtComposite;
 87 ZmFreeBusySchedulerView.prototype.constructor = ZmFreeBusySchedulerView;
 88 
 89 ZmFreeBusySchedulerView.prototype.isZmFreeBusySchedulerView = true;
 90 ZmFreeBusySchedulerView.prototype.toString = function() { return "ZmFreeBusySchedulerView"; };
 91 
 92 
 93 // Consts
 94 
 95 ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS		= 48;
 96 
 97 /**
 98  * Defines the "free" status.
 99  */
100 ZmFreeBusySchedulerView.STATUS_FREE				= 1;
101 /**
102  * Defines the "busy" status.
103  */
104 ZmFreeBusySchedulerView.STATUS_BUSY				= 2;
105 /**
106  * Defines the "tentative" status.
107  */
108 ZmFreeBusySchedulerView.STATUS_TENTATIVE			= 3;
109 /**
110  * Defines the "out" status.
111  */
112 ZmFreeBusySchedulerView.STATUS_OUT				= 4;
113 /**
114  * Defines the "unknown" status.
115  */
116 ZmFreeBusySchedulerView.STATUS_UNKNOWN			= 5;
117 ZmFreeBusySchedulerView.STATUS_WORKING			= 6;
118 // Pre-cache the status css class names
119 ZmFreeBusySchedulerView.STATUS_CLASSES = [];
120 ZmFreeBusySchedulerView.STATUS_CLASSES[ZmFreeBusySchedulerView.STATUS_FREE]		= "ZmScheduler-free";
121 ZmFreeBusySchedulerView.STATUS_CLASSES[ZmFreeBusySchedulerView.STATUS_BUSY]		= "ZmScheduler-busy";
122 ZmFreeBusySchedulerView.STATUS_CLASSES[ZmFreeBusySchedulerView.STATUS_TENTATIVE]	= "ZmScheduler-tentative";
123 ZmFreeBusySchedulerView.STATUS_CLASSES[ZmFreeBusySchedulerView.STATUS_OUT]		= "ZmScheduler-outOfOffice";
124 ZmFreeBusySchedulerView.STATUS_CLASSES[ZmFreeBusySchedulerView.STATUS_UNKNOWN]	= "ZmScheduler-unknown";
125 ZmFreeBusySchedulerView.STATUS_CLASSES[ZmFreeBusySchedulerView.STATUS_WORKING]	= "ZmScheduler-working";
126 
127 ZmFreeBusySchedulerView.PSTATUS_CLASSES = [];
128 ZmFreeBusySchedulerView.PSTATUS_CLASSES[ZmCalBaseItem.PSTATUS_DECLINED]      = "ZmSchedulerPTST-declined";
129 ZmFreeBusySchedulerView.PSTATUS_CLASSES[ZmCalBaseItem.PSTATUS_DEFERRED]      = "ZmSchedulerPTST-deferred";
130 ZmFreeBusySchedulerView.PSTATUS_CLASSES[ZmCalBaseItem.PSTATUS_DELEGATED]     = "ZmSchedulerPTST-delegated";
131 ZmFreeBusySchedulerView.PSTATUS_CLASSES[ZmCalBaseItem.PSTATUS_NEEDS_ACTION]  = "ZmSchedulerPTST-needsaction";
132 ZmFreeBusySchedulerView.PSTATUS_CLASSES[ZmCalBaseItem.PSTATUS_TENTATIVE]     = "ZmSchedulerPTST-tentative";
133 ZmFreeBusySchedulerView.PSTATUS_CLASSES[ZmCalBaseItem.PSTATUS_WAITING]       = "ZmSchedulerPTST-waiting";
134 
135 ZmFreeBusySchedulerView.ROLE_OPTIONS = {};
136 
137 ZmFreeBusySchedulerView.ROLE_OPTIONS[ZmCalBaseItem.PERSON]          = { label: ZmMsg.requiredAttendee, 			value: ZmCalBaseItem.PERSON, 	        image: "AttendeesRequired" };
138 ZmFreeBusySchedulerView.ROLE_OPTIONS[ZmCalItem.ROLE_OPTIONAL]       = { label: ZmMsg.optionalAttendee, 			value: ZmCalItem.ROLE_OPTIONAL, 	image: "AttendeesOptional" };
139 ZmFreeBusySchedulerView.ROLE_OPTIONS[ZmCalBaseItem.LOCATION]        = { label: ZmMsg.location, 			        value: ZmCalBaseItem.LOCATION, 	        image: "Location" };
140 ZmFreeBusySchedulerView.ROLE_OPTIONS[ZmCalBaseItem.EQUIPMENT]       = { label: ZmMsg.equipmentAttendee, 			value: ZmCalBaseItem.EQUIPMENT, 	    image: "Resource" };
141 
142 // Hold on to this one separately because we use it often
143 ZmFreeBusySchedulerView.FREE_CLASS = ZmFreeBusySchedulerView.STATUS_CLASSES[ZmFreeBusySchedulerView.STATUS_FREE];
144 
145 ZmFreeBusySchedulerView.DELAY = 200;
146 ZmFreeBusySchedulerView.BATCH_SIZE = 25;
147 
148 ZmFreeBusySchedulerView._VALUE = "value";
149 
150 // Public methods
151 
152 ZmFreeBusySchedulerView.prototype.setComposeMode =
153 function(isComposeMode) {
154 	this.isComposeMode = isComposeMode;
155 };
156 
157 ZmFreeBusySchedulerView.prototype.showMe =
158 function() {
159 
160     if(this.composeMode) ZmApptViewHelper.getDateInfo(this._editView, this._dateInfo);
161 
162 	this._dateBorder = this._getBordersFromDateInfo();
163 
164 	if (!this._rendered) {
165 		this._initialize();
166 	}
167 
168     var organizer;
169     if(this.isComposeMode) {
170         organizer = this._isProposeTime ? this._editView.getCalItemOrganizer() : this._editView.getOrganizer();
171     }else {
172         organizer = this._editView.getOrganizer();
173     }
174 
175 	this.set(this._dateInfo, organizer, this._attendees);
176     this.enablePartcipantStatusColumn(this.isComposeMode ? this._editView.getRsvp() : true);
177 };
178 
179 ZmFreeBusySchedulerView.prototype.initialize =
180 function(appt, mode, isDirty, apptComposeMode) {
181 	this._appt = appt;
182 	this._mode = mode;
183     this._isForward = (apptComposeMode == ZmApptComposeView.FORWARD);
184     this._isProposeTime = (apptComposeMode == ZmApptComposeView.PROPOSE_TIME);
185 };
186 
187 ZmFreeBusySchedulerView.prototype.set =
188 function(dateInfo, organizer, attendees) {
189 
190     //need to capture initial time set while composing/editing appt
191     if(this.isComposeMode) ZmApptViewHelper.getDateInfo(this._editView, this._dateInfo);
192 
193 	this._setAttendees(organizer, attendees);
194 };
195 
196 ZmFreeBusySchedulerView.prototype.update =
197 function(dateInfo, organizer, attendees) {
198 	this._updateAttendees(organizer, attendees);
199     this.updateFreeBusy();
200 	this._outlineAppt();
201 };
202 
203 ZmFreeBusySchedulerView.prototype.cleanup =
204 function() {
205 	if (!this._rendered) return;
206 
207     if(this._timedActionId)  {
208         AjxTimedAction.cancelAction(this._timedActionId);
209         this._timedActionId = null;
210     }
211 
212 	// remove all but first two rows (header and All Attendees)
213 	while (this._attendeesTable.rows.length > 2) {
214 		this._removeAttendeeRow(2);
215 	}
216 	this._activeInputIdx = null;
217 
218 	// cleanup all attendees row
219 	var allAttCells = this._allAttendeesSlot._coloredCells;
220 	while (allAttCells.length > 0) {
221 		allAttCells[0].className = ZmFreeBusySchedulerView.FREE_CLASS;
222 		allAttCells.shift();
223 	}
224 
225 	for (var i in this._emailToIdx) {
226 		delete this._emailToIdx[i];
227 	}
228 
229 	this._curValStartDate = "";
230 	this._curValEndDate = "";
231 
232 	this._resetAttendeeCount();
233 
234 	// reset autocomplete lists
235 	if (this._acContactsList) {
236 		this._acContactsList.reset();
237 		this._acContactsList.show(false);
238 	}
239 	if (this._acEquipmentList) {
240 		this._acEquipmentList.reset();
241 		this._acEquipmentList.show(false);
242 	}
243 
244     this._emailAliasMap = {};
245     this._emptyRowIndex = null;
246     this._autoCompleteHandled = {}
247 
248     this._fbConflict = {};
249 };
250 
251 // Private / protected methods
252 
253 ZmFreeBusySchedulerView.prototype._initialize =
254 function() {
255 	this._createHTML();
256 	this._initAutocomplete();
257 	this._createDwtObjects();
258 	this._resetAttendeeCount();
259 
260     //intialize a single common event mouseover/out handler for optimization
261     Dwt.setHandler(this.getHtmlElement(), DwtEvent.ONMOUSEOVER, ZmFreeBusySchedulerView._onFreeBusyMouseOver);
262     Dwt.setHandler(this.getHtmlElement(), DwtEvent.ONMOUSEOUT, ZmFreeBusySchedulerView._onFreeBusyMouseOut);
263 
264 
265     Dwt.setHandler(this._showMoreLink, DwtEvent.ONCLICK, ZmFreeBusySchedulerView._onShowMore);
266 
267 
268 	this._rendered = true;
269 };
270 
271 ZmFreeBusySchedulerView.prototype._createHTML =
272 function() {
273 	this._navToolbarId		= this._htmlElId + "_navToolbar";
274 	this._attendeesTableId	= this._htmlElId + "_attendeesTable";
275 	this._showMoreLinkId	= this._htmlElId + "_showMoreLink";
276 
277 	this._schedTable[0] = null;	// header row has no attendee data
278 
279 	var subs = { id:this._htmlElId, isAppt: true, showTZSelector: appCtxt.get(ZmSetting.CAL_SHOW_TIMEZONE) };
280 	this.getHtmlElement().innerHTML = AjxTemplate.expand("calendar.Appointment#InlineScheduleView", subs);
281 };
282 
283 ZmFreeBusySchedulerView.prototype._initAutocomplete =
284 function() {
285 
286 	var acCallback = this._autocompleteCallback.bind(this);
287 	var keyUpCallback = this._autocompleteKeyUpCallback.bind(this);
288 	this._acList = {};
289 
290 	// autocomplete for attendees
291 	if (appCtxt.get(ZmSetting.CONTACTS_ENABLED) || appCtxt.get(ZmSetting.GAL_ENABLED)) {
292 		var params = {
293 			dataClass:		appCtxt.getAutocompleter(),
294 			separator:		"",
295 			options:		{needItem: true},
296 			matchValue:		[ZmAutocomplete.AC_VALUE_NAME, ZmAutocomplete.AC_VALUE_EMAIL],
297 			keyUpCallback:	keyUpCallback,
298 			compCallback:	acCallback
299 		};
300 		params.contextId = [this._controller.getCurrentViewId(), this.toString(), ZmCalBaseItem.PERSON].join("-");
301 		this._acContactsList = new ZmAutocompleteListView(params);
302 		this._acList[ZmCalBaseItem.PERSON] = this._acContactsList;
303 
304 		// autocomplete for locations/equipment
305 		if (appCtxt.get(ZmSetting.GAL_ENABLED)) {
306 			params.options = {type:ZmAutocomplete.AC_TYPE_LOCATION};
307 			params.contextId = [this._controller.getCurrentViewId(), this.toString(), ZmCalBaseItem.LOCATION].join("-");
308 			this._acLocationsList = new ZmAutocompleteListView(params);
309 			this._acList[ZmCalBaseItem.LOCATION] = this._acLocationsList;
310 
311 			params.options = {type:ZmAutocomplete.AC_TYPE_EQUIPMENT};
312 			params.contextId = [this._controller.getCurrentViewId(), this.toString(), ZmCalBaseItem.EQUIPMENT].join("-");
313 			this._acEquipmentList = new ZmAutocompleteListView(params);
314 			this._acList[ZmCalBaseItem.EQUIPMENT] = this._acEquipmentList;
315 		}
316 	}
317 };
318 
319 // Add the attendee, then create a new empty slot since we've now filled one.
320 ZmFreeBusySchedulerView.prototype._autocompleteCallback =
321 function(text, el, match) {
322     if(match && match.fullAddress) {
323         el.value = match.fullAddress;
324     }
325 	if (match && match.item) {
326 		if (match.item.isGroup && match.item.isGroup()) {
327 			var members = match.item.getGroupMembers().good.getArray();
328 			for (var i = 0; i < members.length; i++) {
329 				el.value = members[i].address;
330 
331                 if(el._acHandlerInProgress) { return; }
332                 el._acHandlerInProgress = true;
333 				var index = this._handleAttendeeField(el);
334                 this._editView.showConflicts();
335                 el._acHandlerInProgress = false;
336 
337 				if (index && ((i+1) < members.length)) {
338 					el = this._schedTable[index].inputObj.getInputElement();
339 				}
340 			}
341 		} else {
342             if(el._acHandlerInProgress) { return; }
343             el._acHandlerInProgress = true;
344 			this._handleAttendeeField(el, match.item);
345             this._editView.showConflicts();
346             el._acHandlerInProgress = false;
347 		}
348 	}
349 };
350 
351 // Enter listener. If the user types a return when no autocomplete list is showing,
352 // then go ahead and add a new empty slot.
353 ZmFreeBusySchedulerView.prototype._autocompleteKeyUpCallback =
354 function(ev, aclv, result) {
355 	var key = DwtKeyEvent.getCharCode(ev);
356 	if (DwtKeyEvent.IS_RETURN[key] && !aclv.getVisible()) {
357 		var el = DwtUiEvent.getTargetWithProp(ev, "id");
358         if(el._acHandlerInProgress) { return; }
359         el._acHandlerInProgress = true;
360         this._handleAttendeeField(el);
361         this._editView.showConflicts();
362         el._acHandlerInProgress = false;
363 	}
364 };
365 
366 ZmFreeBusySchedulerView.prototype._addTabGroupMembers =
367 function(tabGroup) {
368 	for (var i = 0; i < this._schedTable.length; i++) {
369 		var sched = this._schedTable[i];
370 		if (sched && sched.inputObj) {
371 			tabGroup.addMember(sched.inputObj);
372 		}
373 	}
374 };
375 
376 ZmFreeBusySchedulerView.prototype._deleteAttendeeEntry =
377 function(email) {
378     var index = this._emailToIdx[email];
379     if(!index) {
380         return;
381     }
382     delete this._emailToIdx[email];
383     Dwt.setDisplay(this._attendeesTable.rows[index], 'none');
384     this._schedTable[index] = null;
385 };
386 
387 ZmFreeBusySchedulerView.prototype._hideRow =
388 function(index) {
389     Dwt.setDisplay(this._attendeesTable.rows[index], 'none');
390 };
391 
392 ZmFreeBusySchedulerView.prototype._deleteAttendeeRow =
393 function(email) {
394     this._deleteAttendeeEntry(email);
395 
396     //remove appt divs created for attendee/calendar
397     this._editView.removeApptByEmail(email);
398 
399     this._updateFreeBusy();
400     this._editView.removeMetadataAttendees(this._schedTable[this._organizerIndex].attendee, email);
401 }
402 
403 /**
404  * Adds a new, empty slot with a select for the attendee type, an input field,
405  * and cells for free/busy info.
406  *
407  * @param isAllAttendees	[boolean]*	if true, this is the "All Attendees" row
408  * @param organizer			[string]*	organizer
409  * @param drawBorder		[boolean]*	if true, draw borders to indicate appt time
410  * @param index				[int]*		index at which to add the row
411  * @param updateTabGroup	[boolean]*	if true, add this row to the tab group
412  * @param setFocus			[boolean]*	if true, set focus to this row's input field
413  */
414 ZmFreeBusySchedulerView.prototype._addAttendeeRow =
415 function(isAllAttendees, organizer, drawBorder, index, updateTabGroup, setFocus) {
416 	index = index || this._attendeesTable.rows.length;
417 
418 	// store some meta data about this table row
419 	var sched = {};
420 	var dwtId = Dwt.getNextId();	// container for input
421 	sched.dwtNameId		= dwtId + "_NAME_";			// TD that contains name
422 	sched.dwtTableId	= dwtId + "_TABLE_";		// TABLE with free/busy cells
423 	sched.dwtSelectId	= dwtId + "_SELECT_";		// TD that contains select menu
424 	sched.dwtInputId	= dwtId + "_INPUT_";		// input field
425 	sched.idx = index;
426 	sched._coloredCells = [];
427 	this._schedTable[index] = sched;
428 
429 	this._dateBorder = this._getBordersFromDateInfo();
430 
431 	var data = {
432 		id: dwtId,
433 		sched: sched,
434 		isAllAttendees: isAllAttendees,
435 		organizer: organizer,
436 		cellCount: ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS,
437         isComposeMode: this.isComposeMode,
438         dateBorder: this._dateBorder
439 	};
440 
441 	var tr = this._attendeesTable.insertRow(index);
442 	var td = tr.insertCell(-1);
443     if(isAllAttendees) {
444         td.className = "ZmSchedulerAllTd";
445     }
446 	td.innerHTML = AjxTemplate.expand("calendar.Appointment#AttendeeName", data);
447 
448 	var td = tr.insertCell(-1);
449 	td.innerHTML = AjxTemplate.expand("calendar.Appointment#AttendeeFreeBusy", data);
450     td.style.padding = "0";
451 
452 	for (var k = 0; k < data.cellCount; k++) {
453 		var id = sched.dwtTableId + "_" + k;
454 		var fbDiv = document.getElementById(id);
455 		if (fbDiv) {
456 			fbDiv._freeBusyCellIndex = k;
457 			fbDiv._schedTableIdx = index;
458 			fbDiv._schedViewPageId = this._svpId;
459 		}
460 	}
461 
462 	// create DwtInputField and DwtSelect for the attendee slots, add handlers
463 	if (!isAllAttendees && !organizer) {
464 		// add DwtSelect
465 		var button;
466 		var btnId = sched.dwtSelectId;
467 		var btnDiv = document.getElementById(btnId);
468 		if (this.isComposeMode && btnDiv) {
469             button  = new DwtButton({parent: this, parentElement: btnId, className: 'ZAttRole'});
470             button.setText("");
471             button.setImage("AttendeesRequired");
472             button.setMenu(new AjxListener(this, this._getAttendeeRoleMenu, [index]));
473             sched.btnObj = button;
474 		}
475 		// add DwtInputField
476 		var nameDiv = document.getElementById(sched.dwtNameId);
477 		if (nameDiv) {
478 			var dwtInputField = new DwtInputField({parent: this, type: DwtInputField.STRING, maxLen: 256});
479 			dwtInputField.setDisplay(Dwt.DISPLAY_INLINE);
480 			var inputEl = dwtInputField.getInputElement();
481             Dwt.setSize(inputEl, Dwt.DEFAULT, "2rem")
482 			inputEl.className = "ZmSchedulerInput";
483 			inputEl.id = sched.dwtInputId;
484             inputEl.style.border = "0px";
485 			sched.attType = inputEl._attType = ZmCalBaseItem.PERSON;
486 			sched.inputObj = dwtInputField;
487 			if (button) {
488 				button.dwtInputField = dwtInputField;
489 			}
490 			dwtInputField.reparentHtmlElement(sched.dwtNameId);
491 		}
492 
493 		sched.ptstObj = document.getElementById(sched.dwtNameId+"_ptst");
494 
495         Dwt.setVisible(sched.ptstObj, this.isComposeMode ? this._editView.getRsvp() : true);
496 
497 		// set handlers
498 		var attendeeInput = document.getElementById(sched.dwtInputId);
499 		if (attendeeInput) {
500 			this._activeInputIdx = index;
501 			// handle focus moving to/from an enabled input
502 			Dwt.setHandler(attendeeInput, DwtEvent.ONFOCUS, ZmFreeBusySchedulerView._onFocus);
503 			Dwt.setHandler(attendeeInput, DwtEvent.ONBLUR, ZmFreeBusySchedulerView._onBlur);
504 			attendeeInput._schedViewPageId = this._svpId;
505 			attendeeInput._schedTableIdx = index;
506 		}
507 	}
508 
509 	if (drawBorder) {
510 		this._updateBorders(sched, isAllAttendees);
511 	}
512     
513 	if (setFocus && sched.inputObj) {
514 		this._kbMgr.grabFocus(sched.inputObj);
515 	}
516 	return index;
517 };
518 
519 ZmFreeBusySchedulerView.prototype._getAttendeeRoleMenu =
520 function(index) {
521     var sched = this._schedTable[index];
522     var listener = new AjxListener(this, this._attendeeRoleListener, [index]);
523     var menu = new DwtMenu({parent:sched.btnObj});
524     for(var i in ZmFreeBusySchedulerView.ROLE_OPTIONS) {
525         var info = ZmFreeBusySchedulerView.ROLE_OPTIONS[i];
526         var menuItem = new DwtMenuItem({parent:menu, style:DwtMenuItem.CASCADE_STYLE});
527         menuItem.setImage(info.image);
528         menuItem.setText(info.label);
529         menuItem.setData(ZmOperation.MENUITEM_ID, i);
530         menuItem.addSelectionListener(listener);
531     }
532     return menu;
533 };
534 
535 ZmFreeBusySchedulerView.prototype._attendeeRoleListener =
536 function(index, ev) {
537     var item = ev.dwtObj;
538     var data = item.getData(ZmOperation.MENUITEM_ID);
539     var sched = this._schedTable[index];
540     sched.btnObj.setImage(ZmFreeBusySchedulerView.ROLE_OPTIONS[data].image);
541     sched.btnObj.getMenu().popdown();
542     this._handleRoleChange(sched, data, this);
543 };
544 
545 ZmFreeBusySchedulerView.prototype._removeAttendeeRow =
546 function(index, updateTabGroup) {
547 	this._attendeesTable.deleteRow(index);
548 	this._schedTable.splice(index, 1);
549 	if (updateTabGroup) {
550 		this._controller._setComposeTabGroup(true);
551 	}
552 };
553 
554 ZmFreeBusySchedulerView.prototype._hideAttendeeRow =
555 function(index, updateTabGroup) {
556     var row = this._attendeesTable.rows[index];
557     if(row){
558         row.style.display="none";
559     }
560     if (updateTabGroup) {
561         this._controller._setComposeTabGroup(true);
562     }
563 
564 };
565 
566 ZmFreeBusySchedulerView.prototype._createDwtObjects =
567 function() {
568 
569     //todo: use time selection listener when appt time is changed
570 	//var timeSelectListener = new AjxListener(this, this._timeChangeListener);
571 
572 	this._curValStartDate = "";
573 	this._curValEndDate = "";
574 
575 	// add All Attendees row
576 	this._svpId = AjxCore.assignId(this);
577 	this._attendeesTable = document.getElementById(this._attendeesTableId);
578 	this._allAttendeesIndex = this._addAttendeeRow(true, null, false);
579 	this._allAttendeesSlot = this._schedTable[this._allAttendeesIndex];
580 	this._allAttendeesTable = document.getElementById(this._allAttendeesSlot.dwtTableId);
581 	this._showMoreLink = document.getElementById(this._showMoreLinkId);
582     this._showMoreLink._schedViewPageId = this._svpId;
583 };
584 
585 ZmFreeBusySchedulerView.prototype._showTimeFields =
586 function(show) {
587 	Dwt.setVisibility(this._startTimeSelect.getHtmlElement(), show);
588 	Dwt.setVisibility(this._endTimeSelect.getHtmlElement(), show);
589 	this._setTimezoneVisible(this._dateInfo);
590 
591 	// also show/hide the "@" text
592 	Dwt.setVisibility(document.getElementById(this._startTimeAtLblId), show);
593 	Dwt.setVisibility(document.getElementById(this._endTimeAtLblId), show);
594 };
595 
596 ZmFreeBusySchedulerView.prototype._isDuplicate =
597 function(email) {
598     return this._emailToIdx[email] ? true : false;
599 }
600 
601 /**
602  * Called by ONBLUR handler for attendee input field.
603  *
604  * @param inputEl
605  * @param attendee
606  * @param useException
607  */
608 ZmFreeBusySchedulerView.prototype._handleAttendeeField =
609 function(inputEl, attendee, useException) {
610 
611 	var idx = inputEl._schedTableIdx;
612 	if (idx != this._activeInputIdx) return;
613 
614 	var sched = this._schedTable[idx];
615 	if (!sched) return;
616 	var input = sched.inputObj;
617 	if (!input) return;
618 
619 	var value = input.getValue();
620 	if (value) {
621 		value = AjxStringUtil.trim(value.replace(/[;,]$/, ""));	// trim separator, white space
622 	}
623 	var curAttendee = sched.attendee;
624 	var type = sched.attType;
625 
626 	if (value) {
627 		if (curAttendee) {
628 			// user edited slot with an attendee in it
629             var lookupEmail = this.getEmail(curAttendee);
630             var emailTextShortForm = ZmApptViewHelper.getAttendeesText(curAttendee, type, true);
631             //parse the email id to separate the name and email address
632             var emailAddrObj = AjxEmailAddress.parse(value);
633             var emailAddr = emailAddrObj ? emailAddrObj.getAddress() : "";
634 			if (emailAddr == lookupEmail || emailAddr == emailTextShortForm) {
635 				return;
636 			} else {
637 				this._resetRow(sched, false, type, true);
638 			}
639 		}
640 		attendee = attendee ? attendee : ZmApptViewHelper.getAttendeeFromItem(value, type, true);
641 		if (attendee) {
642 			var email = this.getEmail(attendee);
643 
644 
645 			if (email instanceof Array) {
646 				for (var i in email) {
647                     if(this._isDuplicate(email[i])) {
648                         //if duplicate - do nothing
649                         return;
650                     }
651 					this._emailToIdx[email[i]] = idx;
652 				}
653 			} else {
654                 if(this._isDuplicate(email)) {
655                     //if duplicate - do nothing
656                     return;
657                 }
658 				this._emailToIdx[email] = idx;
659 			}
660 
661 			// go get this attendee's free/busy info if we haven't already
662 			if (sched.uid != email) {
663 				this._getFreeBusyInfo(this._getStartTime(), email);
664 			}
665             var attendeeType = sched.btnObj ? sched.btnObj.getData(ZmFreeBusySchedulerView._VALUE) : null;
666             var isOptionalAttendee = (attendeeType == ZmCalItem.ROLE_OPTIONAL);
667             if(type != ZmCalBaseItem.LOCATION && type != ZmCalBaseItem.EQUIPMENT) {
668                 attendee.setParticipantRole( isOptionalAttendee ? ZmCalItem.ROLE_OPTIONAL : ZmCalItem.ROLE_REQUIRED);
669             }
670 			sched.attendee = attendee;
671             this._setParticipantStatus(sched, attendee, idx);
672 			this._setAttendeeToolTip(sched, attendee);
673             //directly update attendees
674 			if(this.isComposeMode) {
675                 this._editView.parent.updateAttendees(attendee, type, ZmApptComposeView.MODE_ADD);
676                 if(isOptionalAttendee) this._editView.showOptional();
677                 this._editView._setAttendees();
678             }
679             else {
680                 this._editView.setMetadataAttendees(this._schedTable[this._organizerIndex].attendee, email);
681                 this._editView.refreshAppts();
682             }
683             if (!curAttendee) {
684 				// user added attendee in empty slot
685 				var value = this._emptyRowIndex = this._addAttendeeRow(false, null, true, null, true, true); // add new empty slot
686                 if (this.isComposeMode) {
687                     this._editView.resize();
688                 }
689                 return value;
690 			}
691 		} else {
692 			this._activeInputIdx = null;
693 		}
694 	} else if (curAttendee) {
695 
696         if(this.isComposeMode) {
697             this._editView.parent.updateAttendees(curAttendee, type, ZmApptComposeView.MODE_REMOVE);
698             this._editView.removeAttendees(curAttendee, type);
699             this._editView._setAttendees();
700         }
701 		// user erased an attendee
702 		this._resetRow(sched, false, type);
703         // bug:43660 removing row (splicing array) causes index mismatch.
704         //this._removeAttendeeRow(idx, true);
705 		this._hideAttendeeRow(idx, true);
706 	}
707 };
708 
709 ZmFreeBusySchedulerView.prototype._setAttendeeToolTip =
710 function(sched, attendee, type) {
711 	if (type != ZmCalBaseItem.PERSON) { return; }
712 
713 	var name = attendee.getFullName();
714 	var email = this.getEmail(attendee);
715 	if (name && email) {
716 		var ptst = ZmMsg.attendeeStatusLabel + ZmCalItem.getLabelForParticipationStatus(attendee.getParticipantStatus() || "NE");
717 		sched.inputObj.setToolTipContent(email + (this.isComposeMode && this._editView.getRsvp()) ? ("<br>"+ ptst) : "");
718 	}
719 };
720 
721 ZmFreeBusySchedulerView.prototype._getStartTime =
722 function() {
723 	return this._getStartDate().getTime();
724 };
725 
726 ZmFreeBusySchedulerView.prototype._getEndTime =
727 function() {
728 	return this._getEndDate().getTime();
729 };
730 
731 ZmFreeBusySchedulerView.prototype._getStartDate =
732 function() {
733     var startDate = AjxDateUtil.simpleParseDateStr(this._dateInfo.startDate);
734     return AjxTimezone.convertTimezone(startDate, this._dateInfo.timezone, AjxTimezone.DEFAULT);
735 };
736 
737 ZmFreeBusySchedulerView.prototype._getEndDate =
738 function() {
739     var endDate = AjxDateUtil.simpleParseDateStr(this._dateInfo.endDate);
740     return AjxTimezone.convertTimezone(endDate, this._dateInfo.timezone, AjxTimezone.DEFAULT);
741 };
742 
743 ZmFreeBusySchedulerView.prototype._setDateInfo =
744 function(dateInfo) {
745 	this._dateInfo = dateInfo;
746 };
747 
748 ZmFreeBusySchedulerView.prototype._colorAllAttendees =
749 function() {
750 	var row = this._allAttendeesTable.rows[0];
751 
752 	for (var i = 0; i < this._allAttendees.length; i++) {
753 		//if (this._allAttendees[i] > 0) {
754 			// TODO: opacity...
755 			var status = this.getAllAttendeeStatus(i);
756 			row.cells[i].className = this._getClassForStatus(status);
757 			this._allAttendeesSlot._coloredCells.push(row.cells[i]);
758 		//}
759 	}
760 };
761 
762 ZmFreeBusySchedulerView.prototype.updateFreeBusy =
763 function(onlyUpdateTable) {
764     this._updateFreeBusy();
765 };
766 
767 ZmFreeBusySchedulerView.prototype._updateFreeBusy =
768 function() {
769 	// update the full date field
770 	this._resetFullDateField();
771 
772 	// clear the schedules for existing attendees
773 	for (var i = 0; i < this._schedTable.length; i++) {
774 		var sched = this._schedTable[i];
775 		if (!sched) continue;
776 		while (sched._coloredCells && sched._coloredCells.length > 0) {
777 			sched._coloredCells[0].className = ZmFreeBusySchedulerView.FREE_CLASS;
778 			sched._coloredCells.shift();
779 		}
780 
781 	}
782 
783 	this._resetAttendeeCount();
784 
785     // Set in updateAttendees
786 	if (this._allAttendeeEmails && this._allAttendeeEmails.length) {
787         //all attendees status need to be update even for unshown attendees
788 		var emails = this._allAttendeeEmails.join(",");
789 		this._getFreeBusyInfo(this._getStartTime(), emails);
790 	}
791 };
792 
793 // XXX: optimize later - currently we always update the f/b view :(
794 ZmFreeBusySchedulerView.prototype._setAttendees =
795 function(organizer, attendees) {
796 	this.cleanup();
797 
798     //sync with date info from schedule view
799     if(this.isComposeMode) ZmApptViewHelper.getDateInfo(this._editView, this._dateInfo);
800 
801     var emails = [], email, showMoreLink = false;
802 
803 	// create a slot for the organizer
804 	this._organizerIndex = this._addAttendeeRow(false, ZmApptViewHelper.getAttendeesText(organizer, ZmCalBaseItem.PERSON, true), false);
805 	emails.push(this._setAttendee(this._organizerIndex, organizer, ZmCalBaseItem.PERSON, true));
806 
807     var list = [], totalAttendeesCount = 0;
808     for (var t = 0; t < this._attTypes.length; t++) {
809         var type = this._attTypes[t];
810         if(attendees[type]) {
811             var att = attendees[type].getArray ? attendees[type].getArray() : attendees[type];
812             var attLength = att.length;
813             totalAttendeesCount += att.length;
814             if(this.isComposeMode && !this._isPageless && att.length > 10) {
815                 attLength = 10;
816                 showMoreLink = true;
817             }
818 
819             for (var i = 0; i < attLength; i++) {
820                 list.push(att[i]);
821                 email = att[i] ? this.getEmail(att[i]) : null;
822                 emails.push(email);
823             }
824         }
825     }
826 
827     Dwt.setDisplay(this._showMoreLink, showMoreLink ? Dwt.DISPLAY_INLINE : Dwt.DISPLAY_NONE);
828     //exclude organizer while reporting no of attendees remaining
829     this.updateNMoreAttendeesLabel(totalAttendeesCount - (emails.length - 1));
830 
831     this._updateBorders(this._allAttendeesSlot, true);
832     
833     //chunk processing of UI rendering
834     this.batchUpdate(list);
835 
836     if (emails.length) {
837         //all attendees status need to be update even for unshown attendees
838         var allAttendeeEmails = this._allAttendeeEmails = this.getAllAttendeeEmails(attendees, organizer);
839         this._getFreeBusyInfo(this._getStartTime(), allAttendeeEmails.join(","));
840 	}
841 };
842 
843 ZmFreeBusySchedulerView.prototype.batchUpdate =
844 function(list, updateCycle) {
845 
846     if(list.length == 0) {
847         // make sure there's always an empty slot
848         this._emptyRowIndex = this._addAttendeeRow(false, null, false, null, true, false);
849         this._colorAllAttendees();
850         this.resizeKeySpacer();
851         return;
852     }
853 
854     if(!updateCycle) updateCycle = 0;
855 
856     var isOrganizer = this.isComposeMode ? this._appt.isOrganizer() : null;
857     var emails = [], type;
858 
859     for(var i=0; i < ZmFreeBusySchedulerView.BATCH_SIZE; i++) {
860         if(list.length == 0) break;
861         var att = list.shift();
862         type = (att instanceof ZmResource) ? att.resType : ZmCalBaseItem.PERSON;
863         this.addAttendee(att, type, isOrganizer, emails);
864     }
865     
866     if (this.isComposeMode) {
867         this._editView.resize();
868     }
869     this.batchUpdateSequence(list, updateCycle+1);
870 };
871 
872 ZmFreeBusySchedulerView.prototype.batchUpdateSequence =
873 function(list,updateCycle) {
874     this._timedAction = new AjxTimedAction(this, this.batchUpdate, [list, updateCycle]);
875     this._timedActionId = AjxTimedAction.scheduleAction(this._timedAction, ZmFreeBusySchedulerView.DELAY);
876 };
877 
878 ZmFreeBusySchedulerView.prototype.addAttendee =
879 function(att, type, isOrganizer, emails) {
880     var email = att ? this.getEmail(att) : null;
881     if (email && !this._emailToIdx[email]) {
882         var index = this._addAttendeeRow(false, null, false); // create a slot for this attendee
883         emails.push(this._setAttendee(index, att, type, false));
884 
885         var sched = this._schedTable[index];
886         if(this._appt && sched) {
887             if(sched.inputObj) sched.inputObj.setEnabled(isOrganizer);
888             if(sched.btnObj) sched.btnObj.setEnabled(isOrganizer);
889         }
890     }
891 };
892 
893 ZmFreeBusySchedulerView.prototype.setUpdateCallback =
894 function(callback) {
895     this._updateCallback = callback;
896 };
897 
898 ZmFreeBusySchedulerView.prototype.postUpdateHandler =
899 function() {
900     this._colorAllAttendees();
901     if(this._updateCallback) {
902         this._updateCallback.run();
903         this._updateCallback = null;
904     }
905 };
906 
907 
908 ZmFreeBusySchedulerView.prototype.getAllAttendeeEmails =
909 function(attendees, organizer) {
910     var emails = [];
911     for (var t = 0; t < this._attTypes.length; t++) {
912         var type = this._attTypes[t];
913         var att = attendees[type].getArray ? attendees[type].getArray() : attendees[type];
914         var attLength = att.length;
915         for (var i = 0; i < attLength; i++) {
916             var email = att[i] ? this.getEmail(att[i]) : null;
917             if (email) emails.push(email);
918         }
919     }
920     if(organizer) {
921         var organizerEmail =  this.getEmail(organizer);
922         emails.push(organizerEmail);
923     }
924     return emails;
925 };
926 
927 ZmFreeBusySchedulerView.prototype._updateAttendees =
928 function(organizer, attendees) {
929 
930     var emails = [], newEmails = {}, showMoreLink = false, totalAttendeesCount = 0, attendeesRendered = 0;
931 
932     //update newly added attendee
933 	for (var t = 0; t < this._attTypes.length; t++) {
934 		var type = this._attTypes[t];
935         if(attendees[type]) {
936             var att = attendees[type].getArray ? attendees[type].getArray() : attendees[type];
937 
938             //debug: remove this limitation
939             var attLengthLimit = att.length;
940             totalAttendeesCount += att.length;
941             if(this.isComposeMode && !this._isPageless && att.length > 10) {
942                 attLengthLimit = 10;
943                 showMoreLink = true;
944             }
945 
946             for (var i = 0; i < att.length; i++) {
947                 var email = att[i] ? this.getEmail(att[i]) : null;
948                 if(email) newEmails[email] = true;
949                 if (i < attLengthLimit && email && !this._emailToIdx[email]) {
950                     var index;
951                     if(this._emptyRowIndex != null) {
952                         emails.push(this._setAttendee(this._emptyRowIndex, att[i], type, false));
953                         this._emptyRowIndex = null;
954                     }else {
955                         index = this._addAttendeeRow(false, null, false); // create a slot for this attendee
956                         emails.push(this._setAttendee(index, att[i], type, false));
957                     }
958                 }
959 
960                 //keep track of total attendees rendered
961                 if (this._emailToIdx[email]) attendeesRendered++;
962             }
963         }
964 	}
965 
966     Dwt.setDisplay(this._showMoreLink, showMoreLink ? Dwt.DISPLAY_INLINE : Dwt.DISPLAY_NONE);
967     this.updateNMoreAttendeesLabel(totalAttendeesCount - attendeesRendered);
968 
969     //update deleted attendee
970     for(var id in this._emailToIdx) {
971         if(!newEmails[id]) {
972             var idx = this._emailToIdx[id];
973             if(this._organizerIndex == idx) continue;
974             var sched = this._schedTable[idx];
975             if(!sched) continue;
976             this._resetRow(sched, false, sched.attType, false, true);
977             this._hideRow(idx);
978             this._schedTable[idx] = null;
979         }
980     }
981 
982     this._setAttendee(this._organizerIndex, organizer, ZmCalBaseItem.PERSON, true);
983 
984     if(emails.length > 0) {
985 	    // make sure there's always an empty slot
986 	    this._emptyRowIndex = this._addAttendeeRow(false, null, false, null, true, false);
987     }
988 
989     // Update the attendee list
990     this._allAttendeeEmails = this.getAllAttendeeEmails(attendees, organizer);
991 	if (emails.length) {
992         //all attendees status need to be update even for unshown attendees
993         var allAttendeeEmails =  this._allAttendeeEmails;
994 		this._getFreeBusyInfo(this._getStartTime(), allAttendeeEmails.join(","));
995 	}else {
996         this.postUpdateHandler();
997     }
998 };
999 
1000 ZmFreeBusySchedulerView.prototype.updateNMoreAttendeesLabel =
1001 function(count) {
1002     this._showMoreLink.innerHTML = AjxMessageFormat.format(ZmMsg.moreAttendees, count);
1003 };
1004 
1005 ZmFreeBusySchedulerView.prototype._setAttendee =
1006 function(index, attendee, type, isOrganizer) {
1007 	var sched = this._schedTable[index];
1008 	if (!sched) { return; }
1009 
1010 	sched.attendee = attendee;
1011 	sched.attType = type;
1012 	var input = sched.inputObj;
1013 	if (input) {
1014 		input.setValue(ZmApptViewHelper.getAttendeesText(attendee, type, false), true);
1015 		this._setAttendeeToolTip(sched, attendee, type);
1016 	}
1017 
1018     var nameDiv = document.getElementById(sched.dwtNameId);
1019     if(isOrganizer && nameDiv) {
1020         nameDiv.innerHTML = '<div class="ZmSchedulerInputDisabled">' + ZmApptViewHelper.getAttendeesText(attendee, type, true) + '</div>';
1021     }
1022 
1023     var button = sched.btnObj;
1024     var role = attendee.getParticipantRole() || ZmCalItem.ROLE_REQUIRED;
1025 
1026     if(type == ZmCalBaseItem.PERSON && role == ZmCalItem.ROLE_OPTIONAL) {
1027         type = ZmCalItem.ROLE_OPTIONAL;
1028     }
1029 
1030 	if (button) {
1031         var info = ZmFreeBusySchedulerView.ROLE_OPTIONS[type];
1032         button.setImage(info.image);
1033         button.setData(ZmFreeBusySchedulerView._VALUE, type);
1034 	}
1035 
1036     this._setParticipantStatus(sched, attendee, index);
1037     
1038 	var email = this.getEmail(attendee);
1039 	if (email instanceof Array) {
1040         sched.uid = email[0];
1041 		for (var i in email) {
1042 			this._emailToIdx[email[i]] = index;
1043 		}
1044 	} else {
1045         sched.uid = email;
1046 		this._emailToIdx[email] = index;
1047 	}
1048 
1049 	return email;
1050 };
1051 
1052 ZmFreeBusySchedulerView.prototype.getAttendees =
1053 function() {
1054     var attendees = [];
1055     for (var i=0; i < this._schedTable.length; i++) {
1056         var sched = this._schedTable[i];
1057         if(!sched) {
1058             continue;
1059         }
1060         if(sched.attendee) {
1061             attendees.push(sched.attendee);
1062         }
1063     }
1064     return AjxVector.fromArray(attendees);
1065 };
1066 
1067 /**
1068  * sets participant status for an attendee
1069  *
1070  * @param sched 		[object]		scedule object which contains info related to this attendee row
1071  * @param attendee		[object]		attendee object ZmContact/ZmResource
1072  * @param index 		[Integer]		index of the schedule
1073  */
1074 ZmFreeBusySchedulerView.prototype._setParticipantStatus =
1075 function(sched, attendee, index) {
1076     var ptst = attendee.getParticipantStatus() || "NE";
1077     var ptstCont = sched.ptstObj;
1078     if (ptstCont) {
1079         if(this.isComposeMode) {
1080             var ptstIcon = ZmCalItem.getParticipationStatusIcon(ptst);
1081             if (ptstIcon != "") {
1082                 var ptstLabel = ZmMsg.attendeeStatusLabel + " " + ZmCalItem.getLabelForParticipationStatus(ptst);
1083                 ptstCont.innerHTML = AjxImg.getImageHtml(ptstIcon);
1084                 var imgDiv = ptstCont.firstChild;
1085                 if(imgDiv && !imgDiv._schedViewPageId ){
1086                     Dwt.setHandler(imgDiv, DwtEvent.ONMOUSEOVER, ZmFreeBusySchedulerView._onPTSTMouseOver);
1087                     Dwt.setHandler(imgDiv, DwtEvent.ONMOUSEOUT, ZmFreeBusySchedulerView._onPTSTMouseOut);
1088                     imgDiv._ptstLabel = ptstLabel;
1089                     imgDiv._schedViewPageId = this._svpId;
1090                     imgDiv._schedTableIdx = index;
1091                 }
1092             }
1093         }
1094         else {
1095             var deleteButton = new DwtBorderlessButton({parent:this, className:"Label"});
1096             deleteButton.setImage("Disable");
1097             deleteButton.setText("");
1098             deleteButton.addSelectionListener(new AjxListener(this, this._deleteAttendeeRow, [attendee.getEmail()]));
1099             deleteButton.getHtmlElement().style.cursor = 'pointer';
1100             deleteButton.replaceElement(ptstCont.firstChild, false, false);
1101         }
1102     }
1103 };
1104 
1105 /**
1106  * Resets a row to its starting state. The input is cleared and removed, and
1107  * the free/busy blocks are set back to their default color. Optionally, the
1108  * select is set back to person.
1109  *
1110  * @param sched			[object]		info for this row
1111  * @param resetSelect	[boolean]*		if true, set select to PERSON
1112  * @param type			[constant]*		attendee type
1113  * @param noClear		[boolean]*		if true, don't clear input field
1114  * @param noUpdate		[boolean]*		if true, don't update parent view
1115  */
1116 ZmFreeBusySchedulerView.prototype._resetRow =
1117 function(sched, resetRole, type, noClear, noUpdate) {
1118 
1119 	var input = sched.inputObj;
1120 	if (sched.attendee && type) {
1121 
1122         if(this.isComposeMode && !noUpdate) {
1123             this._editView.parent.updateAttendees(sched.attendee, type, ZmApptComposeView.MODE_REMOVE);
1124             this._editView._setAttendees();
1125         }
1126 
1127         if (input) {
1128 			input.setToolTipContent(null);
1129 		}
1130 
1131         var email = this.getEmail(sched.attendee);
1132         delete this._fbConflict[email];
1133 
1134         if (email instanceof Array) {
1135             for (var i in email) {
1136                 var m = email[i];
1137                 this._emailToIdx[m] = null;
1138                 delete this._emailToIdx[m];
1139             }
1140         } else {
1141             this._emailToIdx[email] = null;
1142             delete this._emailToIdx[email];
1143         }
1144 
1145 		sched.attendee = null;
1146 	}
1147 
1148 	// clear input field
1149 	if (input && !noClear) {
1150 		input.setValue("", true);
1151 	}
1152 
1153 	// reset the row color to non-white
1154 	var table = document.getElementById(sched.dwtTableId);
1155 	if (table) {
1156 		table.rows[0].className = "ZmSchedulerDisabledRow";
1157 	}
1158 
1159 	// remove the bgcolor from the cells that were colored
1160 	this._clearColoredCells(sched);
1161 
1162 	// reset the select to person
1163 	if (resetRole) {
1164 		var button = sched.btnObj;
1165 		if (button) {
1166             var info = ZmFreeBusySchedulerView.ROLE_OPTIONS[ZmCalBaseItem.PERSON];
1167 			button.setImage(info.image);
1168 		}
1169 	}
1170 
1171 	sched.uid = null;
1172 	this._activeInputIdx = null;
1173 
1174 };
1175 
1176 ZmFreeBusySchedulerView.prototype._resetTimezoneSelect =
1177 function(dateInfo) {
1178 	this._tzoneSelect.setSelectedValue(dateInfo.timezone);
1179 };
1180 
1181 ZmFreeBusySchedulerView.prototype._setTimezoneVisible =
1182 function(dateInfo) {
1183 	var showTimezone = !dateInfo.isAllDay;
1184 	if (showTimezone) {
1185 		showTimezone = appCtxt.get(ZmSetting.CAL_SHOW_TIMEZONE) ||
1186 					   dateInfo.timezone != AjxTimezone.getServerId(AjxTimezone.DEFAULT);
1187 	}
1188 	Dwt.setVisibility(this._tzoneSelect.getHtmlElement(), showTimezone);
1189 };
1190 
1191 ZmFreeBusySchedulerView.prototype._clearColoredCells =
1192 function(sched) {
1193 	while (sched._coloredCells.length > 0) {
1194 		// decrement cell count in all attendees row
1195 		var idx = sched._coloredCells[0].cellIndex;
1196 		if (this._allAttendees[idx] > 0) {
1197 			this._allAttendees[idx] = this._allAttendees[idx] - 1;
1198 		}
1199 
1200 		sched._coloredCells[0].className = ZmFreeBusySchedulerView.FREE_CLASS;
1201 		sched._coloredCells.shift();
1202 	}
1203 	var allAttColors = this._allAttendeesSlot._coloredCells;
1204 	while (allAttColors.length > 0) {
1205 		var idx = allAttColors[0].cellIndex;
1206 		// clear all attendees cell if it's now free
1207 		if (this._allAttendees[idx] == 0) {
1208 			allAttColors[0].className = ZmFreeBusySchedulerView.FREE_CLASS;
1209 		}
1210 		allAttColors.shift();
1211 	}
1212 };
1213 
1214 ZmFreeBusySchedulerView.prototype._resetAttendeeCount =
1215 function() {
1216 	for (var i = 0; i < ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS; i++) {
1217 		this._allAttendees[i] = 0;
1218 		delete this._allAttendeesStatus[i];
1219 	}
1220 };
1221 
1222 ZmFreeBusySchedulerView.prototype._resetFullDateField =
1223 function() {
1224 };
1225 
1226 // Listeners
1227 
1228 ZmFreeBusySchedulerView.prototype._navBarListener =
1229 function(ev) {
1230 	var op = ev.item.getData(ZmOperation.KEY_ID);
1231 
1232 	var sd = AjxDateUtil.simpleParseDateStr(this._dateInfo.startDate);
1233 	var ed = AjxDateUtil.simpleParseDateStr(this._dateInfo.endDate);
1234 
1235 	var newSd = op == ZmOperation.PAGE_BACK ? sd.getDate()-1 : sd.getDate()+1;
1236 	var newEd = op == ZmOperation.PAGE_BACK ? ed.getDate()-1 : ed.getDate()+1;
1237 
1238 	sd.setDate(newSd);
1239 	ed.setDate(newEd);
1240 
1241 	this._updateFreeBusy();
1242 
1243 	// finally, update the appt tab view page w/ new date(s)
1244 	if(this.isComposeMode) this._editView.updateDateField(AjxDateUtil.simpleComputeDateStr(sd), AjxDateUtil.simpleComputeDateStr(ed));
1245 };
1246 
1247 
1248 
1249 ZmFreeBusySchedulerView.prototype.changeDate =
1250 function(dateInfo) {
1251 
1252     this._setDateInfo(dateInfo);
1253 	this._updateFreeBusy();
1254 
1255 	// finally, update the appt tab view page w/ new date(s)
1256 	if(this.isComposeMode) this._editView.updateDateField(AjxDateUtil.simpleComputeDateStr(sd), AjxDateUtil.simpleComputeDateStr(ed));
1257 };
1258 
1259 ZmFreeBusySchedulerView.prototype.setDateBorder =
1260 function(dateBorder) {
1261     this._dateBorder = dateBorder;
1262 };
1263 ZmFreeBusySchedulerView.prototype._timeChangeListener =
1264 function(ev, id) {
1265     this.handleTimeChange();
1266 };
1267 
1268 ZmFreeBusySchedulerView.prototype.handleTimeChange =
1269 function() {
1270     if(this.isComposeMode) ZmApptViewHelper.getDateInfo(this._editView, this._dateInfo);
1271 	this._dateBorder = this._getBordersFromDateInfo();
1272 	this._outlineAppt();
1273     this._updateFreeBusy();
1274 };
1275 
1276 ZmFreeBusySchedulerView.prototype._handleRoleChange =
1277 function(sched, type, svp) {
1278 
1279     if(type == ZmCalBaseItem.PERSON || type == ZmCalItem.ROLE_REQUIRED || type == ZmCalItem.ROLE_OPTIONAL) {
1280         if(sched.attendee) {
1281             sched.attendee.setParticipantRole((type == ZmCalItem.ROLE_OPTIONAL) ? ZmCalItem.ROLE_OPTIONAL : ZmCalItem.ROLE_REQUIRED);
1282             if(this.isComposeMode) {
1283                 this._editView._setAttendees();
1284                 this._editView.updateScheduleAssistant(this._attendees[ZmCalBaseItem.PERSON], ZmCalBaseItem.PERSON);
1285                 if(type == ZmCalItem.ROLE_OPTIONAL) this._editView.showOptional();  
1286             }
1287         }
1288         type = ZmCalBaseItem.PERSON;
1289     }
1290 
1291 	if (sched.attType == type) return;
1292 
1293     var attendee = sched.attendee;
1294 
1295 	// if we wiped out an attendee, make sure it's reflected in master list
1296 	if (attendee) {
1297 
1298         var email = this.getEmail(attendee);
1299         delete this._emailToIdx[email];
1300         delete this._fbConflict[email];
1301         this._editView.showConflicts();
1302 
1303 		if(this.isComposeMode) {
1304             this._editView.parent.updateAttendees(attendee, sched.attType, ZmApptComposeView.MODE_REMOVE);
1305             this._editView._setAttendees();
1306             if(type == ZmCalBaseItem.PERSON) this._editView.updateScheduleAssistant(this._attendees[ZmCalBaseItem.PERSON], ZmCalBaseItem.PERSON);
1307         }
1308 		sched.attendee = null;
1309 	}
1310 	sched.attType = type;
1311 
1312 	// reset row
1313 	var input = sched.inputObj;
1314 	input.setValue("", true);
1315     input.focus();
1316 	svp._clearColoredCells(sched);
1317 
1318 	// reset autocomplete handler
1319 	var inputEl = input.getInputElement();
1320 	if (type == ZmCalBaseItem.PERSON && svp._acContactsList) {
1321 		svp._acContactsList.handle(inputEl);
1322 	} else if (type == ZmCalBaseItem.LOCATION && svp._acLocationsList) {
1323 		svp._acLocationsList.handle(inputEl);
1324 	} else if (type == ZmCalBaseItem.EQUIPMENT && svp._acEquipmentList) {
1325 		svp._acEquipmentList.handle(inputEl);
1326 	}
1327 };
1328 
1329 ZmFreeBusySchedulerView.prototype.getEmail =
1330 function(attendee) {
1331     return attendee.getLookupEmail() || attendee.getEmail();
1332 };
1333 
1334 ZmFreeBusySchedulerView.prototype._colorSchedule =
1335 function(status, slots, table, sched) {
1336 	var row = table.rows[0];
1337 	var className = this._getClassForStatus(status);
1338 
1339     var currentDate = this._getStartDate();
1340 
1341 	if (row && className) {
1342 		// figure out the table cell that needs to be colored
1343 		for (var i = 0; i < slots.length; i++) {
1344             if(status == ZmFreeBusySchedulerView.STATUS_WORKING) {
1345                 this._fbCache.convertWorkingHours(slots[i], currentDate);
1346             }
1347 			var startIdx = this._getIndexFromTime(slots[i].s);
1348 			var endIdx = this._getIndexFromTime(slots[i].e, true);
1349 
1350             if(slots[i].s <= currentDate.getTime()) {
1351                 startIdx = 0;
1352             }
1353 
1354             if(slots[i].e >= currentDate.getTime() + AjxDateUtil.MSEC_PER_DAY) {
1355                 endIdx = ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS - 1;
1356             }
1357 
1358             //bug:45623 assume start index is zero if its negative
1359             if(startIdx < 0) {startIdx = 0;}
1360             //bug:45623 skip the slot that has negative end index.
1361             if(endIdx < 0) { continue; }
1362 
1363 			// normalize
1364 			if (endIdx < startIdx) {
1365 				endIdx = ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS - 1;
1366 			}
1367 
1368 			for (j = startIdx; j <= endIdx; j++) {
1369 				if (row.cells[j]) {
1370 					if (status != ZmFreeBusySchedulerView.STATUS_UNKNOWN) {
1371 						this._allAttendees[j] = this._allAttendees[j] + 1;
1372 						this.updateAllAttendeeCellStatus(j, status);
1373 					}
1374                     if(row.cells[j].className != ZmFreeBusySchedulerView.FREE_CLASS && status == ZmFreeBusySchedulerView.STATUS_WORKING) {
1375                         // do not update anything if the status is already changed
1376                         continue;
1377                     }
1378                     sched._coloredCells.push(row.cells[j]);
1379                     row.cells[j].className = className;
1380                     row.cells[j]._fbStatus = status;
1381 
1382 				}
1383 			}
1384 		}
1385 	}
1386 };
1387 
1388 ZmFreeBusySchedulerView.prototype._updateAllAttendees =
1389 function(status, slots) {
1390 
1391     var currentDate = this._getStartDate();
1392 
1393     for (var i = 0; i < slots.length; i++) {
1394         if(status == ZmFreeBusySchedulerView.STATUS_WORKING) {
1395             this._fbCache.convertWorkingHours(slots[i], currentDate);
1396         }
1397         var startIdx = this._getIndexFromTime(slots[i].s);
1398         var endIdx = this._getIndexFromTime(slots[i].e, true);
1399 
1400         if(slots[i].s <= currentDate.getTime()) {
1401             startIdx = 0;
1402         }
1403 
1404         if(slots[i].e >= currentDate.getTime() + AjxDateUtil.MSEC_PER_DAY) {
1405             endIdx = ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS - 1;
1406         }
1407 
1408         //bug:45623 assume start index is zero if its negative
1409         if(startIdx < 0) {startIdx = 0;}
1410         //bug:45623 skip the slot that has negative end index.
1411         if(endIdx < 0) { continue; }
1412 
1413         // normalize
1414         if (endIdx < startIdx) {
1415             endIdx = ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS - 1;
1416         }
1417 
1418         for (j = startIdx; j <= endIdx; j++) {
1419             if (status != ZmFreeBusySchedulerView.STATUS_UNKNOWN) {
1420                 this._allAttendees[j] = this._allAttendees[j] + 1;
1421                 this.updateAllAttendeeCellStatus(j, status);
1422             }
1423         }
1424     }
1425 };
1426 
1427 /**
1428  * Draws a dark border for the appt's start and end times.
1429  */
1430 ZmFreeBusySchedulerView.prototype._outlineAppt =
1431 function() {
1432 	this._updateBorders(this._allAttendeesSlot, true);
1433 	for (var j = 1; j < this._schedTable.length; j++) {
1434 		this._updateBorders(this._schedTable[j]);
1435 	}
1436     this.resizeKeySpacer();
1437 };
1438 
1439 ZmFreeBusySchedulerView.prototype.resizeKeySpacer =
1440 function() {
1441     var graphKeySpacer = document.getElementById(this._htmlElId + '_graphKeySpacer');
1442     if(graphKeySpacer) {
1443         var size = Dwt.getSize(document.getElementById(this._navToolbarId));
1444         Dwt.setSize(graphKeySpacer, size.x - 6, Dwt.DEFAULT);
1445     }
1446 };
1447 
1448 /**
1449  * Outlines the times of the current appt for the given row.
1450  *
1451  * @param sched				[sched]			info for this row
1452  * @param isAllAttendees	[boolean]*		if true, this is the All Attendees row
1453  */
1454 ZmFreeBusySchedulerView.prototype._updateBorders =
1455 function(sched, isAllAttendees) {
1456 	if (!sched) { return; }
1457 
1458 	var td, div, curClass, newClass;
1459 
1460 	// mark right borders of appropriate f/b table cells
1461 	var normalClassName = "ZmSchedulerGridDiv";
1462 	var halfHourClassName = normalClassName + "-halfHour";
1463 	var startClassName = normalClassName + "-start";
1464 	var endClassName = normalClassName + "-end";
1465 
1466 	var table = document.getElementById(sched.dwtTableId);
1467 	var row = table.rows[0];
1468 	if (row) {
1469 		for (var i = 0; i < ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS; i++) {
1470 		    td = row.cells[i];
1471 			div = td ? td.getElementsByTagName("*")[0] : null;
1472 			if (div) {
1473 				curClass = div.className;
1474 				newClass = normalClassName;
1475 				if (i == this._dateBorder.start) {
1476 					newClass = startClassName;
1477 				} else if (i == this._dateBorder.end) {
1478 					newClass = endClassName;
1479 				} else if (i % 2 == 0) {
1480 					newClass = halfHourClassName;
1481 				}
1482 				if (curClass != newClass) {
1483 					div.className = newClass;
1484 				}
1485 			}
1486 		}
1487 		td = row.cells[0];
1488 		div = td ? td.getElementsByTagName("*")[0] : null;
1489 		if (div && (this._dateBorder.start == -1)) {
1490 		    div.className += " " + normalClassName + "-leftStart";
1491 		}
1492 	}
1493 };
1494 
1495 /**
1496  * Calculate index of the cell that covers the given time. A start time on a
1497  * half-hour border covers the corresponding time block, whereas an end time
1498  * does not. For example, an appt with a start time of 5:00 causes the 5:00 -
1499  * 5:30 block to be marked. The end time of 5:30 does not cause the 5:30 - 6:00
1500  * block to be marked.
1501  *
1502  * @param time		[Date or int]		time
1503  * @param isEnd		[boolean]*			if true, this is an appt end time
1504  * @param adjust	[boolean]*			Specify whether the time should be
1505  * 										adjusted based on timezone selector. If
1506  * 										not specified, assumed to be true.
1507  */
1508 ZmFreeBusySchedulerView.prototype._getIndexFromTime =
1509 function(time, isEnd, adjust) {
1510     var hourmin,
1511         seconds;
1512     adjust = adjust != null ? adjust : true;
1513     if(adjust) {
1514         var dayStartTime = this._getStartTime();
1515         var indexTime = (time instanceof Date) ? time.getTime() : time;
1516         hourmin = (indexTime - dayStartTime)/60000; //60000 = 1000(msec) * 60 (sec) - hence, dividing by 60000 means calculating the minutes and
1517         seconds = (indexTime - dayStartTime)%60000; //mod by 60000 means calculating the seconds remaining
1518     }
1519     else {
1520         var d = (time instanceof Date) ? time : new Date(time);
1521         hourmin = d.getHours() * 60 + d.getMinutes();
1522         seconds = d.getSeconds();
1523     }
1524     var idx = Math.floor(hourmin / 60) * 2;
1525 	var minutes = hourmin % 60;
1526 	if (minutes >= 30) {
1527 		idx++;
1528 	}
1529 	// end times don't mark blocks on half-hour boundary
1530 	if (isEnd && (minutes == 0 || minutes == 30)) {
1531 		// block even if it exceeds 1 second
1532 		//var s = d.getSeconds();
1533 		if (seconds == 0) {
1534 			idx--;
1535 		}
1536 	}
1537 
1538 	return idx;
1539 };
1540 
1541 ZmFreeBusySchedulerView.prototype._getBordersFromDateInfo =
1542 function() {
1543 	// Setup the start/end for an all day appt
1544 	var index = {start: -1, end: ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS-1};
1545 	if (this._dateInfo.showTime) {
1546 		// Not an all day appt, determine the appts start and end
1547 		var idx = AjxDateUtil.isLocale24Hour() ? 0 : 1;
1548 		this._processDateInfo(this._dateInfo);
1549 
1550 		// subtract 1 from index since we're marking right borders
1551 		index.start = this._getIndexFromTime(this._startDate, null, false) - 1;
1552 		if (this._dateInfo.endDate == this._dateInfo.startDate) {
1553 			index.end = this._getIndexFromTime(this._endDate, true, false);
1554 		}
1555 	}
1556 	return index;
1557 };
1558 
1559 ZmFreeBusySchedulerView.prototype._processDateInfo =
1560 function(dateInfo) {
1561     var startDate = AjxDateUtil.simpleParseDateStr(dateInfo.startDate);
1562     var endDate   = AjxDateUtil.simpleParseDateStr(dateInfo.endDate);
1563     if (dateInfo.isAllDay) {
1564         startDate.setHours(0,0,0,0);
1565         this._startDate = startDate;
1566         endDate.setHours(23,59,59,999);
1567         this._endDate   = endDate;
1568     } else {
1569         this._startDate = DwtTimeInput.getDateFromFields(dateInfo.startTimeStr,startDate);
1570         this._endDate   = DwtTimeInput.getDateFromFields(dateInfo.endTimeStr,  endDate);
1571     }
1572 }
1573 
1574 ZmFreeBusySchedulerView.prototype._getClassForStatus =
1575 function(status) {
1576 	return ZmFreeBusySchedulerView.STATUS_CLASSES[status];
1577 };
1578 
1579 ZmFreeBusySchedulerView.prototype._getClassForParticipationStatus =
1580 function(status) {
1581 	return ZmFreeBusySchedulerView.PSTATUS_CLASSES[status];
1582 };
1583 
1584 ZmFreeBusySchedulerView.prototype._getFreeBusyInfo =
1585 function(startTime, emailList, callback) {
1586 
1587     var endTime = startTime + AjxDateUtil.MSEC_PER_DAY;
1588     var emails = emailList.split(",");
1589     var freeBusyParams  = {
1590         emails: emails,
1591         startTime: startTime,
1592         endTime: endTime,
1593         callback: callback
1594     };
1595 
1596     var callback = new AjxCallback(this, this._handleResponseFreeBusy, [freeBusyParams]);    
1597 	var errorCallback = new AjxCallback(this, this._handleErrorFreeBusy, [freeBusyParams]);
1598 
1599     var acct = (appCtxt.multiAccounts)
1600         ? this._editView.getCalendarAccount() : null;
1601 
1602 
1603     var params = {
1604         startTime: startTime,
1605         endTime: endTime,
1606         emails: emails,
1607         callback: callback,
1608         errorCallback: errorCallback,
1609         noBusyOverlay: true,
1610         account: acct
1611     };
1612 
1613     var appt = this._editView.parent.getAppt ? this._editView.parent.getAppt(true) : null;
1614     if (appt) {
1615         params.excludedId = appt.uid;
1616 
1617     }
1618     this._freeBusyRequest = this._fbCache.getFreeBusyInfo(params);
1619 };
1620 
1621 // Callbacks
1622 
1623 ZmFreeBusySchedulerView.prototype._handleResponseFreeBusy =
1624 function(params, result) {
1625 
1626     this._freeBusyRequest = null;
1627     var dateInfo = this._dateInfo;
1628     this._processDateInfo(dateInfo);
1629     // Adjust start and end time by 1 msec, to avoid fencepost problems when detecting conflicts
1630     var apptStartTime = this._startDate.getTime(),
1631         apptEndTime = this._endDate.getTime(),
1632         apptConflictStartTime = apptStartTime+ 1,
1633         apptConflictEndTime   = apptEndTime-1,
1634         appt = this._appt,
1635         orgEmail = appt && !appt.inviteNeverSent ? appt.organizer : null,
1636         apptOrigStartTime = appt ? appt.getOrigStartTime() : null,
1637         apptOrigEndTime = appt ? (dateInfo.isAllDay ? appt.getOrigEndTime() - 1 : appt.getOrigEndTime()) : null,
1638         apptTimeChanged = appt ? !(apptOrigStartTime == apptStartTime && apptOrigEndTime == apptEndTime) : false;
1639 
1640     for (var i = 0; i < params.emails.length; i++) {
1641 		var email = params.emails[i];
1642 
1643 		this._detectConflict(email, apptConflictStartTime, apptConflictEndTime);
1644 
1645 		// first clear out the whole row for this email id
1646 		var sched = this._schedTable[this._emailToIdx[email]],
1647             attendee = sched ? sched.attendee : null,
1648             ptst = attendee ? attendee.getParticipantStatus() : null,
1649             usr = this._fbCache.getFreeBusySlot(params.startTime, params.endTime, email),
1650             table = sched ? document.getElementById(sched.dwtTableId) : null;
1651 
1652         if (usr && (ptst == ZmCalBaseItem.PSTATUS_ACCEPT || email == orgEmail)) {
1653             if (!usr.b) {
1654                 usr.b = [];
1655             }
1656             if (apptTimeChanged) {
1657                 usr.b.push({s:apptOrigStartTime, e: apptOrigEndTime});
1658             }
1659             else {
1660                 usr.b.push({s:apptStartTime, e: apptEndTime});
1661             }
1662         }
1663 
1664 		if (table) {
1665 			table.rows[0].className = "ZmSchedulerNormalRow";
1666 			this._clearColoredCells(sched);
1667 
1668             if(!usr) continue;
1669 			sched.uid = usr.id;
1670 
1671             // next, for each free/busy status, color the row for given start/end times
1672 			if (usr.n) this._colorSchedule(ZmFreeBusySchedulerView.STATUS_UNKNOWN, usr.n, table, sched);
1673 			if (usr.t) this._colorSchedule(ZmFreeBusySchedulerView.STATUS_TENTATIVE, usr.t, table, sched);
1674 			if (usr.b) this._colorSchedule(ZmFreeBusySchedulerView.STATUS_BUSY, usr.b, table, sched);
1675 			if (usr.u) this._colorSchedule(ZmFreeBusySchedulerView.STATUS_OUT, usr.u, table, sched);
1676 		}else {
1677 
1678             //update all attendee status - we update all attendee status correctly even if we have slight
1679             if(!usr) continue;
1680 
1681             if (usr.n) this._updateAllAttendees(ZmFreeBusySchedulerView.STATUS_UNKNOWN, usr.n);
1682             if (usr.t) this._updateAllAttendees(ZmFreeBusySchedulerView.STATUS_TENTATIVE, usr.t);
1683             if (usr.b) this._updateAllAttendees(ZmFreeBusySchedulerView.STATUS_BUSY, usr.b);
1684             if (usr.u) this._updateAllAttendees(ZmFreeBusySchedulerView.STATUS_OUT, usr.u);
1685 
1686         }
1687 	}
1688 
1689     if (this._fbParentCallback) {
1690         this._fbParentCallback.run();
1691     }
1692 
1693     var acct = (appCtxt.multiAccounts)
1694         ? this._editView.getCalendarAccount() : null;
1695     
1696     var workingHrsCallback = new AjxCallback(this, this._handleResponseWorking, [params]);
1697     var errorCallback = new AjxCallback(this, this._handleErrorFreeBusy, [params]);
1698 
1699     //optimization: fetch working hrs for a week - wrking hrs pattern repeat everyweek
1700     var weekStartDate = new Date(params.startTime);
1701     var dow = weekStartDate.getDay();
1702     weekStartDate.setDate(weekStartDate.getDate()-((dow+7))%7);
1703 
1704 
1705     var whrsParams = {
1706         startTime: weekStartDate.getTime(),
1707         endTime: weekStartDate.getTime() + 7*AjxDateUtil.MSEC_PER_DAY,
1708         emails: params.emails,
1709         callback: workingHrsCallback,
1710         errorCallback: errorCallback,
1711         noBusyOverlay: true,
1712         account: acct
1713     };
1714 
1715     this._workingHoursRequest = this._fbCache.getWorkingHours(whrsParams);
1716 };
1717 
1718 ZmFreeBusySchedulerView.prototype._detectConflict =
1719 function(email, startTime, endTime) {
1720     var sched = this._fbCache.getFreeBusySlot(startTime, endTime, email);
1721     var isFree = true;
1722     if(sched.b) isFree = isFree && ZmApptAssistantView.isBooked(sched.b, startTime, endTime);
1723     if(sched.t) isFree = isFree && ZmApptAssistantView.isBooked(sched.t, startTime, endTime);
1724     if(sched.u) isFree = isFree && ZmApptAssistantView.isBooked(sched.u, startTime, endTime);
1725 
1726     this._fbConflict[email] = isFree;
1727 }
1728 
1729 ZmFreeBusySchedulerView.prototype.getConflicts =
1730 function() {
1731     return this._fbConflict;
1732 }
1733 
1734 
1735 
1736 ZmFreeBusySchedulerView.prototype._handleResponseWorking =
1737 function(params, result) {
1738 
1739     this._workingHoursRequest = null;
1740 
1741 	for (var i = 0; i < params.emails.length; i++) {
1742 		var email = params.emails[i];
1743         var usr = this._fbCache.getWorkingHrsSlot(params.startTime, params.endTime, email);
1744 
1745         if(!usr) continue;
1746 
1747 		// first clear out the whole row for this email id
1748 		var sched = this._schedTable[this._emailToIdx[usr.id]];
1749 		var table = sched ? document.getElementById(sched.dwtTableId) : null;
1750 		if (table) {
1751             sched.uid = usr.id;
1752             // next, for each free/busy status, color the row for given start/end times
1753 			if (usr.f) this._colorSchedule(ZmFreeBusySchedulerView.STATUS_WORKING, usr.f, table, sched);
1754             //show entire day as working hours if the information is not available (e.g. external accounts)
1755             if (usr.n) {
1756                 var currentDay = this._getStartDate();
1757                 var entireDaySlot = {
1758                     s: currentDay.getTime(),
1759                     e: currentDay.getTime() + AjxDateUtil.MSEC_PER_DAY
1760                 };
1761                 this._colorSchedule(ZmFreeBusySchedulerView.STATUS_WORKING, [entireDaySlot], table, sched);
1762             }
1763 		}
1764 	}
1765 
1766     if(params.callback) {
1767         params.callback.run();
1768     }
1769 
1770     this.postUpdateHandler();    
1771 };
1772 
1773 ZmFreeBusySchedulerView.prototype.colorAppt =
1774 function(appt, div) {
1775     var idx = this._emailToIdx[appt.getFolder().getOwner()];
1776     var sched = this._schedTable[idx];
1777     var table = sched ? document.getElementById(sched.dwtTableId) : null;
1778     if (table) {
1779         table.rows[0].className = "ZmSchedulerNormalRow";
1780 
1781         //this._clearColoredCells(sched);
1782 
1783         var row = table.rows[0];
1784 
1785         var currentDate = this._getStartDate();
1786 
1787         if (row) {
1788             // figure out the table cell that needs to be colored
1789 
1790             var startIdx = this._getIndexFromTime(appt.startDate);
1791             var endIdx = this._getIndexFromTime(appt.endDate, true);
1792 
1793             if(appt.startDate <= currentDate.getTime()) {
1794                 startIdx = 0;
1795             }
1796 
1797             if(appt.endDate >= currentDate.getTime() + AjxDateUtil.MSEC_PER_DAY) {
1798                 endIdx = ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS - 1;
1799             }
1800 
1801             //bug:45623 assume start index is zero if its negative
1802             if(startIdx < 0) {startIdx = 0;}
1803             //bug:45623 skip the slot that has negative end index.
1804             if(endIdx < 0) { return; }
1805 
1806             // normalize
1807             if (endIdx < startIdx) {
1808                 endIdx = ZmFreeBusySchedulerView.FREEBUSY_NUM_CELLS - 1;
1809             }
1810 
1811             var cb = Dwt.getBounds(row.cells[startIdx]),
1812                 pb = Dwt.toWindow(div.parentNode, 0, 0, null, null, new DwtPoint(0, 0)),
1813                 width = (endIdx-startIdx+1)*cb.width;
1814 
1815             Dwt.setBounds(div, cb.x - pb.x + 1, cb.y - pb.y-1, width-2, cb.height-1);            
1816         }
1817 
1818     }
1819 };
1820 
1821 ZmFreeBusySchedulerView.prototype._handleErrorFreeBusy =
1822 function(params, result) {
1823 
1824     this._freeBusyRequest = null;
1825     this._workingHoursRequest = null;
1826 
1827     if (result.code == ZmCsfeException.OFFLINE_ONLINE_ONLY_OP) {
1828 		var emails = params.emails;
1829 		for (var i = 0; i < emails.length; i++) {
1830 			var e = emails[i];
1831 			var sched = this._schedTable[this._emailToIdx[e]];
1832 			var table = sched ? document.getElementById(sched.dwtTableId) : null;
1833 			if (table) {
1834 				table.rows[0].className = "ZmSchedulerNormalRow";
1835 				this._clearColoredCells(sched);
1836 				sched.uid = e;
1837 				var now = new Date();
1838 				var obj = [{s: now.setHours(0,0,0), e:now.setHours(24,0,0)}];
1839 				this._colorSchedule(ZmFreeBusySchedulerView.STATUS_UNKNOWN, obj, table, sched);
1840 			}
1841 		}
1842 	}
1843 	return false;
1844 };
1845 
1846 ZmFreeBusySchedulerView.prototype._emailValidator =
1847 function(value) {
1848 	var str = AjxStringUtil.trim(value);
1849 	if (str.length > 0 && !AjxEmailAddress.isValid(value)) {
1850 		throw ZmMsg.errorInvalidEmail;
1851 	}
1852 
1853 	return value;
1854 };
1855 
1856 ZmFreeBusySchedulerView.prototype._getDefaultFocusItem =
1857 function() {
1858 	for (var i = 0; i < this._schedTable.length; i++) {
1859 		var sched = this._schedTable[i];
1860 		if (sched && sched.inputObj && !sched.inputObj.disabled) {
1861 			return sched.inputObj;
1862 		}
1863 	}
1864 	return null;
1865 };
1866 
1867 ZmFreeBusySchedulerView.prototype.showFreeBusyToolTip =
1868 function() {
1869 	var fbInfo = this._fbToolTipInfo;
1870 	if (!fbInfo) { return; }
1871 
1872 	var sched = fbInfo.sched;
1873 	var cellIndex = fbInfo.index;
1874 	var tableIndex = fbInfo.tableIndex;
1875 	var x = fbInfo.x;
1876 	var y = fbInfo.y;
1877 
1878 	var attendee = sched.attendee;
1879 	var table = sched ? document.getElementById(sched.dwtTableId) : null;
1880 	if (attendee) {
1881 		var email = this.getEmail(attendee);
1882 
1883 		var startDate  = new Date(this._getStartTime());
1884 		var startTime = startDate.getTime() +  cellIndex*30*60*1000;
1885 		startDate = new Date(startTime);
1886 		var endTime = startTime + 30*60*1000;
1887 		var endDate = new Date(endTime);
1888 
1889         var row = table.rows[0];
1890         var cell = row.cells[cellIndex];
1891         //resolve alias before doing owner mounted calendars search
1892         var params = {
1893             startDate: startDate,
1894             endDate: endDate,
1895             x: x,
1896             y: y,
1897             email: email,
1898             status: cell._fbStatus
1899         };
1900         this.getAccountEmail(params);
1901 	}
1902 	this._fbToolTipInfo = null;
1903 };
1904 
1905 ZmFreeBusySchedulerView.prototype.popupFreeBusyToolTop =
1906 function(params) {
1907     var cc = AjxDispatcher.run("GetCalController"),
1908         treeController =  cc.getCalTreeController(),
1909         calendars = treeController ? treeController.getOwnedCalendars(appCtxt.getApp(ZmApp.CALENDAR).getOverviewId(), params.email) : [],
1910         tooltipContent = "",
1911         i,
1912         length;
1913     if(!params.status) params.status = ZmFreeBusySchedulerView.STATUS_FREE;
1914 
1915     var fbStatusMsg = [];
1916     fbStatusMsg[ZmFreeBusySchedulerView.STATUS_FREE]     = ZmMsg.nonWorking;
1917     fbStatusMsg[ZmFreeBusySchedulerView.STATUS_BUSY]     = ZmMsg.busy;
1918     fbStatusMsg[ZmFreeBusySchedulerView.STATUS_TENTATIVE]= ZmMsg.tentative;
1919     fbStatusMsg[ZmFreeBusySchedulerView.STATUS_OUT]      = ZmMsg.outOfOffice;
1920     fbStatusMsg[ZmFreeBusySchedulerView.STATUS_UNKNOWN]  = ZmMsg.unknown;
1921     fbStatusMsg[ZmFreeBusySchedulerView.STATUS_WORKING]  = ZmMsg.free;
1922 
1923     var calIds = [];
1924     var calRemoteIds = new AjxVector();
1925     for (i = 0, length = calendars.length; i < length; i++) {
1926         var cal = calendars[i];
1927         if (cal && (cal.nId != ZmFolder.ID_TRASH)) {
1928             calIds.push(appCtxt.multiAccounts ? cal.id : cal.nId);
1929             calRemoteIds.add(cal.getRemoteId(), null, true);
1930         }
1931     }
1932     var sharedCalIds = this.getUserSharedCalIds(params.email);
1933     var id;
1934     // Check and remove the duplicates
1935     // otherwise results will be duplicated
1936     if(sharedCalIds) {
1937         for(i=0, length = sharedCalIds.length; i<length; i++) {
1938             id = sharedCalIds[i];
1939             if(id && !calRemoteIds.contains(id)) {
1940                 calIds.push(id);
1941             }
1942         }
1943     }
1944     tooltipContent = "<b>" + ZmMsg.statusLabel + " " + fbStatusMsg[params.status] + "</b>";
1945     if(calIds.length > 0) {
1946         var acct = this._editView.getCalendarAccount();
1947         var emptyMsg = tooltipContent || (acct && (acct.name == params.email) ? fbStatusMsg[params.status] : ZmMsg.unknown);
1948         tooltipContent = cc.getUserStatusToolTipText(params.startDate, params.endDate, true, params.email, emptyMsg, calIds);
1949     }
1950     var shell = DwtShell.getShell(window);
1951     var tooltip = shell.getToolTip();
1952     tooltip.setContent(tooltipContent, true);
1953     tooltip.popup(params.x, params.y, true);
1954 };
1955 
1956 ZmFreeBusySchedulerView.prototype.getUserSharedCalIds =
1957 function(email) {
1958     var organizer = this._schedTable[this._organizerIndex] ? this._schedTable[this._organizerIndex].attendee : null,
1959         organizerEmail = organizer ? this.getEmail(organizer) : "",
1960         activeAcct = appCtxt.getActiveAccount(),
1961         acctEmail = activeAcct ? activeAcct.getEmail() : "";
1962 
1963     if(!email || email == organizerEmail || email == acctEmail) {
1964         return [];
1965     }
1966     if(this._sharedCalIds && this._sharedCalIds[email]) {
1967         return this._sharedCalIds[email];
1968     }
1969     var jsonObj = {GetShareInfoRequest:{_jsns:"urn:zimbraAccount"}};
1970 	var request = jsonObj.GetShareInfoRequest;
1971 	if (email) {
1972 		request.owner = {by:"name", _content:email};
1973 	}
1974 	var result = appCtxt.getAppController().sendRequest({jsonObj:	jsonObj});
1975 
1976     //parse the response
1977     var resp = result && result.GetShareInfoResponse;
1978     var share = (resp && resp.share) ? resp.share : null;
1979     var ids = [];
1980     if(share) {
1981         for(var i=0; i<share.length; i++) {
1982             if(share[i].ownerId && share[i].folderId) {
1983                 var folderId = share[i].ownerId + ":" + share[i].folderId;
1984                 ids.push(folderId);
1985             }
1986         }
1987         if(!this._sharedCalIds) {
1988             this._sharedCalIds = {};
1989         }
1990     }
1991     this._sharedCalIds[email] = ids;
1992     return ids;
1993 };
1994 
1995 //bug: 30989 - getting proper email address from alias
1996 ZmFreeBusySchedulerView.prototype.getAccountEmail =
1997 function(params) {
1998 
1999     if(this._emailAliasMap[params.email]) {
2000         params.email = this._emailAliasMap[params.email];
2001         this.popupFreeBusyToolTop(params);
2002         return;
2003     }
2004 
2005     var soapDoc = AjxSoapDoc.create("GetAccountInfoRequest", "urn:zimbraAccount", null);
2006     var elBy = soapDoc.set("account", params.email);
2007     elBy.setAttribute("by", "name");
2008 
2009     var callback = new AjxCallback(this, this._handleGetAccountInfo, [params]);
2010     var errorCallback = new AjxCallback(this, this._handleGetAccountInfoError, [params]);
2011     appCtxt.getAppController().sendRequest({soapDoc:soapDoc, asyncMode:true, callback: callback, errorCallback:errorCallback});
2012 };
2013 
2014 ZmFreeBusySchedulerView.prototype._handleGetAccountInfo =
2015 function(params, result) {
2016     var response = result.getResponse();
2017     var getAccInfoResponse = response.GetAccountInfoResponse;
2018     var accountName = (getAccInfoResponse && getAccInfoResponse.name) ? getAccInfoResponse.name : null;
2019     if(accountName) {
2020         this._emailAliasMap[params.email] = accountName;
2021     }
2022     params.email = accountName || params.email;
2023     this.popupFreeBusyToolTop(params);
2024 };
2025 
2026 ZmFreeBusySchedulerView.prototype._handleGetAccountInfoError =
2027 function(params, result) {
2028     var email = params.email;
2029 	//ignore the error : thrown for external email ids
2030 	this._emailAliasMap[email] = email;
2031 	this.popupFreeBusyToolTop(params);
2032 	return true;
2033 };
2034 
2035 ZmFreeBusySchedulerView.prototype.initAutoCompleteOnFocus =
2036 function(inputElement) {
2037     if (this._acContactsList && !this._autoCompleteHandled[inputElement._schedTableIdx]) {
2038         this._acContactsList.handle(inputElement);
2039         this._autoCompleteHandled[inputElement._schedTableIdx] = true;
2040     }
2041 };
2042 
2043 // Static methods
2044 
2045 ZmFreeBusySchedulerView._onClick =
2046 function(ev) {
2047 	var el = DwtUiEvent.getTarget(ev);
2048 	var svp = AjxCore.objectWithId(el._schedViewPageId);
2049 	if (!svp) { return; }
2050 };
2051 
2052 ZmFreeBusySchedulerView._onFocus =
2053 function(ev) {
2054 	var el = DwtUiEvent.getTarget(ev);
2055 	var svp = AjxCore.objectWithId(el._schedViewPageId);
2056 	if (!svp) { return; }
2057 
2058 	var sched = svp._schedTable[el._schedTableIdx];
2059 	if (sched) {
2060 		svp._activeInputIdx = el._schedTableIdx;
2061         svp.initAutoCompleteOnFocus(el);
2062 	}
2063 };
2064 
2065 ZmFreeBusySchedulerView._onBlur =
2066 function(ev) {
2067 	var el = DwtUiEvent.getTarget(ev);
2068     if(el._acHandlerInProgress) { return; }
2069 	var svp = AjxCore.objectWithId(el._schedViewPageId);
2070 	if (!svp) { return; }
2071     el._acHandlerInProgress = true;
2072     svp._handleAttendeeField(el);
2073     el._acHandlerInProgress = false;
2074     if (svp._editView) { svp._editView.showConflicts(); }
2075 };
2076 
2077 ZmFreeBusySchedulerView._onPTSTMouseOver =
2078 function(ev) {
2079 	ev = DwtUiEvent.getEvent(ev);
2080 	var el = DwtUiEvent.getTarget(ev);
2081 	var svp = AjxCore.objectWithId(el._schedViewPageId);
2082 	if (!svp) return;
2083 	var sched = svp._schedTable[el._schedTableIdx];
2084 	if (sched) {
2085 		var shell = DwtShell.getShell(window);
2086 		var tooltip = shell.getToolTip();
2087 		tooltip.setContent(el._ptstLabel, true);
2088 		tooltip.popup((ev.pageX || ev.clientX), (ev.pageY || ev.clientY), true);
2089 	}
2090 };
2091 
2092 ZmFreeBusySchedulerView._onPTSTMouseOut =
2093 function(ev) {
2094 	ev = DwtUiEvent.getEvent(ev);
2095 	var el = DwtUiEvent.getTarget(ev);
2096 	var svp = AjxCore.objectWithId(el._schedViewPageId);
2097 	if (!svp) { return; }
2098 
2099 	var sched = svp._schedTable[el._schedTableIdx];
2100 	if (sched) {
2101 		var shell = DwtShell.getShell(window);
2102 		var tooltip = shell.getToolTip();
2103 		tooltip.popdown();
2104 	}
2105 };
2106 
2107 ZmFreeBusySchedulerView._onFreeBusyMouseOver =
2108 function(ev) {
2109 	ev = DwtUiEvent.getEvent(ev);
2110 	var fbDiv = DwtUiEvent.getTarget(ev);
2111 	if (!fbDiv || fbDiv._freeBusyCellIndex == undefined) { return; }
2112 
2113 	var svp = AjxCore.objectWithId(fbDiv._schedViewPageId);
2114 	if (!svp) { return; }
2115 
2116 	var sched = svp._schedTable[fbDiv._schedTableIdx];
2117 	var cellIndex = fbDiv._freeBusyCellIndex;
2118 
2119 	if (svp && sched) {
2120 		svp._fbToolTipInfo = {
2121 			x: (ev.pageX || ev.clientX),
2122 			y: (ev.pageY || ev.clientY),
2123 			el: fbDiv,
2124 			sched: sched,
2125 			index: cellIndex,
2126 			tableIndex: fbDiv._schedTableIdx
2127 		};
2128 		//avoid redundant request to server
2129 		AjxTimedAction.scheduleAction(new AjxTimedAction(svp, svp.showFreeBusyToolTip), 1000);
2130 	}
2131 };
2132 
2133 /**
2134  * Called when "Show more" link is clicked, this module shows all the attendees without pagination
2135  * @param ev click event
2136  */
2137 ZmFreeBusySchedulerView._onShowMore =
2138 function(ev) {
2139 	ev = DwtUiEvent.getEvent(ev);
2140     var showMoreLink = DwtUiEvent.getTarget(ev);
2141     var svp = AjxCore.objectWithId(showMoreLink._schedViewPageId);
2142     if (!svp) { return; }
2143     svp.showMoreResults();
2144 };
2145 
2146 ZmFreeBusySchedulerView._onFreeBusyMouseOut =
2147 function(ev) {
2148 	ev = DwtUiEvent.getEvent(ev);
2149 
2150 	var el = DwtUiEvent.getTarget(ev);
2151 	var svp = el && el._schedViewPageId ? AjxCore.objectWithId(el._schedViewPageId) : null;
2152 	if (!svp) { return; }
2153 
2154 	svp._fbToolTipInfo = null;
2155 	var sched = svp._schedTable[el._schedTableIdx];
2156 	if (sched) {
2157 		var shell = DwtShell.getShell(window);
2158 		var tooltip = shell.getToolTip();
2159 		tooltip.popdown();
2160 	}
2161 };
2162 
2163 ZmFreeBusySchedulerView.prototype.updateAllAttendeeCellStatus =
2164 function(idx, status) {
2165 
2166     if(status == ZmFreeBusySchedulerView.STATUS_WORKING) return;
2167 
2168 	if (!this._allAttendeesStatus[idx]) {
2169 		this._allAttendeesStatus[idx] = status;
2170 	} else if (status!= this._allAttendeesStatus[idx]) {
2171 		if (status != ZmFreeBusySchedulerView.STATUS_UNKNOWN &&
2172 			status != ZmFreeBusySchedulerView.STATUS_FREE)
2173 		{
2174             if(status == ZmFreeBusySchedulerView.STATUS_OUT || this._allAttendeesStatus[idx] == ZmFreeBusySchedulerView.STATUS_OUT) {
2175     			this._allAttendeesStatus[idx] = ZmFreeBusySchedulerView.STATUS_OUT;
2176             }else {
2177             	this._allAttendeesStatus[idx] = ZmFreeBusySchedulerView.STATUS_BUSY;
2178             }
2179 		}
2180 	}
2181 };
2182 
2183 ZmFreeBusySchedulerView.prototype.getAllAttendeeStatus =
2184 function(idx) {
2185 	return this._allAttendeesStatus[idx] ? this._allAttendeesStatus[idx] : ZmFreeBusySchedulerView.STATUS_FREE;
2186 };
2187 
2188 
2189 ZmFreeBusySchedulerView.prototype.enablePartcipantStatusColumn =
2190 function(show) {
2191     for(var i in this._schedTable) {
2192         var sched = this._schedTable[i];
2193         if(sched && sched.ptstObj) {
2194             Dwt.setVisible(sched.ptstObj, show);
2195         }else if(i == this._organizerIndex) {
2196             var ptstObj = document.getElementById(sched.dwtNameId+"_ptst");
2197             Dwt.setVisible(ptstObj, show);
2198         }
2199     }
2200 };
2201 
2202 ZmFreeBusySchedulerView.prototype.enableAttendees =
2203 function(enable) {
2204   for(var i in this._schedTable) {
2205       var sched = this._schedTable[i];
2206       if(sched) {
2207           if(sched.inputObj) {
2208             sched.inputObj.setEnabled(enable);
2209           }
2210           if(sched.btnObj) {
2211             sched.btnObj.setEnabled(enable);
2212           }
2213       }
2214   }
2215 };
2216 
2217 
2218 /**
2219  * Resets pageless mode while rendering attendees list, when pageless mode is enabled all attendees will be shown in
2220  * single list without 'Show more' controls
2221  *
2222  * @param enable	[boolean]*		if true, enable pageless mode
2223  */
2224 ZmFreeBusySchedulerView.prototype.resetPagelessMode =
2225 function(enable) {
2226     this._isPageless = enable;
2227 };
2228 
2229 ZmFreeBusySchedulerView.prototype.showMoreResults =
2230 function() {
2231     //enable pageless mode and render entire list
2232     this.resetPagelessMode(true);
2233     Dwt.setDisplay(this._showMoreLink, Dwt.DISPLAY_NONE);
2234     this.showMe();
2235 };
2236