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