Merge branch '2014.11-develop' into 2015.07
[tine20] / tine20 / Calendar / js / EventEditDialog.js
1 /*
2  * Tine 2.0
3  * 
4  * @package     Calendar
5  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
6  * @author      Cornelius Weiss <c.weiss@metaways.de>
7  * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
8  *
9  */
10  
11 Ext.ns('Tine.Calendar');
12
13 /**
14  * @namespace Tine.Calendar
15  * @class Tine.Calendar.EventEditDialog
16  * @extends Tine.widgets.dialog.EditDialog
17  * Calendar Edit Dialog <br>
18  * 
19  * @author      Cornelius Weiss <c.weiss@metaways.de>
20  */
21 Tine.Calendar.EventEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
22     /**
23      * @cfg {Number} containerId initial container id
24      */
25     containerId: -1,
26     
27     labelAlign: 'side',
28     windowNamePrefix: 'EventEditWindow_',
29     appName: 'Calendar',
30     recordClass: Tine.Calendar.Model.Event,
31     recordProxy: Tine.Calendar.backend,
32     showContainerSelector: false,
33     tbarItems: [],
34     displayNotes: true,
35
36     mode: 'local',
37     
38     // note: we need up use new action updater here or generally in the widget!
39     evalGrants: false,
40     
41     onResize: function() {
42         Tine.Calendar.EventEditDialog.superclass.onResize.apply(this, arguments);
43         this.setTabHeight.defer(100, this);
44     },
45     
46     /**
47      * returns dialog
48      * 
49      * NOTE: when this method gets called, all initalisation is done.
50      * @return {Object} components this.itmes definition
51      */
52     getFormItems: function() {
53         var timeIncrement = parseInt(this.app.getRegistry().get('preferences').get('timeIncrement'));
54
55         return {
56             xtype: 'tabpanel',
57             border: false,
58             plugins: [{
59                 ptype : 'ux.tabpanelkeyplugin'
60             }],
61             defaults: {
62                 hideMode: 'offsets'
63             },
64             plain:true,
65             activeTab: 0,
66             border: false,
67             items:[{
68                 title: this.app.i18n.n_('Event', 'Events', 1),
69                 border: false,
70                 frame: true,
71                 layout: 'border',
72                 items: [{
73                     region: 'center',
74                     layout: 'hfit',
75                     border: false,
76                     items: [{
77                         layout: 'hbox',
78                         items: [{
79                             margins: '5',
80                             width: 100,
81                             xtype: 'label',
82                             text: this.app.i18n._('Summary')
83                         }, {
84                             flex: 1,
85                             xtype:'textfield',
86                             name: 'summary',
87                             listeners: {render: function(field){field.focus(false, 250);}},
88                             allowBlank: false,
89                             requiredGrant: 'editGrant',
90                             maxLength: 255
91                         }]
92                     }, {
93                         layout: 'hbox',
94                         items: [{
95                             margins: '5',
96                             width: 100,
97                             xtype: 'label',
98                             text: this.app.i18n._('View')
99                         }, Ext.apply(this.perspectiveCombo, {
100                             flex: 1
101                         })]
102                     }, {
103                         layout: 'hbox',
104                         height: 115,
105                         layoutConfig: {
106                             align : 'stretch',
107                             pack  : 'start'
108                         },
109                         items: [{
110                             flex: 1,
111                             xtype: 'fieldset',
112                             layout: 'hfit',
113                             margins: '0 5 0 0',
114                             title: this.app.i18n._('Details'),
115                             items: [{
116                                 xtype: 'columnform',
117                                 labelAlign: 'side',
118                                 labelWidth: 100,
119                                 formDefaults: {
120                                     xtype:'textfield',
121                                     anchor: '100%',
122                                     labelSeparator: '',
123                                     columnWidth: .7
124                                 },
125                                 items: [[{
126                                     columnWidth: 1,
127                                     fieldLabel: this.app.i18n._('Location'),
128                                     name: 'location',
129                                     requiredGrant: 'editGrant',
130                                     maxLength: 255
131                                 }], [{
132                                     xtype: 'datetimefield',
133                                     fieldLabel: this.app.i18n._('Start Time'),
134                                     listeners: {scope: this, change: this.onDtStartChange},
135                                     name: 'dtstart',
136                                     increment: timeIncrement,
137                                     requiredGrant: 'editGrant'
138                                 }, {
139                                     columnWidth: .19,
140                                     xtype: 'checkbox',
141                                     hideLabel: true,
142                                     boxLabel: this.app.i18n._('whole day'),
143                                     listeners: {scope: this, check: this.onAllDayChange},
144                                     name: 'is_all_day_event',
145                                     requiredGrant: 'editGrant'
146                                 }], [{
147                                     xtype: 'datetimefield',
148                                     fieldLabel: this.app.i18n._('End Time'),
149                                     listeners: {scope: this, change: this.onDtEndChange},
150                                     name: 'dtend',
151                                     increment: timeIncrement,
152                                     requiredGrant: 'editGrant'
153                                 }, {
154                                     columnWidth: .3,
155                                     xtype: 'combo',
156                                     hideLabel: true,
157                                     readOnly: true,
158                                     hideTrigger: true,
159                                     disabled: true,
160                                     name: 'originator_tz',
161                                     requiredGrant: 'editGrant'
162                                 }], [ this.containerSelectCombo = new Tine.widgets.container.selectionComboBox({
163                                     columnWidth: 1,
164                                     id: this.app.appName + 'EditDialogContainerSelector' + Ext.id(),
165                                     fieldLabel: _('Saved in'),
166                                     ref: '../../../../../../../../containerSelect',
167                                     //width: 300,
168                                     //listWidth: 300,
169                                     name: this.recordClass.getMeta('containerProperty'),
170                                     recordClass: this.recordClass,
171                                     containerName: this.app.i18n.n_hidden(this.recordClass.getMeta('containerName'), this.recordClass.getMeta('containersName'), 1),
172                                     containersName: this.app.i18n._hidden(this.recordClass.getMeta('containersName')),
173                                     appName: this.app.appName,
174                                     requiredGrant: 'readGrant',
175                                     disabled: true
176                                 }), Ext.apply(this.perspectiveCombo.getAttendeeContainerField(), {
177                                     columnWidth: 1
178                                 })]]
179                             }]
180                         }, {
181                             width: 130,
182                             xtype: 'fieldset',
183                             title: this.app.i18n._('Status'),
184                             items: [{
185                                 xtype: 'checkbox',
186                                 hideLabel: true,
187                                 boxLabel: this.app.i18n._('non-blocking'),
188                                 name: 'transp',
189                                 requiredGrant: 'editGrant',
190                                 getValue: function() {
191                                     var bool = Ext.form.Checkbox.prototype.getValue.call(this);
192                                     return bool ? 'TRANSPARENT' : 'OPAQUE';
193                                 },
194                                 setValue: function(value) {
195                                     var bool = (value == 'TRANSPARENT' || value === true);
196                                     return Ext.form.Checkbox.prototype.setValue.call(this, bool);
197                                 }
198                             }, Ext.apply(this.perspectiveCombo.getAttendeeTranspField(), {
199                                 hideLabel: true
200                             }), {
201                                 xtype: 'checkbox',
202                                 hideLabel: true,
203                                 boxLabel: this.app.i18n._('Tentative'),
204                                 name: 'status',
205                                 requiredGrant: 'editGrant',
206                                 getValue: function() {
207                                     var bool = Ext.form.Checkbox.prototype.getValue.call(this);
208                                     return bool ? 'TENTATIVE' : 'CONFIRMED';
209                                 },
210                                 setValue: function(value) {
211                                     var bool = (value == 'TENTATIVE' || value === true);
212                                     return Ext.form.Checkbox.prototype.setValue.call(this, bool);
213                                 }
214                             }, {
215                                 xtype: 'checkbox',
216                                 hideLabel: true,
217                                 boxLabel: this.app.i18n._('Private'),
218                                 name: 'class',
219                                 requiredGrant: 'editGrant',
220                                 getValue: function() {
221                                     var bool = Ext.form.Checkbox.prototype.getValue.call(this);
222                                     return bool ? 'PRIVATE' : 'PUBLIC';
223                                 },
224                                 setValue: function(value) {
225                                     var bool = (value == 'PRIVATE' || value === true);
226                                     return Ext.form.Checkbox.prototype.setValue.call(this, bool);
227                                 }
228                             }, Ext.apply(this.perspectiveCombo.getAttendeeStatusField(), {
229                                 width: 115,
230                                 hideLabel: true
231                             })]
232                         }]
233                     }, {
234                         xtype: 'tabpanel',
235                         deferredRender: false,
236                         activeTab: 0,
237                         border: false,
238                         height: 235,
239                         form: true,
240                         items: [
241                             this.attendeeGridPanel,
242                             this.rrulePanel,
243                             this.alarmPanel
244                         ]
245                     }]
246                 }, {
247                     // activities and tags
248                     region: 'east',
249                     layout: 'accordion',
250                     animate: true,
251                     width: 200,
252                     split: true,
253                     collapsible: true,
254                     collapseMode: 'mini',
255                     header: false,
256                     margins: '0 5 0 5',
257                     border: true,
258                     items: [
259                         new Tine.widgets.tags.TagPanel({
260                             app: 'Calendar',
261                             border: false,
262                             bodyStyle: 'border:1px solid #B5B8C8;'
263                         }),
264                         new Ext.Panel({
265                             // @todo generalise!
266                             title: this.app.i18n._('Description'),
267                             iconCls: 'descriptionIcon',
268                             layout: 'form',
269                             labelAlign: 'top',
270                             border: false,
271                             items: [{
272                                 style: 'margin-top: -4px; border 0px;',
273                                 labelSeparator: '',
274                                 xtype:'textarea',
275                                 name: 'description',
276                                 hideLabel: true,
277                                 grow: false,
278                                 preventScrollbars:false,
279                                 anchor:'100% 100%',
280                                 emptyText: this.app.i18n._('Enter description'),
281                                 requiredGrant: 'editGrant'
282                             }]
283                         })
284                     ]
285                 }]
286             }, new Tine.widgets.activities.ActivitiesTabPanel({
287                 app: this.appName,
288                 record_id: (this.record) ? this.record.id : '',
289                 record_model: this.appName + '_Model_' + this.recordClass.getMeta('modelName'),
290                 getRecordId: (function() {
291                     return this.record.isRecurInstance() ? this.record.get('base_event_id') : this.record.get('id');
292                 }).createDelegate(this)
293             })]
294         };
295     },
296
297     /**
298      * mute first alert
299      * 
300      * @param {} button
301      * @param {} e
302      */
303     onMuteNotificationOnce: function (button, e) {
304         this.record.set('mute', button.pressed);
305     },
306
307     initComponent: function() {
308         this.addEvents(
309             /**
310              * @event dtStartChange
311              * @desc  Fired when dtstart chages in UI
312              * @param {Json String} oldValue, newValue
313              */
314             'dtStartChange'
315         );
316
317         this.tbarItems = new Ext.Button(new Ext.Action({
318             text: Tine.Tinebase.appMgr.get('Calendar').i18n._('Mute Notification'),
319             handler: this.onMuteNotificationOnce,
320             iconCls: 'notes_noteIcon',
321             disabled: false,
322             scope: this,
323             enableToggle: true
324         }));
325
326         var organizerCombo;
327         this.attendeeGridPanel = new Tine.Calendar.AttendeeGridPanel({
328             bbar: [{
329                 xtype: 'label',
330                 html: Tine.Tinebase.appMgr.get('Calendar').i18n._('Organizer') + "&nbsp;"
331             }, organizerCombo = Tine.widgets.form.RecordPickerManager.get('Addressbook', 'Contact', {
332                 width: 300,
333                 name: 'organizer',
334                 userOnly: true,
335                 getValue: function() {
336                     var id = Tine.Addressbook.SearchCombo.prototype.getValue.apply(this, arguments),
337                         record = this.store.getById(id);
338                         
339                     return record ? record.data : id;
340                 }
341             })]
342         });
343         
344         // auto location
345         this.attendeeGridPanel.on('afteredit', function(o) {
346             if (o.field == 'user_id'
347                 && o.record.get('user_type') == 'resource'
348                 && o.record.get('user_id')
349                 && o.record.get('user_id').is_location
350             ) {
351                 this.getForm().findField('location').setValue(
352                     this.attendeeGridPanel.renderAttenderResourceName(o.record.get('user_id'))
353                 );
354             }
355         }, this);
356         
357         this.on('render', function() {this.getForm().add(organizerCombo);}, this);
358         
359         this.rrulePanel = new Tine.Calendar.RrulePanel({
360             eventEditDialog : this
361         });
362         this.alarmPanel = new Tine.widgets.dialog.AlarmPanel({});
363         this.attendeeStore = this.attendeeGridPanel.getStore();
364         
365         // a combo with all attendee + origin/organizer
366         this.perspectiveCombo = new Tine.Calendar.PerspectiveCombo({
367             editDialog: this
368         });
369         
370         Tine.Calendar.EventEditDialog.superclass.initComponent.call(this);
371         
372         this.addAttendee();
373     },
374
375     /**
376      * if this addRelations is set, iterate and create attendee
377      */
378     addAttendee: function() {
379         var attendee = this.record.get('attendee');
380         var attendee = Ext.isArray(attendee) ? attendee : [];
381         
382         if (Ext.isArray(this.plugins)) {
383             for (var index = 0; index < this.plugins.length; index++) {
384                 if (this.plugins[index].hasOwnProperty('addRelations')) {
385
386                     var config = this.plugins[index].hasOwnProperty('relationConfig') ? this.plugins[index].relationConfig : {};
387                     
388                     for (var index2 = 0; index2 < this.plugins[index].addRelations.length; index2++) {
389                         var item = this.plugins[index].addRelations[index2];
390                         var attender = Ext.apply({
391                             user_type: 'user',
392                             role: 'REQ',
393                             quantity: 1,
394                             status: 'NEEDS-ACTION',
395                             user_id: item
396                         }, config);
397                         
398                         attendee.push(attender);
399                     }
400                 }
401             }
402         }
403         
404         this.record.set('attendee', attendee);
405     },
406     
407     /**
408      * checks if form data is valid
409      * 
410      * @return {Boolean}
411      */
412     isValid: function() {
413         var isValid = this.validateDtStart() && this.validateDtEnd();
414         
415         if (! this.rrulePanel.isValid()) {
416             isValid = false;
417             
418             this.rrulePanel.ownerCt.setActiveTab(this.rrulePanel);
419         }
420         
421         return isValid && Tine.Calendar.EventEditDialog.superclass.isValid.apply(this, arguments);
422     },
423      
424     onAllDayChange: function(checkbox, isChecked) {
425         var dtStartField = this.getForm().findField('dtstart');
426         var dtEndField = this.getForm().findField('dtend');
427         dtStartField.setDisabled(isChecked, 'time');
428         dtEndField.setDisabled(isChecked, 'time');
429         
430         if (isChecked) {
431             dtStartField.clearTime();
432             var dtend = dtEndField.getValue();
433             if (Ext.isDate(dtend) && dtend.format('H:i:s') != '23:59:59') {
434                 dtEndField.setValue(dtend.clearTime(true).add(Date.HOUR, 24).add(Date.SECOND, -1));
435             }
436             
437         } else {
438             dtStartField.undo();
439             dtEndField.undo();
440         }
441     },
442     
443     onDtEndChange: function(dtEndField, newValue, oldValue) {
444         this.validateDtEnd();
445     },
446     
447     /**
448      * on dt start change
449      * 
450      * @param {} dtStartField
451      * @param {} newValue
452      * @param {} oldValue
453      */
454     onDtStartChange: function(dtStartField, newValue, oldValue) {
455         if (this.validateDtStart() == false) {
456             return false;
457         }
458         
459         if (Ext.isDate(newValue) && Ext.isDate(oldValue)) {
460             var dtEndField = this.getForm().findField('dtend'),
461                 dtEnd = dtEndField.getValue();
462                 
463             if (Ext.isDate(dtEnd)) {
464                 var duration = dtEnd.getTime() - oldValue.getTime(),
465                     newDtEnd = newValue.add(Date.MILLI, duration);
466                 dtEndField.setValue(newDtEnd);
467                 this.validateDtEnd();
468             }
469         }
470
471         this.fireEvent('dtStartChange', Ext.util.JSON.encode({newValue: newValue, oldValue: oldValue}));
472     },
473     
474     /**
475      * copy record
476      * 
477      * TODO change attender status?
478      */
479     doCopyRecord: function() {
480         Tine.Calendar.EventEditDialog.superclass.doCopyRecord.call(this);
481         
482         // remove attender ids
483         Ext.each(this.record.data.attendee, function(attender) {
484             delete attender.id;
485         }, this);
486         
487         // Calendar is the only app with record based grants -> user gets edit grant for all fields when copying
488         this.record.set('editGrant', true);
489
490         Tine.log.debug('Tine.Calendar.EventEditDialog::doCopyRecord() -> record:');
491         Tine.log.debug(this.record);
492     },
493     
494     /**
495      * is called after all subpanels have been loaded
496      */
497     onAfterRecordLoad: function() {
498         Tine.Calendar.EventEditDialog.superclass.onAfterRecordLoad.call(this);
499
500         // disable relations panel for non persistent exceptions till we have the baseEventId
501         if (this.record.isRecurInstance()) {
502             this.relationsPanel.setDisabled(true);
503         }
504         this.attendeeGridPanel.onRecordLoad(this.record);
505         this.rrulePanel.onRecordLoad(this.record);
506         this.alarmPanel.onRecordLoad(this.record);
507         
508         // apply grants
509         if (! this.record.get('editGrant')) {
510             this.getForm().items.each(function(f){
511                 if(f.isFormField && f.requiredGrant !== undefined){
512                     f.setDisabled(! this.record.get(f.requiredGrant));
513                 }
514             }, this);
515         }
516         
517         this.perspectiveCombo.loadPerspective();
518         // disable container selection combo if user has no right to edit
519         this.containerSelect.setDisabled.defer(20, this.containerSelect, [(! this.record.get('editGrant'))]);
520         
521         // disable time selectors if this is a whole day event
522         if (this.record.get('is_all_day_event')) {
523             this.onAllDayChange(null, true);
524         }
525     },
526     
527     onRecordUpdate: function() {
528         Tine.Calendar.EventEditDialog.superclass.onRecordUpdate.apply(this, arguments);
529         this.attendeeGridPanel.onRecordUpdate(this.record);
530         this.rrulePanel.onRecordUpdate(this.record);
531         this.alarmPanel.onRecordUpdate(this.record);
532         this.perspectiveCombo.updatePerspective();
533     },
534
535     setTabHeight: function() {
536         var eventTab = this.items.first().items.first();
537         var centerPanel = eventTab.items.first();
538         var tabPanel = centerPanel.items.last();
539         tabPanel.setHeight(centerPanel.getEl().getBottom() - tabPanel.getEl().getTop());
540     },
541     
542     validateDtEnd: function() {
543         var dtStart = this.getForm().findField('dtstart').getValue(),
544             dtEndField = this.getForm().findField('dtend'),
545             dtEnd = dtEndField.getValue(),
546             endTime = this.adjustTimeToUserPreference(dtEndField.getValue(), 'daysviewendtime');
547         
548         if (! Ext.isDate(dtEnd)) {
549             dtEndField.markInvalid(this.app.i18n._('End date is not valid'));
550             return false;
551         } else if (Ext.isDate(dtStart) && dtEnd.getTime() - dtStart.getTime() <= 0) {
552             dtEndField.markInvalid(this.app.i18n._('End date must be after start date'));
553             return false;
554         } else if (! Tine.Tinebase.configManager.get('daysviewallowallevents', 'Calendar')
555                 && this.getForm().findField('is_all_day_event').checked === false
556                 && !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && dtEnd > endTime)
557         {
558             dtEndField.markInvalid(this.app.i18n._('End time is not allowed to be after the configured time.'));
559             return false;
560         } else {
561             dtEndField.clearInvalid();
562             return true;
563         }
564     },
565     
566     /**
567      * adjusts given date (end/start) to user preference (hours)
568      * 
569      * @param {Date} dateValue
570      * @param {String} prefKey
571      * @return {Date}
572      */
573     adjustTimeToUserPreference: function(dateValue, prefKey) {
574         var userPreferenceDate = dateValue;
575             prefs = this.app.getRegistry().get('preferences'),
576             hour = prefs.get(prefKey).split(':')[0];
577         
578         // adjust date to user preference
579         userPreferenceDate.setHours(hour);
580         userPreferenceDate.setMinutes(0);
581         if (prefKey == 'daysviewendtime' && userPreferenceDate.format('H:i') == '00:00') {
582             userPreferenceDate = userPreferenceDate.add(Date.MINUTE, -1);
583         }
584         
585         return userPreferenceDate;
586     },
587     
588     validateDtStart: function() {
589         var dtStartField = this.getForm().findField('dtstart'),
590             dtStart = dtStartField.getValue(),
591             startTime = this.adjustTimeToUserPreference(dtStartField.getValue(), 'daysviewstarttime');
592         
593         if (! Ext.isDate(dtStart)) {
594             dtStartField.markInvalid(this.app.i18n._('Start date is not valid'));
595             return false;
596         } else if (! Tine.Tinebase.configManager.get('daysviewallowallevents', 'Calendar')
597                 && this.getForm().findField('is_all_day_event').checked === false
598                 && !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar')
599                 && dtStart < startTime)
600         {
601             dtStartField.markInvalid(this.app.i18n._('Start date is not allowed to be before the configured time.'));
602             return false;
603         } else {
604             dtStartField.clearInvalid();
605             return true;
606         }
607     },
608     
609     /**
610      * is called from onApplyChanges
611      * @param {Boolean} closeWindow
612      */
613     doApplyChanges: function(closeWindow) {
614         this.onRecordUpdate();
615         if (this.isValid()) {
616             this.fireEvent('update', Ext.util.JSON.encode(this.record.data));
617             this.onAfterApplyChanges(closeWindow);
618         } else {
619             this.saving = false;
620             this.loadMask.hide();
621             Ext.MessageBox.alert(_('Errors'), this.getValidationErrorMessage());
622         }
623     }
624 });
625
626 /**
627  * Opens a new event edit dialog window
628  * 
629  * @return {Ext.ux.Window}
630  */
631 Tine.Calendar.EventEditDialog.openWindow = function (config) {
632     // record is JSON encoded here...
633     var id = config.recordId ? config.recordId : 0;
634     var window = Tine.WindowFactory.getWindow({
635         width: 800,
636         height: 505,
637         name: Tine.Calendar.EventEditDialog.prototype.windowNamePrefix + id,
638         contentPanelConstructor: 'Tine.Calendar.EventEditDialog',
639         contentPanelConstructorConfig: config
640     });
641     return window;
642 };