1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file defines a task.
 27  */
 28 
 29 /**
 30  * @class
 31  * 
 32  * This class represents a task.
 33  * 
 34  * @author Parag Shah
 35  *
 36  * @param	{Object}	list		the list
 37  * @param	{int}	id				the task id
 38  * @param	{String}	folderId	the folder id
 39  * 
 40  * @extends    ZmCalItem
 41  */
 42 ZmTask = function(list, id, folderId) {
 43 	ZmCalItem.call(this, ZmItem.TASK, list, id, folderId);
 44 
 45 	this.priority = ZmCalItem.PRIORITY_NORMAL;
 46 	this.pComplete = 0;
 47 	this.status = ZmCalendarApp.STATUS_NEED;
 48     this.startDate = null;
 49     this.endDate = null;
 50     this.remindDate = new Date();
 51     this.alarm = false;
 52 	this._useAbsoluteReminder = true;
 53 };
 54 
 55 ZmTask.prototype = new ZmCalItem;
 56 ZmTask.prototype.constructor = ZmTask;
 57 
 58 
 59 // Consts
 60 
 61 /**
 62  * @private
 63  */
 64 ZmTask.PCOMPLETE_INT = 10;
 65 
 66 /**
 67  * Used to make our own copy because the form will modify the date object by
 68  * calling its setters instead of replacing it with a new date object.
 69  * 
 70  * @private
 71  */
 72 ZmTaskClone = function() { };
 73 ZmTask.quickClone =
 74 function(task) {
 75 	ZmTaskClone.prototype = task;
 76 
 77 	var newTask = new ZmTaskClone();
 78 	newTask.startDate = task.startDate ? (new Date(task.startDate.getTime())) : null;
 79 	newTask.endDate = task.endDate ? (new Date(task.endDate.getTime())) : null;
 80     newTask._uniqId = Dwt.getNextId();
 81 
 82     newTask._validAttachments = AjxUtil.createProxy(task._validAttachments);
 83     
 84 	if (!newTask._orig)
 85 		newTask._orig = task;
 86 
 87 	newTask.type = ZmItem.TASK;
 88 
 89 	return newTask;
 90 };
 91 
 92 /**
 93  * Creates a task from the DOM.
 94  * 
 95  * @param	{Object}	taskNode	the task
 96  * @param	{Hash}		args		arguments
 97  * @param	{Object}	instNode	(not used)
 98  * 
 99  * @return	{ZmTask}		the task
100  */
101 ZmTask.createFromDom =
102 function(taskNode, args, instNode) {
103 	// NOTE: passing ID implies this item should get cached!
104 	var task = new ZmTask(args.list, taskNode.id);
105 	task._loadFromDom(taskNode, instNode);
106 
107 	return task;
108 };
109 
110 
111 // Public Methods
112 
113 /**
114  * Returns a string representation of the object.
115  * 
116  * @return		{String}		a string representation of the object
117  */
118 ZmTask.prototype.toString =
119 function() {
120 	return "ZmTask";
121 };
122 
123 /**
124  * Gets the icon.
125  * 
126  * @return	{String}	the icon
127  */
128 ZmTask.prototype.getIcon = function() { return "Task"; };
129 
130 /**
131  * Gets the folder.
132  * 
133  * @return	{ZmTaskFolder}	the folder
134  */
135 ZmTask.prototype.getFolder =
136 function() {
137 	return appCtxt.getById(this.folderId);
138 };
139 
140 /**
141  *
142  *
143  * @return	{id}	task id
144  */
145 ZmTask.prototype.getId =
146 function() {
147 	return this.id;
148 };
149 /**
150  * Gets the summary.
151  * 
152  * @private
153  */
154 ZmTask.prototype.getSummary =
155 function(isHtml) {
156 	// TODO
157 };
158 
159 /**
160 * Gets the tool tip.
161 * 
162 * @private
163 */
164 ZmTask.prototype.getToolTip =
165 function(controller) {
166 	// TODO
167 	DBG.println("------------ TODO: getTooltip! --------------");
168 };
169 
170 /**
171  * @private
172  */
173 ZmTask.prototype.notifyModify =
174 function(obj) {
175 	ZmItem.prototype.notifyModify.call(this, obj);
176 
177     this.uid = obj.uid;
178     if (obj.l) this.folderId = obj.l;
179 
180 	// update this task with notify data
181 	this._loadFromDom(obj);
182 	this._notify(ZmEvent.E_MODIFY, obj);
183 };
184 
185 /**
186  * Checks if this task is past due.
187  * 
188  * @return	{Boolean}	<code>true</code> if the task is past due
189  */
190 ZmTask.prototype.isPastDue =
191 function() {
192 	return (this.endDate && ((new Date()).getTime() > this.endDate.getTime()));
193 };
194 
195 /**
196  * Gets the end time.
197  *
198  * @return	{Date}	the end time
199  */
200 ZmTask.prototype.getEndTime = function() { return this.endDate ? this.endDate.getTime() : null; }; 	// end time in ms
201 
202 
203 /**
204  * Gets the start time.
205  *
206  * @return	{Date}	the start time
207  */
208 ZmTask.prototype.getStartTime = function() { return this.startDate ? this.startDate.getTime() : null; }; 	// start time in ms
209 
210 
211 /**
212  * Checks if the task is complete.
213  * 
214  * @return	{Boolean}	<code>true</code> if the task is complete
215  */
216 ZmTask.prototype.isComplete =
217 function() {
218 	return (this.pComplete == 100) || (this.status == ZmCalendarApp.STATUS_COMP);
219 };
220 
221 /**
222  * Gets the percent complete (between 0 and 100).
223  * 
224  * @return	{int}	the percentage complete
225  */
226 ZmTask.prototype.getPercentComplete =
227 function() {
228 	return this.pComplete;
229 };
230 
231 /**
232  * Gets the status.
233  * 
234  * @return	{int}		the status
235  * 
236  * @see	ZmCalendarApp.STATUS_COMP
237  * @see	ZmCalendarApp.STATUS_DEFR
238  * @see	ZmCalendarApp.STATUS_INPR
239  * @see	ZmCalendarApp.STATUS_NEED
240  * @see	ZmCalendarApp.STATUS_WAIT
241  * 
242  * @see	ZmCalItem.getLabelForStatus
243  */
244 ZmTask.prototype.getStatus =
245 function() {
246 	return	this.status;
247 }
248 
249 /**
250  * Gets the priority.
251  * 
252  * @return	{int}		the priority
253  * 
254  * @see	ZmCalItem.PRIORITY_LOW
255  * @see	ZmCalItem.PRIORITY_NORMAL
256  * @see	ZmCalItem.PRIORITY_HIGH
257  * @see	ZmCalItem.getLabelForPriority
258  * @see	ZmCalItem.getImageForPriority
259  */
260 ZmTask.prototype.getPriority =
261 function() {
262 	return	this.priority;
263 }
264 
265 /**
266 * Simplify deleting/canceling of Tasks by just not worrying about attendees,
267 * recurrence, etc. and always assume it will use BatchRequest. At some point,
268 * when Tasks supports attendees (aka assignment) and/or recurrence, this method
269 * will have to go thru ZmCalItem (share code with ZmAppt).
270 *
271 * @param mode		[Int]				Required constant. Usually ZmCalItem.MODE_DELETE
272 * @param batchCmd	[ZmBatchCommand]	Required API for batch request
273 * 
274 * @private
275 */
276 ZmTask.prototype.cancel =
277 function(mode, batchCmd) {
278 	this.setViewMode(mode);
279     var jsonObj = {},
280         requestName = this._getRequestNameForMode(mode),
281         request = jsonObj[requestName] = {
282             _jsns : "urn:zimbraMail"
283         };
284 	this._addInviteAndCompNum(request);
285 
286 	// NOTE: we dont bother w/ handling the response - since UI gets updated via notifications
287 	batchCmd.addRequestParams(jsonObj);
288 };
289 
290 /**
291  * Gets the "owner" of remote/shared calItem folder this calItem belongs to.
292  * 
293  * @return	{ZmFolder}		the folder
294  */
295 ZmTask.prototype.getRemoteFolderOwner =
296 function() {
297 	// bug fix #18855 - dont return the folder owner if moving betw. accounts
298 	var controller = AjxDispatcher.run("GetTaskController");
299 	if (controller.isMovingBetwAccounts(this, this.folderId)) {
300 		return null;
301 	}
302 	var folder = this.getFolder();
303 	return (folder && folder.link) ? folder.owner : null;
304 };
305 
306 // Private/protected methods
307 
308 /**
309  * @private
310  */
311 ZmTask.prototype._getDefaultFolderId =
312 function() {
313 	return ZmOrganizer.ID_TASKS;
314 };
315 
316 /**
317  * @private
318  */
319 ZmTask.prototype._loadFromDom =
320 function(node, instNode) {
321 	var inv = node.inv ? node.inv[0] : null
322 	var comp = inv ? inv.comp[0] : null;
323 
324 	if (!node.id) this.id = node.id;
325 	// always re-compute invId if given since its mutable
326 	if (node.invId) {
327 		this.invId = node.invId;
328 	} else if (inv) {
329 		var remoteIndex = inv.id;
330         remoteIndex = remoteIndex.toString().indexOf(":");
331 		if (remoteIndex != -1) {
332 			this.invId = this.id + "-" + inv.id.substring(remoteIndex+1);
333 		} else {
334 			this.invId = [node.id, inv.id].join("-");
335 		}
336 	}
337 	this.uid = node.uid; // XXX: what is this?
338 
339 	if (node.l) this.folderId = node.l;
340 	if (node.s) this.size = node.s;
341 	if (node.sf) this.sf = node.sf;
342 
343     this.allDayEvent	= (instNode ? instNode.allDay : null || node.allDay)  ? "1" : "0";
344 
345     var nodeInst = node.inst && node.inst.length > 0 ? node.inst[0] : null;
346     var tzo = this.tzo = nodeInst && nodeInst.tzo != null ? parseInt(nodeInst.tzo) : 0;
347     var tzoDue = this.tzoDue = nodeInst && nodeInst.tzoDue != null ? parseInt(nodeInst.tzoDue) : 0;
348 
349     if (nodeInst && nodeInst.s) {
350         var adjustMs = this.isAllDayEvent() ? (tzo + new Date(parseInt(nodeInst.s,10)).getTimezoneOffset()*60*1000) : 0;
351         var startTime = parseInt(nodeInst.s,10) + adjustMs;
352         this.startDate = new Date(startTime);
353         this.uniqStartTime = this.startDate.getTime();
354     } else {
355         if (comp) {
356             this.startDate = null;
357             if (comp.s && comp.s[0].d) {
358                 var start = comp.s[0].d;
359                 var yyyy = parseInt(start.substr(0,4), 10);
360                 var MM = parseInt(start.substr(4,2), 10);
361                 var dd = parseInt(start.substr(6,2), 10);
362                 this.startDate = new Date(yyyy, MM -1, dd);
363             }
364         }
365     }
366 
367     if (nodeInst && nodeInst.dueDate) {
368         var adjustMs = this.isAllDayEvent() ? (tzoDue + new Date(parseInt(nodeInst.dueDate,10)).getTimezoneOffset()*60*1000) : 0;
369         var endTime = parseInt(nodeInst.dueDate,10) + adjustMs;
370         this.endDate = new Date(endTime);
371     } else {
372         if(comp) {
373             this.endDate = null;
374             if (comp.e && comp.e[0].d) {
375                 var end = comp.e[0].d;
376                 var yyyy = parseInt(end.substr(0,4), 10);
377                 var MM = parseInt(end.substr(4,2), 10);
378                 var dd = parseInt(end.substr(6,2), 10);
379                 this.endDate = new Date(yyyy, MM -1, dd);
380             }
381         }
382     }
383 
384     if(node.alarm){
385         if(node.alarm) this.alarm = node.alarm;
386         if(node.alarmData) this.alarmData = this._getAttr(node, comp, "alarmData");
387     } else {
388        if(comp && comp.alarm && (comp.alarm.length > 0) ){
389          this.alarm = node.alarm = true;
390          this.alarmData = node.alarmData = comp.alarm;
391        }
392     }
393 
394     if (node.name || comp)				this.name		= this._getAttr(node, comp, "name");
395 	if (node.loc || comp)				this.location	= this._getAttr(node, comp, "loc");
396 	if (node.allDay || comp)			this.setAllDayEvent(this._getAttr(node, comp, "allDay"));
397 	if (node.priority || comp)			this.priority	= parseInt(this._getAttr(node, comp, "priority"));
398 	if (node.percentComplete || comp)	this.pComplete	= parseInt(this._getAttr(node, comp, "percentComplete"));
399 	if (node.status || comp)			this.status	= this._getAttr(node, comp, "status");
400 	if (node.isOrg || comp)				this.isOrg		= new Boolean(this._getAttr(node, comp, "isOrg"));
401 	if (node.or || comp)				this.organizer	= node.or ? node.or.a : (comp.or ? comp.or.a : null);
402 	if (node.ptst || comp)				this.ptst		= this._getAttr(node, comp, "ptst");
403 	if (node.compNum != null)			this.compNum	= (this._getAttr(node, comp, "compNum") || "0");
404 
405 	if (node.f)	this._parseFlags(node.f);
406 	if (node.tn) {
407 		this._parseTagNames(node.tn);
408 	}
409 
410     this.type = ZmItem.TASK;
411 };
412 
413 /**
414  * Checks if alarm is in range (based on current time).
415  *
416  * @return	{Boolean}	<code>true</code> if the alarm is in range
417  */
418 ZmTask.prototype.isAlarmInRange =
419 function() {
420 	if (!this.alarmData) { return false; }
421 
422 	var alarmData = this.alarmData[0];
423 
424 	if (!alarmData) { return false; }
425 
426     this._nextAlarmTime = new Date(alarmData.nextAlarm);
427     this._alarmInstStart = this.adjustMS(alarmData.alarmInstStart, this.tzo);
428 
429 	var currentTime = (new Date()).getTime();
430  
431     return this._nextAlarmTime <= currentTime;
432 };
433 
434 /**
435  * @private
436  */
437 ZmTask.prototype._getAttr =
438 function(node, comp, name) {
439 	if (node[name] != null) return node[name];
440 	if (comp) return comp[name];
441 	return null;
442 };
443 
444 /**
445  * Checks if alarm is modified.
446  *
447  * @return	{Boolean}	<code>true</code> if the alarm is modified
448  */
449 ZmTask.prototype.isAlarmModified =
450 function() {
451     if(this._orig.alarm == true && this.alarm == false) {
452         return true;
453     }
454     return false;
455 }
456 
457 /**
458  * Checks if this item is multi-day.
459  *
460  * @return	{Boolean}	<code>true</code> if start date and end date are on different days
461  *
462  * @see		#getStartTime
463  * @see		#getEndTime
464  */
465 ZmTask.prototype.isMultiDay =
466 function() {
467 	var start = this.startDate;
468 	var end = this.endDate;
469 
470     if(!start && !end) { return false; }
471 
472     if(!start) { return false; }
473 
474     //bug:55197 for task both startdate & enddate time is 00:00:00, so skipping the time based logic to check multiday or not
475 	/*
476     if (end.getHours() == 0 && end.getMinutes() == 0 && end.getSeconds() == 0) {
477 		// if end is the beginning of day, then disregard that it
478 		// technically crossed a day boundary for the purpose of
479 		// determining if it is a multi-day appt
480         end = new Date(end.getTime() - 2 * AjxDateUtil.MSEC_PER_HOUR);
481 	}*/
482 
483 	return (start.getDate() != end.getDate()) ||
484 		   (start.getMonth() != end.getMonth()) ||
485 		   (start.getFullYear() != end.getFullYear());
486 };
487 
488 /**
489  * @private
490  */
491 ZmTask.prototype._setExtrasFromMessage =
492 function(message) {
493     ZmCalItem.prototype._setExtrasFromMessage.apply(this, arguments);
494 
495 	this.location = message.invite.getLocation();
496 };
497 
498 /**
499  * @private overriden to set endDate to be null only if endDate is empty
500  * @param message
501  * @param viewMode
502  */
503 ZmTask.prototype._setTimeFromMessage =
504 function(message, viewMode) {
505     ZmCalItem.prototype._setTimeFromMessage.apply(this, arguments);
506     if(message.invite.components[0].s == null){
507         this.startDate = null;
508     }
509 };
510 
511 /**
512  * @private
513  */
514 ZmTask.prototype.parseAlarm =
515 function(tmp) {
516 	if (!tmp) { return; }
517 
518 	var d;
519 	var trigger = (tmp) ? tmp.trigger : null;
520 	var abs = (trigger && (trigger.length > 0)) ? trigger[0].abs : null;
521 	d = (abs && (abs.length > 0)) ? abs[0].d : null;
522 
523 	this._reminderMinutes = 0;
524 	if (tmp && (tmp.action == "DISPLAY")) {
525 		if (d != null) {
526 			this._reminderAbs = d;
527             this.remindDate = d ? AjxDateUtil.parseServerDateTime(d) : null;
528 		}
529 	}
530 };
531 
532 
533 /**
534  * @private
535  */
536 ZmTask.prototype._getRequestNameForMode =
537 function(mode, isException) {
538 	switch (mode) {
539 		case ZmCalItem.MODE_NEW:
540 			return "CreateTaskRequest";
541 
542 		case ZmCalItem.MODE_EDIT_SINGLE_INSTANCE:
543 			return !isException
544 				? "CreateTaskExceptionRequest"
545 				: "ModifyTaskRequest";
546 
547 		case ZmCalItem.MODE_EDIT:
548 		case ZmCalItem.MODE_EDIT_SERIES:
549 			return "ModifyTaskRequest";
550 
551 		case ZmCalItem.MODE_DELETE:
552 		case ZmCalItem.MODE_DELETE_SERIES:
553 		case ZmCalItem.MODE_DELETE_INSTANCE:
554 			return "CancelTaskRequest";
555 
556 		case ZmCalItem.MODE_GET:
557 			return "GetTaskRequest";
558 	}
559 
560 	return null;
561 };
562 
563 /**
564  * @private
565  */
566 ZmTask.prototype._addExtrasToRequest =
567 function(request, comp) {
568 	ZmCalItem.prototype._addExtrasToRequest.call(this, request, comp);
569 
570 	comp.percentComplete = this.pComplete;
571 
572 	// TODO - set "completed" if applicable
573 };
574 
575 /**
576  * @private
577  */
578 ZmTask.prototype._getInviteFromError =
579 function(result) {
580 	return (result._data.GetTaskResponse.task[0].inv[0]);
581 };
582 
583 /**
584  * @private
585  */
586 ZmTask.prototype.setTaskReminder =
587 function(absStr) {
588     this._reminderAbs = absStr;
589 };
590 
591 /**
592  * Checks if the task is in the trash.
593  *
594  * @param	{ZmTask}	task		the task
595  * @return	{Boolean}	<code>true</code> if in trash
596  */
597 ZmTask.isInTrash =
598 function(task) {
599 	var folderId = (task instanceof ZmTask) ? task.folderId : task.l;
600 	var folder = appCtxt.getById(folderId);
601 	return (folder && folder.isInTrash());
602 };
603