1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2011, 2012, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * Creates a left pane view for suggesting locations 26 * @constructor 27 * @class 28 * This class displays suggested free locations for a new appointment 29 * 30 * @author Vince Bellows 31 * 32 * @param apptEditView [ZmApptQuickAddDialog or 33 * ZmApptComposeView] View containing the suggestions 34 * @param container [DOM Element] The dialog's content Element 35 * @param controller [ZmApptComposeController] The appt compose controller 36 * @param closeCallback [Callback Function] Function to invoke upon close 37 */ 38 ZmApptAssistantView = function(parent, controller, apptView, closeCallback) { 39 if (arguments.length == 0) { return; } 40 41 DwtComposite.call(this, {parent: parent, posStyle: DwtControl.RELATIVE_STYLE, className: "ZmScheduleAssistantView"}); 42 43 this.setScrollStyle(Dwt.SCROLL_Y); 44 45 this._controller = controller; 46 this._apptView = apptView; 47 this._prefDialog = appCtxt.getSuggestionPreferenceDialog(); 48 this._closeCallback = closeCallback; 49 50 // For bug 68531 51 //var app = appCtxt.getApp(ZmApp.CALENDAR); 52 //this._fbCache = app.getFreeBusyCache(); 53 this._fbCache = apptView.getFreeBusyCache(); 54 55 this._rendered = false; 56 this.type = ZmCalBaseItem.LOCATION; 57 this._resources = []; 58 59 this._enabled = false; 60 61 this.numRecurrence = this.getLocationConflictNumRecurrence(); 62 63 this.initialize(); 64 }; 65 66 ZmApptAssistantView.prototype = new DwtComposite; 67 ZmApptAssistantView.prototype.constructor = ZmApptAssistantView; 68 69 70 ZmApptAssistantView.prototype.toString = 71 function() { 72 return "ZmApptAssistantView"; 73 } 74 75 ZmApptAssistantView.ATTRS = {}; 76 ZmApptAssistantView.ATTRS[ZmCalBaseItem.LOCATION] = 77 ["fullName", "email", "zimbraCalResLocationDisplayName", 78 "zimbraCalResCapacity", "zimbraCalResContactEmail", "description", "zimbraCalResType"]; 79 80 ZmApptAssistantView.prototype.initialize = 81 function() { 82 this._createHTML(); 83 this._createWidgets(); 84 this.addControlListener(this._resetSize.bind(this)); 85 this._resetSize(); 86 }; 87 88 ZmApptAssistantView.prototype.isInitialized = 89 function() { 90 var prefInitialized = this._prefDialog ? this._prefDialog.getPrefLoaded() : false; 91 // Only checking pref Dialog initialization for now 92 return prefInitialized; 93 }; 94 95 ZmApptAssistantView.prototype.cleanup = 96 function() { 97 }; 98 99 ZmApptAssistantView.prototype._createHTML = 100 function() { 101 var subs = { 102 id: this._htmlElId 103 }; 104 this.getHtmlElement().innerHTML = AjxTemplate.expand("calendar.Appointment#SuggestionsView", subs); 105 }; 106 107 ZmApptAssistantView.prototype._createWidgets = 108 function() { 109 110 this._closeId = this._htmlElId + "_suggest_close"; 111 this._closeBtn = document.getElementById(this._closeId); 112 Dwt.setHandler(this._closeBtn, DwtEvent.ONCLICK, this._closeListener.bind(this)); 113 114 this._suggestionContainerElId = this._htmlElId + "_suggest_container"; 115 this._suggestionsContainer = document.getElementById(this._suggestionContainerElId); 116 117 this._suggestionNameElId = this._htmlElId + "_suggestion_name" 118 this._suggestionName = document.getElementById(this._suggestionNameElId); 119 120 this._suggestionViewElId = this._htmlElId + "_suggest_view"; 121 this._suggestionsView = document.getElementById(this._suggestionViewElId); 122 123 this._createMiniCalendar(); 124 this._suggestMinicalElId = this._htmlElId + "_suggest_minical"; 125 this._suggestMinical = document.getElementById(this._suggestMinicalElId); 126 127 this._optionsBtnId = this._htmlElId + "_suggest_options_image"; 128 this._optionsBtn = document.getElementById(this._optionsBtnId); 129 Dwt.setHandler(this._optionsBtn, DwtEvent.ONCLICK, this._prefListener.bind(this)); 130 131 this._configureSuggestionWidgets(); 132 }; 133 134 ZmApptAssistantView.prototype._configureSuggestionWidgets = 135 function() { 136 }; 137 138 ZmApptAssistantView.prototype._createMiniCalendar = 139 function(date) { 140 } 141 142 ZmApptAssistantView.prototype.clearResources = 143 function() { 144 this._resources = []; 145 }; 146 147 148 ZmApptAssistantView.prototype.getLocationConflictNumRecurrence = 149 function() { 150 return this._prefDialog ? 151 parseInt(this._prefDialog.getPreference(ZmTimeSuggestionPrefDialog.RECURRENCE)) : 152 ZmTimeSuggestionPrefDialog.DEFAULT_NUM_RECURRENCE; 153 }; 154 155 ZmApptAssistantView.prototype._prefListener = 156 function(ev) { 157 // Record the current numRecurrence value, for detecting changes upon 158 // completion of the preferences dialog 159 this.numRecurrence = this.getLocationConflictNumRecurrence(); 160 this._prefDialog.popup(this._apptView.getCalendarAccount()); 161 }; 162 163 164 ZmApptAssistantView.prototype._closeListener = 165 function(ev) { 166 this.close(); 167 }; 168 ZmApptAssistantView.prototype.close = 169 function() { 170 var parentEl = this.getHtmlElement().parentNode; 171 Dwt.setVisible(parentEl, false); 172 this._enabled = false; 173 if (this._closeCallback) { 174 this._closeCallback.run(); 175 } 176 }; 177 178 ZmApptAssistantView.prototype.suggestAction = 179 function() { 180 }; 181 182 ZmApptAssistantView.prototype._getTimeFrame = 183 function() { 184 }; 185 186 ZmApptAssistantView.prototype.updateTime = 187 function() { 188 }; 189 190 ZmApptAssistantView.prototype.reset = 191 function(date) { 192 }; 193 194 //smart scheduler suggestion modules 195 196 ZmApptAssistantView.prototype.searchCalendarResources = 197 function(callback, sortBy) { 198 var currAcct = this._apptView.getCalendarAccount(); 199 var value = (this.type == ZmCalBaseItem.LOCATION) ? "Location" : "Equipment"; 200 201 var conds = [{attr: "zimbraCalResType", op: "eq", value: value}]; 202 if(this._prefDialog) { 203 for (var i = 0; i < ZmTimeSuggestionPrefDialog.PREF_FIELDS.length; i++) { 204 var sf = ZmTimeSuggestionPrefDialog.PREF_FIELDS[i]; 205 206 if(!ZmTimeSuggestionPrefDialog.isSearchCondition(sf)) continue; 207 208 value = AjxStringUtil.trim(this._prefDialog.getPreference(sf)); 209 210 if (value.length) { 211 var attr = ZmTimeSuggestionPrefDialog.SF_ATTR[sf]; 212 var op = ZmTimeSuggestionPrefDialog.SF_OP[sf] ? ZmTimeSuggestionPrefDialog.SF_OP[sf] : "has"; 213 conds.push({attr: attr, op: op, value: value}); 214 } 215 } 216 } 217 218 var params = { 219 sortBy: sortBy, 220 offset: 0, 221 limit: ZmContactsApp.SEARCHFOR_MAX, 222 conds: conds, 223 attrs: ZmApptAssistantView.ATTRS[this.type], 224 accountName: appCtxt.isOffline ? currAcct.name : null 225 }; 226 var search = new ZmSearch(params); 227 search.execute({callback: new AjxCallback(this, this._handleResponseSearchCalendarResources, callback)}); 228 }; 229 230 ZmApptAssistantView.prototype._handleResponseSearchCalendarResources = 231 function(callback, result) { 232 var resp = result.getResponse(); 233 var items = resp.getResults(ZmItem.RESOURCE).getVector(); 234 if (items) 235 this._resources = (items instanceof AjxVector) ? items.getArray() : (items instanceof Array) ? items : [items]; 236 if(callback) callback.run(); 237 }; 238 239 // This should only be called for time suggestions 240 ZmApptAssistantView.prototype._findFreeBusyInfo = 241 function(params) { 242 }; 243 244 ZmApptAssistantView.prototype._copyResourcesToParams = 245 function(params, emails) { 246 var list = this._resources; 247 for (var i = list.length; --i >= 0;) { 248 var item = list[i]; 249 var email = item.getEmail(); 250 251 // bug: 30824 - Don't list all addresses/aliases of a resource in 252 // GetFreeBusyRequest. One should suffice. 253 if (email instanceof Array) { 254 email = email[0]; 255 } 256 emails.push(email); 257 258 params.items.push(email); 259 params.itemIndex[email] = params.items.length-1; 260 } 261 } 262 263 ZmApptAssistantView.prototype.suggestLocations = 264 function(params) { 265 var emails = []; 266 this._copyResourcesToParams(params, emails); 267 this._duration = this._apptView.getDurationInfo(); 268 params.emails = emails; 269 params.duration = this._duration.duration; 270 params.locationInfo = this.computeLocationAvailability(this._duration, params); 271 this.renderSuggestions(params); 272 }; 273 274 // For a single given time slot, determine the available rooms 275 ZmApptAssistantView.prototype.computeLocationAvailability = 276 function(durationInfo, params) { 277 278 var locationInfo = { 279 startTime: durationInfo.startTime, 280 endTime: durationInfo.endTime, 281 locations: new AjxVector() 282 }; 283 284 var list = this._resources; 285 for (var i = list.length; --i >= 0;) { 286 var email = list[i].getEmail(); 287 288 if (email instanceof Array) { 289 email = email[0]; 290 } 291 292 var excludeTimeSlots = this._apptView.getFreeBusyExcludeInfo(email); 293 294 // Adjust start and end time by 1 msec, to avoid fencepost problems 295 sched = this._fbCache.getFreeBusySlot(durationInfo.startTime+1, 296 durationInfo.endTime-1, email, excludeTimeSlots); 297 isFree = true; 298 if(sched.b) isFree = isFree && ZmApptAssistantView.isBooked(sched.b, durationInfo.startTime, durationInfo.endTime); 299 if(sched.t) isFree = isFree && ZmApptAssistantView.isBooked(sched.t, durationInfo.startTime, durationInfo.endTime); 300 if(sched.u) isFree = isFree && ZmApptAssistantView.isBooked(sched.u, durationInfo.startTime, durationInfo.endTime); 301 302 //collect all the item indexes of the locations available at this slot 303 if(isFree) { 304 var displayInfo = this._createLocationDisplayInfo(email); 305 locationInfo.locations.add(displayInfo); 306 } 307 } 308 locationInfo.locations.sort(this._compareItems.bind(this)); 309 return locationInfo; 310 }; 311 312 313 ZmApptAssistantView.isBooked = 314 function(slots, startTime, endTime) { 315 for (var i = 0; i < slots.length; i++) { 316 var startConflict = startTime >= slots[i].s && startTime < slots[i].e; 317 var endConflict = endTime > slots[i].s && endTime <= slots[i].e; 318 var inlineSlotConflict = slots[i].s >= startTime && slots[i].e <= endTime; 319 if(startConflict || endConflict || inlineSlotConflict) { 320 return false; 321 } 322 }; 323 return true; 324 }; 325 326 ZmApptAssistantView.prototype._createLocationDisplayInfo = 327 function (email) { 328 var info = { email: email }; 329 info.locationObj = this.getLocationByEmail(email); 330 info.name = email; 331 info.description = ''; 332 if(info.locationObj) { 333 info.name = info.locationObj._fileAs; 334 info.description = info.locationObj.getAttr(ZmResource.F_locationName) || 335 info.locationObj.getAttr(ZmResource.F_name); 336 if (info.description == info.name) { 337 info.description = ''; 338 } 339 info.contactMail = info.locationObj.getAttr(ZmResource.F_contactMail) 340 info.capacity = info.locationObj.getAttr(ZmResource.F_capacity) 341 } 342 return info; 343 } 344 345 ZmApptAssistantView.prototype._sortLocation = function(list) { 346 if (list) { 347 list.sort(this._compareItems.bind(this)); 348 } 349 }; 350 ZmApptAssistantView.prototype._compareItems = function(item1, item2) { 351 var aVal = item1.name.toLowerCase(); 352 var bVal = item2.name.toLowerCase(); 353 354 if (aVal < bVal) { 355 return -1; 356 } else if (aVal > bVal) { 357 return 1; } 358 else { 359 return 0; 360 } 361 362 }; 363 364 ZmApptAssistantView.prototype.renderSuggestions = 365 function(params) { 366 }; 367 368 ZmApptAssistantView.prototype.getLocationByEmail = 369 function(item) { 370 var locations = this._resources; 371 for (var i = 0; i < locations.length; i++) { 372 var value = locations[i].getEmail(); 373 374 if(value instanceof Array) { 375 for(var j = 0; j < value.length; j++) { 376 if(item == value[j]) return locations[i]; 377 } 378 } 379 if (item == value) { 380 return locations[i]; 381 } 382 } 383 return null; 384 }; 385 386 ZmApptAssistantView.prototype._resetSize = function() { 387 if (!this._suggestionsView) { 388 return; 389 } 390 391 var header = this._suggestionsContainer.firstChild; 392 var bounds = this.boundsForChild(this._suggestionsView); 393 var insets = Dwt.getInsets(this._suggestionsView); 394 395 var width = bounds.width - insets.left - insets.right; 396 var height = (bounds.height - Dwt.getOuterSize(this._suggestMinical).y - 397 Dwt.getOuterSize(header).y); 398 399 Dwt.setSize(this._suggestionsView, width, height); 400 }; 401