Merge branch '2015.11' into 2015.11-develop
[tine20] / tine20 / HumanResources / js / DatePicker.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Alexander Stintzing <a.stintzing@metaways.de>
6  * @copyright   Copyright (c) 2012-2016 Metaways Infosystems GmbH (http://www.metaways.de)
7  */
8 Ext.ns('Tine.HumanResources');
9
10 /**
11  * @namespace   Tine.HumanResources
12  * @class       Tine.HumanResources.FreeTimeEditDialog
13  * @extends     Tine.widgets.dialog.EditDialog
14  * 
15  * <p>DatePicker with multiple days</p>
16  * <p></p>
17  * 
18  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
19  * @author      Alexander Stintzing <a.stintzing@metaways.de>
20  * Create a new Tine.HumanResources.DatePicker
21  */
22 Tine.HumanResources.DatePicker = Ext.extend(Ext.DatePicker, {
23     
24     recordClass: null,
25     app: null,
26     
27     /**
28      * the employee to use for this freetime
29      * 
30      * @type {Tine.HumanResources.Model.Employee}
31      */
32     employee: null,
33     
34     /**
35      * dates higlighted as vacation day
36      * 
37      * @type {Array}
38      */
39     vacationDates: null,
40     
41     /**
42      * dates higlighted as feast day
43      * 
44      * @type {Array}
45      */
46     feastDates : null,
47     
48     /**
49      * dates higlighted as sickness day
50      * 
51      * @type {Array}
52      */
53     sicknessDates: null,
54     
55     /**
56      * holds the freetime type (SICKNESS or VACATION)
57      * 
58      * @type {String}
59      */
60     freetimeType: null,
61     
62     /**
63      * the editdialog this is nested in
64      * 
65      * @type {Tine.HumanResources.FreeTimeEditDialog}
66      */
67     editDialog: null,
68     
69     /**
70      * if vacation is handled, the account picker of the edit dialog is active
71      * 
72      * @type {Boolean}
73      */
74     accountPickerActive: null,
75     
76     dateProperty: 'date',
77     recordsProperty: 'freedays',
78     foreignIdProperty: 'freetime_id',
79     useWeekPickerPlugin: false,
80     
81     /**
82      * holds the previous year selected (to switch back on no account found exception
83      * 
84      * @type {Number}
85      */
86     previousYear: null,
87     
88     /**
89      * holds the current year selected
90      * 
91      * @type {Number}
92      */
93     currentYear: null,
94
95     /**
96      * used to enable/disable checks in update()
97      */
98     initializing: true,
99     
100     /**
101      * initializes the component
102      */
103     initComponent: function() {
104         if (this.useWeekPickerPlugin) {
105             this.plugins = this.plugins ? this.plugins : [];
106             this.plugins.push(new Ext.ux.DatePickerWeekPlugin({
107                 weekHeaderString: Tine.Tinebase.appMgr.get('Calendar').i18n._('WK')
108             }));
109         }
110         
111         this.vacationDates = [];
112         this.sicknessDates = [];
113         this.feastDates    = [];
114         
115         this.initStore();
116
117         Tine.HumanResources.DatePicker.superclass.initComponent.call(this);
118     },
119     
120     /**
121      * initializes the store
122      */
123     initStore: function() {
124         Tine.log.debug('Initializing the store...');
125         var picker = this;
126         this.store = new Tine.Tinebase.data.RecordStore({
127             remoteSort: false,
128             recordClass: this.recordClass,
129             autoSave: false,
130             getByDate: function(date) {
131                 if (!Ext.isDate(date)) {
132                     date = new Date(date);
133                 }
134                 var index = this.findBy(function(record) {
135                     if(record.get(picker.dateProperty).toString() == date.toString()) {
136                         return true;
137                     }
138                 });
139                 return this.getAt(index);
140             },
141             getFirstDay: function() {
142                 this.sort('date', 'ASC');
143                 return this.getAt(0);
144             },
145             
146             getLastDay: function() {
147                 this.sort('date', 'ASC');
148                 return this.getAt(this.getCount() - 1);
149             }
150         }, this);
151     },
152     
153     /**
154      * loads the feast days of the configured feast calendar from the server
155      * 
156      * @param {Boolean} fromYearChange
157      * @param {Boolean} onInit
158      * @param {Date} date
159      * @param {Number} year
160      */
161     loadFeastDays: function(fromYearChange, onInit, date, year) {
162         
163         Tine.log.debug('Loading feast and freedays, using the year ' + year);
164         
165         this.disableYearChange = fromYearChange;
166         
167         var employeeId = this.editDialog.fixedFields.get('employee_id').id;
168         var freeTimeId = this.editDialog.record.get('id') ? this.editDialog.record.get('id') : null;
169         var accountId  = this.editDialog.record.get('account_id') ? this.editDialog.record.get('account_id').id : null;
170         this.loadMask = new Ext.LoadMask(this.getEl(), {
171             msg: this.app.i18n._('Loading calendar data...')
172         });
173         
174         this.loadMask.show();
175         
176         var that = this;
177         
178         Ext.Ajax.request({
179             url : 'index.php',
180             params : { 
181                 method:      'HumanResources.getFeastAndFreeDays', 
182                 _employeeId: employeeId, 
183                 _year:       year, 
184                 _freeTimeId: freeTimeId,
185                 _accountId:  accountId
186             },
187             success : function(_result, _request) {
188                 that.onFeastDaysLoad(Ext.decode(_result.responseText), onInit, date, year);
189             },
190             failure : function(exception) {
191                 Tine.log.debug('Loading feast and freedays failed with the exception:', exception);
192                 Tine.Tinebase.ExceptionHandler.handleRequestException(exception, that.onFeastDaysLoadFailureCallback, that);
193             },
194             scope: that
195         });
196     },
197     
198     /**
199      * loads the feast days from loadFeastDays
200      * 
201      * @param {Object} result
202      * @param {Boolean} onInit
203      * @param {Date} date
204      * @param {Number} year
205      */
206     onFeastDaysLoad: function(result, onInit, date, year) {
207         Tine.log.debug('Loaded feast and freedays for the year ' + year + ':');
208         var rr = result.results;
209         Tine.log.debug(rr);
210         Tine.log.debug(result);
211
212         this.initializing = onInit;
213
214         this.disabledDates = [];
215         //  days not to work on by contract
216         var exdates = rr.excludeDates || [];
217         var freetime = this.editDialog.record;
218         
219         // format dates to fit the datepicker format
220         Ext.each(exdates, function(d) {
221             Ext.each(d, function(date) {
222                 // TODO invent helper function for this date splitting stuff
223                 var split = date.date.split(' '), dateSplit = split[0].split('-');
224                 var disabledDate = new Date(dateSplit[0], dateSplit[1] - 1, dateSplit[2]);
225                 this.disabledDates.push(disabledDate);
226             }, this);
227         }, this);
228
229         this.setVacationDates(this.editDialog.localVacationDays);
230         this.setSicknessDates(this.editDialog.localSicknessDays);
231         this.setFeastDates(rr.feastDays);
232
233         this.setDisabledDates(this.disabledDates);
234         
235         this.updateCellClasses();
236         
237         var split = rr.firstDay.date.split(' '), dateSplit = split[0].split('-');
238         var firstDay = new Date(dateSplit[0], dateSplit[1] - 1, dateSplit[2]);
239         this.setMinDate(firstDay);
240         
241         var split = rr.lastDay.date.split(' '), dateSplit = split[0].split('-');
242         var lastDay = new Date(dateSplit[0], dateSplit[1] - 1, dateSplit[2]);
243         // allow too book last years leftover holidays
244         lastDay.setFullYear(lastDay.getFullYear() + 1);
245         this.setMaxDate(lastDay);
246         
247         // if ownFreeDays is empty, the record hasn't been saved already, so use the properties from the local record
248         var iterate = (rr.ownFreeDays && rr.ownFreeDays.length > 0) ? rr.ownFreeDays : (freetime ? freetime.get('freedays') : null);
249         
250         if (Ext.isArray(iterate)) {
251             Ext.each(iterate, function(fd) {
252                 var split = fd.date.split(' '), dateSplit = split[0].split('-');
253                 fd.date = new Date(dateSplit[0], dateSplit[1] - 1, dateSplit[2]);
254                 fd.date.clearTime();
255                 this.store.add(new this.recordClass(fd));
256             }, this);
257         }
258
259         if (this.accountPickerActive && onInit) {
260             // set remaining vacation days in edit dialog
261             // TODO this should be refactored and moved to edit dialog as this is an unexpected place here
262             var substractDays = this.editDialog.getDaysToSubstract();
263             this.editDialog.getForm().findField('remaining_vacation_days').setValue(rr.allVacation - substractDays);
264         }
265         
266         this.updateCellClasses();
267         this.loadMask.hide();
268
269         if (onInit) {
270             this.previousYear = this.currentYear;
271             this.currentYear = parseInt(rr.firstDay.date.split('-')[0]);
272         }
273
274         var focusDate = freetime.get('firstday_date');
275         if (date) {
276             focusDate = date;
277         } else if (this.disableYearChange) {
278             if (this.previousYear < this.currentYear) {
279                 focusDate = new Date(this.currentYear + '/01/01 12:00:00 AM');
280             } else {
281                 focusDate = new Date(this.currentYear + '/12/31 12:00:00 AM');
282             }
283         }
284
285         // disableYearChange here to make sure we don't load feast and free days again during update() or enable()
286         // TODO this needs to be refactored!!
287         this.disableYearChange = true;
288
289         // focus
290         if (focusDate) {
291             Tine.log.debug('focusDate ' + focusDate + ' currentYear ' + this.currentYear);
292             this.update(focusDate);
293         }
294
295         this.enable();
296         
297         this.disableYearChange = false;
298     },
299     
300     /**
301      * if loading feast and freedays fails
302      */
303     onFeastDaysLoadFailureCallback: function() {
304         
305         Tine.log.debug('Calling onFeastDaysLoadFailureCallback with current year ' + this.currentYear + ' and previous year ' + this.previousYear);
306         
307         var year = this.currentYear;
308         this.currentYear = this.previousYear;
309         this.previousYear = year;
310         this.onYearChange();
311     },
312     
313     /**
314      * set vacation dates
315      * 
316      * @param {Object} localVacationDays
317      * @param {Array} remoteVacationDays
318      * @param {Object} locallyRemovedDays
319      */
320     setVacationDates: function(localVacationDays, remoteVacationDays, locallyRemovedDays) {
321         this.vacationDates = this.getTimestampsFromDays(localVacationDays, remoteVacationDays, locallyRemovedDays);
322     },
323     
324     /**
325      * set sickness dates
326      * 
327      * @param {Object} localSicknessDays
328      * @param {Array} remoteSicknessDays
329      * @param {Object} locallyRemovedDays
330      */
331     setSicknessDates: function(localSicknessDays, remoteSicknessDays, locallyRemovedDays) {
332         this.sicknessDates = this.getTimestampsFromDays(localSicknessDays, remoteSicknessDays, locallyRemovedDays);
333     },
334     
335     /**
336      * set feast dates
337      */
338     setFeastDates: function(feastDays) {
339         this.feastDates = this.getTimestampsFromDays([], feastDays);
340     },
341     
342     /**
343      * returns a timestamp from a day
344      * 
345      * @param {Object} localDays
346      * @param {Array} remoteDays
347      * @param {Object} locallyRemovedDays
348      * 
349      * @return {Array}
350      */
351     getTimestampsFromDays: function(localDays, remoteDays, locallyRemovedDays) {
352         
353         var dates = [];
354         Ext.iterate(localDays, function(accountId, localdates) { 
355             for (var index = 0; index < localdates.length; index++) {
356                 var newdate = new Date(localdates[index].date.replace(/-/g,'/') + ' AM');
357                 newdate.setHours(0);
358                 dates.push((newdate.getTime()));
359             }
360         });
361         
362         // find out removed dates
363         var remove = [];
364         if (locallyRemovedDays) {
365             Ext.iterate(locallyRemovedDays, function(accountId, removeDays) {
366                 for (var index = 0; index < removeDays.length; index++) {
367                     remove.push(removeDays[index].date.split(' ')[0]);
368                 }
369             }, this);
370         }
371         
372         // do not mark day as taken, if it is deleted already in the grid
373         if (remoteDays) {
374             for (var index = 0; index < remoteDays.length; index++) {
375                 var day = remoteDays[index].date.split(' ')[0];
376                 if (remove.indexOf(day) == -1) {
377                     var newdate = new Date(remoteDays[index].date.replace(/-/g,'/') + ' AM');
378                     dates.push(newdate.getTime());
379                 }
380             }
381         }
382         
383         return dates;
384     },
385     
386     /**
387      * is called on year change
388      *
389      * @param {Date} date
390      */
391     onYearChange: function(date) {
392         Tine.log.debug('Calling onYearChange with the date ' + date);
393         // this is called on changing the year in the picker
394         this.loadFeastDays(true, false, date, date.format('Y'));
395     },
396     
397     /**
398      * overwrites update function of superclass
399      * 
400      * @param {Date} date
401      * @param {Boolean} forceRefresh
402      */
403     update : function(date, forceRefresh) {
404         Tine.HumanResources.DatePicker.superclass.update.call(this, date, forceRefresh);
405         
406         if (! this.disableYearChange && ! this.initializing) {
407             var year = parseInt(date.format('Y'));
408             if (year !== this.currentYear) {
409                 if (this.getData().length > 0) {
410                     Ext.MessageBox.show({
411                         title: this.app.i18n._('Year can not be changed'),
412                         msg: this.app.i18n._('You have already selected some dates from another year. Please create a new record to add dates from another year!'),
413                         buttons: Ext.Msg.OK,
414                         icon: Ext.MessageBox.WARNING,
415                         // jump to the first day of the selected
416                         fn: function () {
417                             var firstDay = this.store.getFirstDay();
418                             this.update(firstDay.get('date'));
419                         },
420                         scope: this
421                     });
422                 } else {
423                     this.previousYear = this.currentYear;
424                     this.currentYear = parseInt(date.format('Y'));
425                     this.onYearChange(date);
426                 }
427             }
428         }
429         
430         this.updateCellClasses();
431     },
432
433     /**
434      * removes or adds a date on date click
435      * 
436      * @param {Object} e
437      * @param {Object} t
438      */
439     handleDateClick: function(e, t) {
440         // don't handle date click, if this is disabled, or the clicked node doesn't have a timestamp assigned
441         if (this.disabled || ! t.dateValue) {
442             return Tine.HumanResources.DatePicker.superclass.handleDateClick.call(this, e, t);
443         }
444         // don't handle click on disabled dates defined by contract or feast calendar
445         if (Ext.fly(t.parentNode).hasClass('x-date-disabled')) {
446             return Tine.HumanResources.DatePicker.superclass.handleDateClick.call(this, e, t);
447         }
448         
449         // dont't handle click on already defined sickness days
450         if (Ext.fly(t.parentNode).hasClass('hr-date-sickness')) {
451             return Tine.HumanResources.DatePicker.superclass.handleDateClick.call(this, e, t);
452         }
453         
454         var date = new Date(t.dateValue),
455             existing = this.store.getByDate(date);
456             
457         date.clearTime();
458         
459         if (this.accountPickerActive) {
460
461             var remaining = this.editDialog.getForm().findField('remaining_vacation_days').getValue();
462
463             if (existing) {
464                 remaining++;
465             } else {
466                 remaining--;
467             }
468
469             if (remaining < 0) {
470                 Ext.MessageBox.show({
471                     title: this.app.i18n._('No more vacation days'),
472                     msg: this.app.i18n._('The Employee has no more possible vacation days left for this year. Create a new vacation and use another personal account the vacation should be taken from.'),
473                     icon: Ext.MessageBox.WARNING,
474                     buttons: Ext.Msg.OK
475                 });
476                 return Tine.HumanResources.DatePicker.superclass.handleDateClick.call(this, e, t);
477             }
478         } else {
479             var remaining = 0;
480         }
481
482         if (existing) {
483             this.store.remove(existing);
484         } else {
485             this.store.addSorted(new this.recordClass({date: date, duration: 1}));
486         }
487         
488         if (this.accountPickerActive) {
489             if (this.store.getCount() > 0) {
490                 this.editDialog.accountPicker.disable();
491             } else {
492                 this.editDialog.accountPicker.enable();
493             }
494
495             this.editDialog.getForm().findField('remaining_vacation_days').setValue(remaining);
496         }
497         
498         Tine.HumanResources.DatePicker.superclass.handleDateClick.call(this, e, t);
499     },
500     
501     /**
502      * updates the cell classes
503      */
504     updateCellClasses: function() {
505         
506         this.cells.each(function(c) {
507             
508             var timestamp = c.dom.firstChild.dateValue;
509             
510             if (this.store.getByDate(timestamp)) {
511                 c.addClass('x-date-selected');
512             } else {
513                 c.removeClass('x-date-selected');
514             }
515             
516             if (this.vacationDates.indexOf(timestamp) > -1) {
517                 c.addClass('hr-date-vacation');
518             }
519             
520             if (this.sicknessDates.indexOf(timestamp) > -1) {
521                 c.addClass('hr-date-sickness');
522             }
523             
524             if (this.feastDates.indexOf(timestamp) > -1) {
525                 c.addClass('hr-date-feast');
526             }
527            
528         }, this);
529     },
530     
531     /**
532      * returns data for the editDialog
533      * 
534      * @return {Array}
535      */
536     getData: function() {
537         var ret = [];
538         this.store.sort({field: 'date', direction: 'ASC'});
539         this.store.query().each(function(record) {
540             ret.push(record.data);
541         }, this);
542         
543         return ret;
544     }
545 });