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