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