Merge branch '2015.11-develop' into 2016.11
[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                 // TODO check cases when fd.date has no split()
253                 if (Ext.isFunction(fd.date.split)) {
254                     var split = fd.date.split(' '), dateSplit = split[0].split('-');
255                     fd.date = new Date(dateSplit[0], dateSplit[1] - 1, dateSplit[2]);
256                     fd.date.clearTime();
257                     this.store.add(new this.recordClass(fd));
258                 }
259             }, this);
260         }
261
262         if (this.accountPickerActive && onInit) {
263             // set remaining vacation days in edit dialog
264             // TODO this should be refactored and moved to edit dialog as this is an unexpected place here
265             var substractDays = this.editDialog.getDaysToSubstract();
266             this.editDialog.getForm().findField('remaining_vacation_days').setValue(rr.allVacation - substractDays);
267         }
268         
269         this.updateCellClasses();
270         this.loadMask.hide();
271
272         if (onInit) {
273             this.previousYear = this.currentYear;
274             this.currentYear = parseInt(rr.firstDay.date.split('-')[0]);
275         }
276
277         var focusDate = freetime.get('firstday_date');
278         if (date) {
279             focusDate = date;
280         } else if (this.disableYearChange) {
281             if (this.previousYear < this.currentYear) {
282                 focusDate = new Date(this.currentYear + '/01/01 12:00:00 AM');
283             } else {
284                 focusDate = new Date(this.currentYear + '/12/31 12:00:00 AM');
285             }
286         }
287
288         // disableYearChange here to make sure we don't load feast and free days again during update() or enable()
289         // TODO this needs to be refactored!!
290         this.disableYearChange = true;
291
292         // focus
293         if (focusDate) {
294             Tine.log.debug('focusDate ' + focusDate + ' currentYear ' + this.currentYear);
295             this.update(focusDate);
296         }
297
298         this.enable();
299         
300         this.disableYearChange = false;
301     },
302     
303     /**
304      * if loading feast and freedays fails
305      */
306     onFeastDaysLoadFailureCallback: function() {
307         
308         Tine.log.debug('Calling onFeastDaysLoadFailureCallback with current year ' + this.currentYear + ' and previous year ' + this.previousYear);
309         
310         var year = this.currentYear;
311         this.currentYear = this.previousYear;
312         this.previousYear = year;
313         this.onYearChange();
314     },
315     
316     /**
317      * set vacation dates
318      * 
319      * @param {Object} localVacationDays
320      * @param {Array} remoteVacationDays
321      * @param {Object} locallyRemovedDays
322      */
323     setVacationDates: function(localVacationDays, remoteVacationDays, locallyRemovedDays) {
324         this.vacationDates = this.getTimestampsFromDays(localVacationDays, remoteVacationDays, locallyRemovedDays);
325     },
326     
327     /**
328      * set sickness dates
329      * 
330      * @param {Object} localSicknessDays
331      * @param {Array} remoteSicknessDays
332      * @param {Object} locallyRemovedDays
333      */
334     setSicknessDates: function(localSicknessDays, remoteSicknessDays, locallyRemovedDays) {
335         this.sicknessDates = this.getTimestampsFromDays(localSicknessDays, remoteSicknessDays, locallyRemovedDays);
336     },
337     
338     /**
339      * set feast dates
340      */
341     setFeastDates: function(feastDays) {
342         this.feastDates = this.getTimestampsFromDays([], feastDays);
343     },
344     
345     /**
346      * returns a timestamp from a day
347      * 
348      * @param {Object} localDays
349      * @param {Array} remoteDays
350      * @param {Object} locallyRemovedDays
351      * 
352      * @return {Array}
353      */
354     getTimestampsFromDays: function(localDays, remoteDays, locallyRemovedDays) {
355         
356         var dates = [];
357         Ext.iterate(localDays, function(accountId, localdates) { 
358             for (var index = 0; index < localdates.length; index++) {
359                 var newdate = new Date(localdates[index].date.replace(/-/g,'/') + ' AM');
360                 newdate.setHours(0);
361                 dates.push((newdate.getTime()));
362             }
363         });
364         
365         // find out removed dates
366         var remove = [];
367         if (locallyRemovedDays) {
368             Ext.iterate(locallyRemovedDays, function(accountId, removeDays) {
369                 for (var index = 0; index < removeDays.length; index++) {
370                     remove.push(removeDays[index].date.split(' ')[0]);
371                 }
372             }, this);
373         }
374         
375         // do not mark day as taken, if it is deleted already in the grid
376         if (remoteDays) {
377             for (var index = 0; index < remoteDays.length; index++) {
378                 var day = remoteDays[index].date.split(' ')[0];
379                 if (remove.indexOf(day) == -1) {
380                     var newdate = new Date(remoteDays[index].date.replace(/-/g,'/') + ' AM');
381                     dates.push(newdate.getTime());
382                 }
383             }
384         }
385         
386         return dates;
387     },
388     
389     /**
390      * is called on year change
391      *
392      * @param {Date} date
393      */
394     onYearChange: function(date) {
395         Tine.log.debug('Calling onYearChange with the date ' + date);
396         // this is called on changing the year in the picker
397         this.loadFeastDays(true, false, date, date.format('Y'));
398     },
399     
400     /**
401      * overwrites update function of superclass
402      * 
403      * @param {Date} date
404      * @param {Boolean} forceRefresh
405      */
406     update : function(date, forceRefresh) {
407         Tine.HumanResources.DatePicker.superclass.update.call(this, date, forceRefresh);
408         
409         if (! this.disableYearChange && ! this.initializing) {
410             var year = parseInt(date.format('Y'));
411             if (year !== this.currentYear) {
412                 if (this.getData().length > 0) {
413                     Ext.MessageBox.show({
414                         title: this.app.i18n._('Year can not be changed'),
415                         msg: this.app.i18n._('You have already selected some dates from another year. Please create a new record to add dates from another year!'),
416                         buttons: Ext.Msg.OK,
417                         icon: Ext.MessageBox.WARNING,
418                         // jump to the first day of the selected
419                         fn: function () {
420                             var firstDay = this.store.getFirstDay();
421                             this.update(firstDay.get('date'));
422                         },
423                         scope: this
424                     });
425                 } else {
426                     this.previousYear = this.currentYear;
427                     this.currentYear = parseInt(date.format('Y'));
428                     this.onYearChange(date);
429                 }
430             }
431         }
432         
433         this.updateCellClasses();
434     },
435
436     /**
437      * removes or adds a date on date click
438      * 
439      * @param {Object} e
440      * @param {Object} t
441      */
442     handleDateClick: function(e, t) {
443         // don't handle date click, if this is disabled, or the clicked node doesn't have a timestamp assigned
444         if (this.disabled || ! t.dateValue) {
445             return Tine.HumanResources.DatePicker.superclass.handleDateClick.call(this, e, t);
446         }
447         // don't handle click on disabled dates defined by contract or feast calendar
448         if (Ext.fly(t.parentNode).hasClass('x-date-disabled')) {
449             return Tine.HumanResources.DatePicker.superclass.handleDateClick.call(this, e, t);
450         }
451         
452         // dont't handle click on already defined sickness days
453         if (Ext.fly(t.parentNode).hasClass('hr-date-sickness')) {
454             return Tine.HumanResources.DatePicker.superclass.handleDateClick.call(this, e, t);
455         }
456         
457         // dont't handle click on feast days
458         if (Ext.fly(t.parentNode).hasClass('hr-date-feast')) {
459             return;
460         }
461         
462         // dont't handle click on already defined vacation days from different vacation entries
463         if (Ext.fly(t.parentNode).hasClass('hr-date-vacation') &&
464            !Ext.fly(t.parentNode).hasClass('x-date-selected')) {
465             return;
466         }
467         
468         var date = new Date(t.dateValue),
469             existing = this.store.getByDate(date);
470             
471         date.clearTime();
472         
473         if (this.accountPickerActive) {
474
475             var remaining = this.editDialog.getForm().findField('remaining_vacation_days').getValue();
476
477             if (existing) {
478                 remaining++;
479             } else {
480                 remaining--;
481             }
482
483             if (remaining < 0) {
484                 Ext.MessageBox.show({
485                     title: this.app.i18n._('No more vacation days'),
486                     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.'),
487                     icon: Ext.MessageBox.WARNING,
488                     buttons: Ext.Msg.OK
489                 });
490                 return Tine.HumanResources.DatePicker.superclass.handleDateClick.call(this, e, t);
491             }
492         } else {
493             var remaining = 0;
494         }
495
496         if (existing) {
497             this.store.remove(existing);
498             var index = this.vacationDates.indexOf(t.dateValue);
499             if (index > -1) {
500                 this.vacationDates.splice(index, 1);
501             }
502         } else {
503             this.store.addSorted(new this.recordClass({date: date, duration: 1}));
504         }
505         
506         if (this.accountPickerActive) {
507             if (this.store.getCount() > 0) {
508                 this.editDialog.accountPicker.disable();
509             } else {
510                 this.editDialog.accountPicker.enable();
511             }
512
513             this.editDialog.getForm().findField('remaining_vacation_days').setValue(remaining);
514         }
515         
516         Tine.HumanResources.DatePicker.superclass.handleDateClick.call(this, e, t);
517     },
518     
519     /**
520      * updates the cell classes
521      */
522     updateCellClasses: function() {
523         
524         this.cells.each(function(c) {
525             
526             var timestamp = c.dom.firstChild.dateValue;
527             
528             if (this.store.getByDate(timestamp)) {
529                 c.addClass('x-date-selected');
530             } else {
531                 c.removeClass('x-date-selected');
532                 if (c.hasClass('hr-date-vacation')) {
533                     c.removeClass('hr-date-vacation');
534                 }
535             }
536             
537             if (this.vacationDates.indexOf(timestamp) > -1) {
538                 c.addClass('hr-date-vacation');
539             }
540             
541             if (this.sicknessDates.indexOf(timestamp) > -1) {
542                 c.addClass('hr-date-sickness');
543             }
544             
545             if (this.feastDates.indexOf(timestamp) > -1) {
546                 c.addClass('hr-date-feast');
547             }
548            
549         }, this);
550     },
551     
552     /**
553      * returns data for the editDialog
554      * 
555      * @return {Array}
556      */
557     getData: function() {
558         var ret = [];
559         this.store.sort({field: 'date', direction: 'ASC'});
560         this.store.query().each(function(record) {
561             ret.push(record.data);
562         }, this);
563         
564         return ret;
565     }
566 });