Merge branch '2015.11' into 2015.11-develop
[tine20] / tine20 / Calendar / js / MainScreenCenterPanel.js
1 /* 
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
7  */
8
9 /* global Ext, Tine */
10
11 Ext.ns('Tine.Calendar');
12
13 Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
14     /**
15      * @cfg {String} activeView
16      */
17     activeView: 'weekSheet',
18     
19     startDate: new Date().clearTime(),
20     
21     /**
22      * $property Object view -> startdate
23      */
24     startDates: null,
25     
26     /**
27      * @cfg {String} loadMaskText
28      * _('Loading events, please wait...')
29      */
30     loadMaskText: 'Loading events, please wait...',
31     
32     /**
33      * @cfg {Number} autoRefreshInterval (seconds)
34      */
35     autoRefreshInterval: 300,
36     
37     /**
38      * @property autoRefreshTask
39      * @type Ext.util.DelayedTask
40      */
41     autoRefreshTask: null,
42     
43     /**
44      * add records from other applications using the split add button
45      * - activated by default
46      * 
47      * @type Bool
48      * @property splitAddButton
49      */
50     splitAddButton: true,
51     
52     /**
53      * default print mode
54      * 
55      * @type String {sheet|grid}
56      * @property defaultPrintMode
57      */
58     defaultPrintMode: 'sheet',
59     
60     periodRe: /^(day|week|month|year)/i,
61     presentationRe: /(sheet|grid)$/i,
62     
63     calendarPanels: {},
64     
65     border: false,
66     layout: 'border',
67     
68     stateful: true,
69     stateId: 'cal-mainscreen',
70     stateEvents: ['changeview'],
71     
72     getState: function () {
73         return Ext.copyTo({}, this, 'activeView');
74     },
75     
76     applyState: Ext.emptyFn,
77     
78     initComponent: function () {
79         var me = this;
80         
81         this.addEvents(
82         /**
83          * @event changeview
84          * fired if an event got clicked
85          * @param {Tine.Calendar.MainScreenCenterPanel} mspanel
86          * @param {String} view
87          */
88         'changeview');
89         
90         this.recordClass = Tine.Calendar.Model.Event;
91         
92         this.app = Tine.Tinebase.appMgr.get('Calendar');
93         
94         // init some translations
95         this.i18nRecordName = this.app.i18n.n_hidden(this.recordClass.getMeta('recordName'), this.recordClass.getMeta('recordsName'), 1);
96         this.i18nRecordsName = this.app.i18n._hidden(this.recordClass.getMeta('recordsName'));
97         this.i18nContainerName = this.app.i18n.n_hidden(this.recordClass.getMeta('containerName'), this.recordClass.getMeta('containersName'), 1);
98         this.i18nContainersName = this.app.i18n._hidden(this.recordClass.getMeta('containersName'));
99         
100         this.loadMaskText = this.app.i18n._hidden(this.loadMaskText);
101         
102         var state = Ext.state.Manager.get(this.stateId, {});
103         Ext.apply(this, state);
104         
105         this.defaultFilters = [
106             {field: 'attender', operator: 'in', value: [Ext.apply(Tine.Calendar.Model.Attender.getDefaultData(), {
107                 user_id: Tine.Tinebase.registry.get('currentAccount')
108             })]},
109             {field: 'attender_status', operator: 'notin', value: ['DECLINED']}
110         ];
111         this.filterToolbar = this.getFilterToolbar({
112             onFilterChange: this.refresh.createDelegate(this, [false]),
113             getAllFilterData: this.getAllFilterData.createDelegate(this)
114         });
115         
116         this.filterToolbar.getQuickFilterPlugin().criteriaIgnores.push(
117             {field: 'period'},
118             {field: 'grants'}
119         );
120         
121         this.startDates = [];
122         this.initActions();
123         this.initLayout();
124         
125         // init autoRefresh
126         this.autoRefreshTask = new Ext.util.DelayedTask(this.refresh, this, [{
127             refresh: true,
128             autoRefresh: true
129         }]);
130         
131         Tine.Calendar.MainScreenCenterPanel.superclass.initComponent.call(this);
132     },
133     
134     initActions: function () {
135         this.action_editInNewWindow = new Ext.Action({
136             requiredGrant: 'readGrant',
137             text: this.i18nEditActionText ? this.app.i18n._hidden(this.i18nEditActionText) : String.format(Tine.Tinebase.translation._hidden('Edit {0}'), this.i18nRecordName),
138             disabled: true,
139             handler: this.onEditInNewWindow.createDelegate(this, ["edit"]),
140             iconCls: 'action_edit'
141         });
142         
143         this.action_addInNewWindow = new Ext.Action({
144             requiredGrant: 'addGrant',
145             text: this.i18nAddActionText ? this.app.i18n._hidden(this.i18nAddActionText) : String.format(Tine.Tinebase.translation._hidden('Add {0}'), this.i18nRecordName),
146             handler: this.onEditInNewWindow.createDelegate(this, ["add"]),
147             iconCls: 'action_add'
148         });
149         
150         this.action_cut = new Ext.Action({
151             requiredGrant: 'deleteGrant',
152             text: this.app.i18n._('Cut event'),
153             handler: this.onCutEvent.createDelegate(this),
154             iconCls: 'action_cut'
155         });
156
157         this.action_copy_to = new Ext.Action({
158             requiredGrant: 'deleteGrant',
159             text: this.app.i18n._('Copy Event to clipboard'),
160             handler: this.onCopyToEvent.createDelegate(this),
161             iconCls: 'action_editcopy'
162         });
163         
164         this.action_cancelPasting = new Ext.Action({
165             requiredGrant: 'deleteGrant',
166             text: this.app.i18n._('Stop cut / copy & paste'),
167             handler: this.onCutCancelEvent.createDelegate(this),
168             iconCls: 'action_cut_break'
169         });
170         
171         // note: unprecise plural form here, but this is hard to change
172         this.action_deleteRecord = new Ext.Action({
173             requiredGrant: 'deleteGrant',
174             allowMultiple: true,
175             singularText: this.i18nDeleteActionText ? i18nDeleteActionText[0] : String.format(Tine.Tinebase.translation.n_hidden('Delete {0}', 'Delete {0}', 1), this.i18nRecordName),
176             pluralText: this.i18nDeleteActionText ? i18nDeleteActionText[1] : String.format(Tine.Tinebase.translation.n_hidden('Delete {0}', 'Delete {0}', 1), this.i18nRecordsName),
177             translationObject: this.i18nDeleteActionText ? this.app.i18n : Tine.Tinebase.translation,
178             text: this.i18nDeleteActionText ? this.i18nDeleteActionText[0] : String.format(Tine.Tinebase.translation.n_hidden('Delete {0}', 'Delete {0}', 1), this.i18nRecordName),
179             handler: this.onDeleteRecords,
180             disabled: true,
181             iconCls: 'action_delete',
182             scope: this
183         });
184         
185         this.actions_print = new Ext.Action({
186             requiredGrant: 'readGrant',
187             text: this.app.i18n._('Print Page'),
188             handler: this.onPrint.createDelegate(this, []),
189             iconCls:'action_print',
190             scope: this,
191             listeners: {
192                 arrowclick: this.onPrintMenuClick.createDelegate(this)
193             },
194             menu:{
195                 items:[{
196                     text: this.app.i18n._('Grid'),
197                     iconCls: 'cal-grid-view-type',
198                     handler: this.onPrint.createDelegate(this, ['grid'])
199                 }, {
200                     text: this.app.i18n._('Sheet'),
201                     iconCls: 'cal-week-view',
202                     handler: this.onPrint.createDelegate(this, ['sheet']),
203                     disabled: Ext.isIE || Ext.isNewIE
204                 }]
205             }
206         });
207         
208         this.showSheetView = new Ext.Button({
209             pressed: this.isActiveView('Sheet'),
210             scale: 'medium',
211             minWidth: 60,
212             rowspan: 2,
213             iconAlign: 'top',
214             requiredGrant: 'readGrant',
215             text: this.app.i18n._('Sheet'),
216             handler: this.changeView.createDelegate(this, ["sheet"]),
217             iconCls:'cal-sheet-view-type',
218             xtype: 'tbbtnlockedtoggle',
219             toggleGroup: 'Calendar_Toolbar_tgViewTypes',
220             scope: this
221         });
222         
223         this.showGridView = new Ext.Button({
224             pressed: this.isActiveView('Grid'),
225             scale: 'medium',
226             minWidth: 60,
227             rowspan: 2,
228             iconAlign: 'top',
229             requiredGrant: 'readGrant',
230             text: this.app.i18n._('Grid'),
231             handler: this.changeView.createDelegate(this, ["grid"]),
232             iconCls:'cal-grid-view-type',
233             xtype: 'tbbtnlockedtoggle',
234             toggleGroup: 'Calendar_Toolbar_tgViewTypes',
235             scope: this
236         });
237
238         this.showDayView = new Ext.Toolbar.Button({
239             pressed: this.isActiveView('Day'),
240             text: this.app.i18n._('Day'),
241             iconCls: 'cal-day-view',
242             xtype: 'tbbtnlockedtoggle',
243             handler: this.changeView.createDelegate(this, ["day"]),
244             enableToggle: true,
245             toggleGroup: 'Calendar_Toolbar_tgViews'
246         });
247         this.showWeekView = new Ext.Toolbar.Button({
248             pressed: this.isActiveView('Week'),
249             text: this.app.i18n._('Week'),
250             iconCls: 'cal-week-view',
251             xtype: 'tbbtnlockedtoggle',
252             handler: this.changeView.createDelegate(this, ["week"]),
253             enableToggle: true,
254             toggleGroup: 'Calendar_Toolbar_tgViews'
255         });
256         this.showMonthView = new Ext.Toolbar.Button({
257             pressed: this.isActiveView('Month'),
258             text: this.app.i18n._('Month'),
259             iconCls: 'cal-month-view',
260             xtype: 'tbbtnlockedtoggle',
261             handler: this.changeView.createDelegate(this, ["month"]),
262             enableToggle: true,
263             toggleGroup: 'Calendar_Toolbar_tgViews'
264         });
265         this.showYearView = new Ext.Toolbar.Button({
266             pressed: String(this.activeView).match(/^year/i),
267             text: this.app.i18n._('Year'),
268             iconCls: 'cal-year-view',
269             xtype: 'tbbtnlockedtoggle',
270             handler: this.changeView.createDelegate(this, ["year"]),
271             enableToggle: true,
272             toggleGroup: 'Calendar_Toolbar_tgViews'
273         });
274         this.toggleFullScreen = new Ext.Toolbar.Button({
275             text: '\u2197',
276             scope: this,
277             handler: function() {
278                 if (this.ownerCt.ref == 'tineViewportMaincardpanel') {
279                     Tine.Tinebase.viewport.tineViewportMaincardpanel.remove(this, false);
280                     Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.setActiveItem(Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.lastActiveItem);
281                     this.originalOwner.add(this);
282                     this.originalOwner.layout.setActiveItem(this);
283                     this.toggleFullScreen.setText('\u2197');
284                     this.southPanel.expand();
285                 } else {
286                     this.originalOwner = this.ownerCt;
287                     this.originalOwner.remove(this, false);
288                     Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.lastActiveItem = Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.activeItem;
289                     Tine.Tinebase.viewport.tineViewportMaincardpanel.add(this);
290                     Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.setActiveItem(this);
291                     this.toggleFullScreen.setText('\u2199');
292                     this.southPanel.collapse();
293                 }
294             }
295         });
296         
297        this.action_import = new Ext.Action({
298             requiredGrant: 'addGrant',
299             text: this.app.i18n._('Import Events'),
300             disabled: false,
301             handler: this.onImport,
302             minWidth: 60,
303             requiredGrant: 'readGrant',
304             iconCls: 'action_import',
305             scope: this,
306             allowMultiple: true
307         });
308
309         this.action_export = new Tine.Calendar.ExportButton({
310             text: this.app.i18n._('Export Events'),
311             iconCls: 'action_export',
312             exportFunction: 'Calendar.exportEvents',
313             showExportDialog: true,
314             gridPanel: this
315         });
316
317         this.changeViewActions = [
318             this.showDayView,
319             this.showWeekView,
320             this.showMonthView
321         ];
322
323         if (this.app.featureEnabled('featureYearView')) {
324             this.changeViewActions.push(this.showYearView);
325         }
326
327         this.changeViewActions.push(this.toggleFullScreen);
328
329         this.recordActions = [
330             this.action_editInNewWindow,
331             this.action_deleteRecord
332         ];
333         
334         this.actionUpdater = new  Tine.widgets.ActionUpdater({
335             actions: this.recordActions,
336             grantsProperty: false,
337             containerProperty: false
338         });
339     },
340
341     /**
342      * get current view type (Grid/Day/Week/...)
343      *
344      * @param {String} view
345      * @returns {Boolean}
346      */
347     isActiveView: function(view)
348     {
349         switch (view) {
350             case 'Grid':
351                 return String(this.activeView).match(/grid$/i);
352                 break;
353             case 'Day':
354                 return String(this.activeView).match(/day/i);
355                 break;
356             case 'Week':
357                 return String(this.activeView).match(/week/i);
358                 break;
359             case 'Month':
360                 return String(this.activeView).match(/month/i);
361                 break;
362             case 'Sheet':
363                 return String(this.activeView).match(/sheet/i);
364                 break;
365             default:
366                 return false;
367         }
368     },
369     
370     /**
371      * returns the paste action
372      * 
373      * @param {Date} datetime
374      * @param {Tine.Calendar.Model.Event} event
375      */
376     getPasteAction: function(datetime, event) {
377         var shortSummary = Ext.util.Format.ellipsis(event.get('summary'), 15);
378         return new Ext.Action({
379             requiredGrant: 'addGrant',
380             text: String.format(this.app.i18n._('Paste event "{0}"'), shortSummary),
381             handler: this.onPasteEvent.createDelegate(this, [datetime]),
382             iconCls: 'action_paste'
383         });
384     },
385         
386     getActionToolbar: Tine.widgets.grid.GridPanel.prototype.getActionToolbar,
387     
388     getActionToolbarItems: function() {
389         return [{
390             xtype: 'buttongroup',
391             columns: 1,
392             rows: 2,
393             frame: false,
394             items: [
395                 this.action_import,
396                 this.action_export
397             ]
398         }, {
399             xtype: 'buttongroup',
400             plugins: [{
401                 ptype: 'ux.itemregistry',
402                 key:   'Calendar-MainScreenPanel-ViewBtnGrp'
403             }],
404             items: [
405                 this.showSheetView,
406                 this.showGridView
407             ]
408         }];
409     },
410     
411     /**
412      * @private
413      * 
414      * NOTE: Order of items matters! Ext.Layout.Border.SplitRegion.layout() does not
415      *       fence the rendering correctly, as such it's impotant, so have the ftb
416      *       defined after all other layout items
417      */
418     initLayout: function () {
419         this.items = [{
420             region: 'center',
421             layout: 'card',
422             activeItem: 0,
423             border: false,
424             items: [this.getCalendarPanel(this.activeView)]
425         }];
426         
427         // add detail panel
428         if (this.detailsPanel) {
429             this.items.push({
430                 region: 'south',
431                 ref: 'southPanel',
432                 border: false,
433                 collapsible: true,
434                 collapseMode: 'mini',
435                 header: false,
436                 split: true,
437                 layout: 'fit',
438                 height: this.detailsPanel.defaultHeight ? this.detailsPanel.defaultHeight : 125,
439                 items: this.detailsPanel
440                 
441             });
442             //this.detailsPanel.doBind(this.activeView);
443         }
444         
445         // add filter toolbar
446         if (this.filterToolbar) {
447             this.items.push({
448                 region: 'north',
449                 border: false,
450                 items: this.filterToolbar,
451                 listeners: {
452                     scope: this,
453                     afterlayout: function (ct) {
454                         ct.suspendEvents();
455                         ct.setHeight(this.filterToolbar.getHeight());
456                         ct.ownerCt.layout.layout();
457                         ct.resumeEvents();
458                     }
459                 }
460             });
461         }
462     },
463     
464     /**
465      * import events
466      * 
467      * @param {Button} btn 
468      */
469     onImport: function(btn) {
470         var popupWindow = Tine.Calendar.ImportDialog.openWindow({
471             appName: 'Calendar',
472             modelName: 'Event',
473             defaultImportContainer: this.app.getMainScreen().getWestPanel().getContainerTreePanel().getDefaultContainer('defaultContainer'),
474             
475             ignoreConflicts: true,
476             doTryRun: false,
477             
478             
479             // update grid after import
480             listeners: {
481                 scope: this,
482                 'finish': function() {
483                     this.refresh();
484                 }
485             }
486         });
487     },
488     
489     /**
490      * @private
491      */
492     onRender: function(ct, position) {
493         Tine.Calendar.MainScreenCenterPanel.superclass.onRender.apply(this, arguments);
494         
495         var defaultFavorite = Tine.widgets.persistentfilter.model.PersistentFilter.getDefaultFavorite(this.app.appName, this.recordClass.prototype.modelName),
496             favoritesPanel  = this.app.getMainScreen().getWestPanel().getFavoritesPanel();
497         
498         this.loadMask = new Ext.LoadMask(this.body, {msg: this.loadMaskText});
499         
500         if (defaultFavorite) {
501             favoritesPanel.selectFilter(defaultFavorite);
502         } else {
503             this.refresh();
504         }
505     },
506     
507     getViewParts: function (view) {
508         view = String(view);
509         
510         var activeView = String(this.activeView),
511             periodMatch = view.match(this.periodRe),
512             period = Ext.isArray(periodMatch) ? periodMatch[0] : null,
513             activePeriodMatch = activeView.match(this.periodRe),
514             activePeriod = Ext.isArray(activePeriodMatch) ? activePeriodMatch[0] : 'week',
515             presentationMatch = view.match(this.presentationRe),
516             presentation = Ext.isArray(presentationMatch) ? presentationMatch[0] : null,
517             activePresentationMatch = activeView.match(this.presentationRe),
518             activePresentation = Ext.isArray(activePresentationMatch) ? activePresentationMatch[0] : 'sheet';
519             
520         return {
521             period: period ? period : activePeriod,
522             presentation: presentation ? presentation : activePresentation,
523             toString: function() {return this.period + Ext.util.Format.capitalize(this.presentation);}
524         };
525     },
526     
527     changeView: function (view, startDate) {
528         // autocomplete view
529         var viewParts = this.getViewParts(view);
530         view = viewParts.toString();
531         
532         Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::changeView(' + view + ',' + startDate + ')');
533         
534         // save current startDate
535         this.startDates[this.activeView] = this.startDate.clone();
536         
537         if (startDate && Ext.isDate(startDate)) {
538             this.startDate = startDate.clone();
539         } else {
540             // see if a recent startDate of that view fits
541             var lastStartDate = this.startDates[view],
542                 currentPeriod = this.getCalendarPanel(this.activeView).getView().getPeriod();
543                 
544             if (Ext.isDate(lastStartDate) && lastStartDate.between(currentPeriod.from, currentPeriod.until)) {
545                 this.startDate = this.startDates[view].clone();
546             }
547         }
548         
549         var panel = this.getCalendarPanel(view);
550         var cardPanel = this.items.first();
551         
552         if (panel.rendered) {
553             cardPanel.layout.setActiveItem(panel.id);
554         } else {
555             cardPanel.add(panel);
556             cardPanel.layout.setActiveItem(panel.id);
557             cardPanel.doLayout();
558         }
559         
560         this.activeView = view;
561         
562         // move around changeViewButtons
563         var rightRow = Ext.get(Ext.DomQuery.selectNode('tr[class=x-toolbar-right-row]', panel.tbar.dom));
564         
565         for (var i = this.changeViewActions.length - 1; i >= 0; i--) {
566             rightRow.insertFirst(this.changeViewActions[i].getEl().parent().dom);
567         }
568         this['show' + Ext.util.Format.capitalize(viewParts.period) +  'View'].toggle(true);
569         this['show' + Ext.util.Format.capitalize(viewParts.presentation) +  'View'].toggle(true);
570         
571         // update actions
572         this.updateEventActions();
573         
574         // update data
575         panel.getView().updatePeriod({from: this.startDate});
576         panel.getStore().load({});
577         
578         this.fireEvent('changeview', this, view);
579     },
580
581     /**
582      * returns all filter data for current view
583      */
584     getAllFilterData: function (options) {
585         var store = this.getCalendarPanel(this.activeView).getStore();
586         
587         options = Ext.apply({
588             refresh: true, // ommit loadMask
589             noPeriodFilter: true
590         }, options || {});
591         this.onStoreBeforeload(store, options);
592         
593         return options.params.filter;
594     },
595     
596     getCustomfieldFilters: Tine.widgets.grid.GridPanel.prototype.getCustomfieldFilters,
597     
598     getFilterToolbar: Tine.widgets.grid.GridPanel.prototype.getFilterToolbar,
599     
600     /**
601      * returns store of currently active view
602      */
603     getStore: function () {
604         return this.getCalendarPanel(this.activeView).getStore();
605     },
606     
607     /**
608      * onContextMenu
609      * 
610      * @param {Event} e
611      */
612     onContextMenu: function (e) {
613         e.stopEvent();
614
615         var view = this.getCalendarPanel(this.activeView).getView();
616         var event = view.getTargetEvent(e);
617         var datetime = view.getTargetDateTime(e);
618         
619         if (event && event.id.match(/new-ext-gen/) ) {
620             // new event, no ctx menu possible atm
621             // @see 0008948: deactivate context menu on new events
622             return;
623         }
624         
625         var addAction, responseAction, copyAction;
626
627         if (datetime || event) {
628             var dtStart = datetime || event.get('dtstart').clone();
629             if (dtStart.format('H:i') === '00:00') {
630                 dtStart = dtStart.add(Date.HOUR, 9);
631             }
632             
633             addAction = {
634                 text: this.i18nAddActionText ? this.app.i18n._hidden(this.i18nAddActionText) : String.format(Tine.Tinebase.translation._hidden('Add {0}'), this.i18nRecordName),
635                 handler: this.onEditInNewWindow.createDelegate(this, ["add", null, null, {dtStart: dtStart, is_all_day_event: datetime && datetime.is_all_day_event}]),
636                 iconCls: 'action_add'
637             };
638             
639             // assemble event actions
640             if (event) {
641                 responseAction = this.getResponseAction(event);
642                 copyAction = this.getCopyAction(event);
643             }
644         } else {
645             addAction = this.action_addInNewWindow;
646         }
647         
648         if (event) {
649             view.getSelectionModel().select(event, e, e.ctrlKey);
650         } else {
651             view.getSelectionModel().clearSelections();
652         }
653
654         var menuitems = this.recordActions.concat(addAction, responseAction || [], copyAction || []);
655         
656         if (event) {
657             this.action_copy_to.setDisabled(event.isRecurInstance() || event.isRecurException() || event.isRecurBase());
658             menuitems = menuitems.concat(['-', this.action_cut, this.action_copy_to, '-']);
659         } else if (Tine.Tinebase.data.Clipboard.has('Calendar', 'Event')) {
660             menuitems = menuitems.concat(['-', this.getPasteAction(datetime, Tine.Tinebase.data.Clipboard.pull('Calendar', 'Event', true)), this.action_cancelPasting, '-']);
661         }
662
663         var ctxMenu = new Ext.menu.Menu({
664             plugins: [{
665                 ptype: 'ux.itemregistry',
666                 key:   'Calendar-MainScreenPanel-ContextMenu',
667                 config: {
668                     event: event,
669                     datetime: datetime
670                 }
671             },
672             // to allow gridpanel hooks (like email compose)
673             {
674                 ptype: 'ux.itemregistry',
675                 key:   'Calendar-Event-GridPanel-ContextMenu'
676             }],
677             items: menuitems
678         });
679         ctxMenu.showAt(e.getXY());
680     },
681     
682     /**
683      * get response action
684      * 
685      * @param {Tine.Calendar.Model.Event} event
686      * @return {Object}
687      */
688     getResponseAction: function(event) {
689         var statusStore = Tine.Tinebase.widgets.keyfield.StoreMgr.get('Calendar', 'attendeeStatus'),
690             myAttenderRecord = event.getMyAttenderRecord(),
691             myAttenderStatus = myAttenderRecord ? myAttenderRecord.get('status') : null,
692             myAttenderStatusRecord = statusStore.getById(myAttenderStatus),
693             responseAction = null;
694         
695         if (myAttenderRecord) {
696             responseAction = {
697                 text: this.app.i18n._('Set my response'),
698                 icon: myAttenderStatusRecord ? myAttenderStatusRecord.get('icon') : false,
699                 menu: []
700             };
701             
702             statusStore.each(function(status) {
703                 var isCurrent = myAttenderRecord.get('status') === status.id;
704                 
705                 // NOTE: we can't use checked items here as we use icons already
706                 responseAction.menu.push({
707                     text: status.get('i18nValue'),
708                     handler: this.setResponseStatus.createDelegate(this, [event, status.id]),
709                     icon: status.get('icon'),
710                     disabled: myAttenderRecord.get('status') === status.id
711                 });
712             }, this);
713         }
714         
715         return responseAction;
716     },
717
718     /**
719      * get copy action
720      * 
721      * @param {Tine.Calendar.Model.Event} event
722      * @return {Object}
723      */
724     getCopyAction: function(event) {
725         var copyAction = {
726             text: String.format(this.app.i18n._('Copy {0}'), this.i18nRecordName),
727             handler: this.onEditInNewWindow.createDelegate(this, ["copy", event]),
728             iconCls: 'action_editcopy',
729             // TODO allow to copy recurring events / exceptions
730             disabled: event.isRecurInstance() || event.isRecurException() || event.isRecurBase()
731         };
732         
733         return copyAction
734     },
735     
736     checkPastEvent: function(event, checkBusyConflicts, actionType, oldEvent) {
737         var start = event.get('dtstart').getTime();
738         var morning = new Date().clearTime().getTime();
739
740         switch (actionType) {
741             case 'update':
742                 var title = this.app.i18n._('Updating event in the past'),
743                     optionYes = this.app.i18n._('Update this event'),
744                     optionNo = this.app.i18n._('Do not update this event');
745                 break;
746             case 'add':
747             default:
748                 var title = this.app.i18n._('Creating event in the past'),
749                     optionYes = this.app.i18n._('Create this event'),
750                     optionNo = this.app.i18n._('Do not create this event');
751         }
752         
753         if(start < morning) {
754             Tine.widgets.dialog.MultiOptionsDialog.openWindow({
755                 title: title,
756                 height: 170,
757                 scope: this,
758                 options: [
759                     {text: optionYes, name: 'yes'},
760                     {text: optionNo, name: 'no'}
761                 ],
762                 
763                 handler: function(option) {
764                     try {
765                         switch (option) {
766                             case 'yes':
767                                 if (actionType == 'update') this.onUpdateEvent(event, true, oldEvent);
768                                 else this.onAddEvent(event, checkBusyConflicts, true);
769                                 break;
770                             case 'no':
771                             default:
772                                 try {
773                                     var panel = this.getCalendarPanel(this.activeView);
774                                     if(panel) {
775                                         var store = panel.getStore(),
776                                             view = panel.getView();
777                                     }
778                                 } catch(e) {
779                                     var panel = null, 
780                                         store = null,
781                                         view = null;
782                                 }
783                                 
784                                 if (actionType == 'add') {
785                                     if(store) store.remove(event);
786                                 } else {
787                                     if (view && view.rendered) {
788                                         this.loadMask.show();
789                                         store.reload();
790 //                                        TODO: restore original event so no reload is needed
791 //                                        var updatedEvent = event;
792 //                                        updatedEvent.dirty = false;
793 //                                        updatedEvent.editing = false;
794 //                                        updatedEvent.modified = null;
795 //                                        
796 //                                        store.replaceRecord(event, updatedEvent);
797 //                                        view.getSelectionModel().select(event);
798                                     }
799                                     this.setLoading(false);
800                                 }
801                         }
802                     } catch (e) {
803                         Tine.log.error('Tine.Calendar.MainScreenCenterPanel::checkPastEvent::handler');
804                         Tine.log.error(e);
805                     }
806                 }             
807             });
808         } else {
809             if (actionType == 'update') this.onUpdateEvent(event, true, oldEvent);
810             else this.onAddEvent(event, checkBusyConflicts, true);
811         }
812     },
813     
814     onAddEvent: function(event, checkBusyConflicts, pastChecked) {
815         if(!pastChecked) {
816             this.checkPastEvent(event, checkBusyConflicts, 'add');
817             return;
818         }
819         
820         this.setLoading(true);
821         
822         // remove temporary id
823         if (event.get('id').match(/new/)) {
824             event.set('id', '');
825         }
826         
827         if (event.isRecurBase()) {
828             if (this.loadMask) {
829                 this.loadMask.show();
830             }
831         }
832         
833         var panel = this.getCalendarPanel(this.activeView),
834             store = event.store,
835             view = panel.getView();
836         
837         Tine.Calendar.backend.saveRecord(event, {
838             scope: this,
839             success: function(createdEvent) {
840                 if (createdEvent.isRecurBase()) {
841                     store.load({refresh: true});
842                 } else {
843                     // store may be lost on conflict or else
844                     if (store) {
845                         store.replaceRecord(event, createdEvent);
846                         this.setLoading(false);
847                     } else {
848                         this.refresh();
849                     }
850                     
851                     if (view && view.rendered) {
852                         view.getSelectionModel().select(createdEvent);
853                     }
854                 }
855             },
856             failure: this.onProxyFail.createDelegate(this, [event], true)
857         }, {
858             checkBusyConflicts: checkBusyConflicts === false ? 0 : 1
859         });
860     },
861     
862     onUpdateEvent: function(event, pastChecked, oldEvent) {
863         this.setLoading(true);
864         
865         if(!pastChecked) {
866             this.checkPastEvent(event, null, 'update', oldEvent);
867             return;
868         }
869         
870         if (event.isRecurBase() && this.loadMask) {
871            this.loadMask.show();
872         }
873         
874         if (event.id && (event.isRecurInstance() || event.isRecurException() || (event.isRecurBase() && ! event.get('rrule').newrule))) {
875             
876             var options = [];
877             
878             // the container has changed - force update whole series
879 //            if (! oldEvent || event.get('container_id') == oldEvent.get('container_id')) {
880                 options.push({text: this.app.i18n._('Update this event only'), name: 'this'});
881                 options.push({text: this.app.i18n._('Update this and all future events'), name: (event.isRecurBase() && ! event.get('rrule').newrule) ? 'series' : 'future'});
882 //            }
883             
884             options.push({text: this.app.i18n._('Update whole series'), name: 'series'});
885             options.push({text: this.app.i18n._('Update nothing'), name: 'cancel'});
886             
887             Tine.widgets.dialog.MultiOptionsDialog.openWindow({
888                 title: this.app.i18n._('Update Event'),
889                 height: 170,
890                 scope: this,
891                 options: options,
892                 handler: function(option) {
893                     var store = event.store;
894
895                     if (! store) {
896                         store = this.getStore();
897                     }
898                     
899                     switch (option) {
900                         case 'series':
901                             if (this.loadMask) {
902                                 this.loadMask.show();
903                             }
904                             
905                             var options = {
906                                 scope: this,
907                                 success: function() {
908                                     store.load({refresh: true});
909                                 }
910                             };
911                             options.failure = this.onProxyFail.createDelegate(this, [event, Tine.Calendar.backend.updateRecurSeries.createDelegate(Tine.Calendar.backend, [event, false, options])], true);
912                             
913                             if (event.isRecurException()) {
914                                 Tine.Calendar.backend.saveRecord(event, options, {range: 'ALL', checkBusyConflicts: true});
915                             } else {
916                                 Tine.Calendar.backend.updateRecurSeries(event, true, options);
917                             }
918                             break;
919                             
920                         case 'this':
921                         case 'future':
922                             var options = {
923                                 scope: this,
924                                 success: function(updatedEvent) {
925                                     // NOTE: we also need to refresh for 'this' as otherwise
926                                     //       baseevent is not refreshed -> concurrency failures
927                                     store.load({refresh: true});
928                                 },
929                                 failure: this.onProxyFail.createDelegate(this, [event], true)
930                             };
931                             
932                             
933                             if (event.isRecurException()) {
934                                 var range = (option === 'this' ? 'THIS' : 'THISANDFUTURE');
935                                 options.failure = this.onProxyFail.createDelegate(this, [event, Tine.Calendar.backend.saveRecord.createDelegate(Tine.Calendar.backend, [event, options, {range: range, checkBusyConflicts: false}])], true);
936                                 Tine.Calendar.backend.saveRecord(event, options, {range: range, checkBusyConflicts: true});
937                             } else {
938                                 options.failure = this.onProxyFail.createDelegate(this, [event, Tine.Calendar.backend.createRecurException.createDelegate(Tine.Calendar.backend, [event, false, option == 'future', false, options])], true);
939                                 Tine.Calendar.backend.createRecurException(event, false, option === 'future', true, options);
940                             }
941                             break;
942                             
943                         default:
944                             if(this.loadMask) { //no loadMask called from another app
945                                 this.loadMask.show();
946                                 store.load({refresh: true});
947                             }
948                             break;
949                     }
950
951                 } 
952             });
953         } else {
954             this.onUpdateEventAction(event);
955         }
956     },
957     
958     onUpdateEventAction: function(event) {
959         var panel = this.getCalendarPanel(this.activeView),
960             store = panel.getStore(),
961             view = panel.getView();
962             
963         Tine.Calendar.backend.saveRecord(event, {
964             scope: this,
965             success: function(updatedEvent) {
966                 if (updatedEvent.isRecurBase()) {
967                     store.load({refresh: true});
968                 } else {
969                     this.congruenceFilterCheck(event, updatedEvent);
970                 }
971             },
972             failure: this.onProxyFail.createDelegate(this, [event], true)
973         }, {
974             checkBusyConflicts: 1
975         });
976     },
977
978     /**
979      * checks, if the last filter still matches after update
980      * 
981      * @param {Tine.Calendar.Model.Event} event
982      * @param {Tine.Calendar.Model.Event} updatedEvent
983      */
984     congruenceFilterCheck: function(event, updatedEvent) {
985         if (! (event.ui && event.ui.rendered)) {
986             event.store.replaceRecord(event, updatedEvent);
987             this.setLoading(false);
988             return;
989         }
990         
991         var filterData = this.getAllFilterData(),
992             store = event.store;
993         
994         if (! store) {
995             store = this.getStore();
996         }
997         
998         filterData[0].filters[0].filters.push({field: 'id', operator: 'in', value: [ updatedEvent.get('id') ]});
999         
1000         Tine.Calendar.searchEvents(filterData, {}, function(r) {
1001             if(r.totalcount == 0) {
1002                 updatedEvent.outOfFilter = true;
1003             }
1004             
1005             store.replaceRecord(event, updatedEvent);
1006             this.setLoading(false);
1007         }, this);
1008     },
1009     
1010     onDeleteRecords: function () {
1011         var panel = this.getCalendarPanel(this.activeView);
1012         var selection = panel.getSelectionModel().getSelectedEvents();
1013         
1014         var containsRecurBase = false,
1015             containsRecurInstance = false,
1016             containsRecurException = false;
1017         
1018         Ext.each(selection, function (event) {
1019             if(event.ui) event.ui.markDirty();
1020             if (event.isRecurInstance()) {
1021                 containsRecurInstance = true;
1022             }
1023             if (event.isRecurBase()) {
1024                 containsRecurBase = true;
1025             }
1026             if (event.isRecurException()) {
1027                 containsRecurException = true;
1028             }
1029         });
1030         
1031         if (selection.length > 1 && (containsRecurBase || containsRecurInstance)) {
1032             Ext.Msg.show({
1033                 title: this.app.i18n._('Please Change Selection'), 
1034                 msg: this.app.i18n._('Your selection contains recurring events. Recuring events must be deleted seperatly!'),
1035                 icon: Ext.MessageBox.INFO,
1036                 buttons: Ext.Msg.OK,
1037                 scope: this,
1038                 fn: function () {
1039                     this.onDeleteRecordsConfirmFail(panel, selection);
1040                 }
1041             });
1042             return;
1043         }
1044         
1045         if (selection.length === 1 && (containsRecurBase || containsRecurInstance || containsRecurException)) {
1046             this.deleteMethodWin = Tine.widgets.dialog.MultiOptionsDialog.openWindow({
1047                 title: this.app.i18n._('Delete Event'),
1048                 scope: this,
1049                 height: 170,
1050                 options: [
1051                     {text: this.app.i18n._('Delete this event only'), name: 'this'},
1052                     {text: this.app.i18n._('Delete this and all future events'), name: containsRecurBase ? 'all' : 'future'},
1053                     {text: this.app.i18n._('Delete whole series'), name: 'all'},
1054                     {text: this.app.i18n._('Delete nothing'), name: 'nothing'}
1055                 ],
1056                 handler: function (option) {
1057                     try {
1058                         switch (option) {
1059                             case 'all':
1060                             case 'this':
1061                             case 'future':
1062                                 panel.getTopToolbar().beforeLoad();
1063                                 if (option !== 'this') {
1064                                     this.loadMask.show();
1065                                 }
1066                                 
1067                                 var options = {
1068                                     scope: this,
1069                                     success: function () {
1070                                         if (option === 'this') {
1071                                             Ext.each(selection, function (event) {
1072                                                 panel.getStore().remove(event);
1073                                             });
1074                                         }
1075                                         this.refresh(true);
1076                                     }
1077                                 };
1078                                 
1079                                 if (containsRecurException) {
1080                                     var range = (option === 'future') ? 'THISANDFUTURE' : Ext.util.Format.uppercase(option);
1081                                     Tine.Calendar.backend.deleteRecords([selection[0]], options, {range: range});
1082                                 } else {
1083                                     if (option === 'all') {
1084                                         Tine.Calendar.backend.deleteRecurSeries(selection[0], options);
1085                                     } else {
1086                                         Tine.Calendar.backend.createRecurException(selection[0], true, option === 'future', false, options);
1087                                     }
1088                                 }
1089                                 break;
1090                             default:
1091                                 this.onDeleteRecordsConfirmFail(panel, selection);
1092                                 break;
1093                     }
1094                     
1095                     } catch (e) {
1096                         Tine.log.error('Tine.Calendar.MainScreenCenterPanel::onDeleteRecords::handle');
1097                         Tine.log.error(e);
1098                     }
1099                 }
1100             });
1101             return;
1102         }
1103         
1104         // else
1105         var i18nQuestion = String.format(this.app.i18n.ngettext('Do you really want to delete this event?', 'Do you really want to delete the {0} selected events?', selection.length), selection.length);
1106         Ext.MessageBox.confirm(Tine.Tinebase.translation._hidden('Confirm'), i18nQuestion, function (btn) {
1107             if (btn === 'yes') {
1108                 this.onDeleteRecordsConfirmNonRecur(panel, selection);
1109             } else {
1110                 this.onDeleteRecordsConfirmFail(panel, selection);
1111             }
1112         }, this);
1113         
1114     },
1115     
1116     onDeleteRecordsConfirmNonRecur: function (panel, selection) {
1117         panel.getTopToolbar().beforeLoad();
1118         
1119         // create a copy of selection so selection changes don't affect this
1120         var sel = Ext.unique(selection);
1121                 
1122         var options = {
1123             scope: this,
1124             success: function () {
1125                 panel.getTopToolbar().onLoad();
1126                 Ext.each(sel, function (event) {
1127                     panel.getStore().remove(event);
1128                 });
1129             },
1130             failure: function () {
1131                 panel.getTopToolbar().onLoad();
1132                 Ext.MessageBox.alert(Tine.Tinebase.translation._hidden('Failed'), String.format(this.app.i18n.n_('Failed to delete event', 'Failed to delete the {0} events', selection.length), selection.length));
1133             }
1134         };
1135         
1136         Tine.Calendar.backend.deleteRecords(selection, options);
1137     },
1138     
1139     onDeleteRecordsConfirmFail: function (panel, selection) {
1140         Ext.each(selection, function (event) {
1141             event.ui.clearDirty();
1142         });
1143     },
1144     
1145     /**
1146      * is called on action cut
1147      * 
1148      * @param {} action
1149      * @param {} event
1150      */
1151     onCutEvent: function(action, event) {
1152         var panel = this.getCalendarPanel(this.activeView);
1153         var selection = panel.getSelectionModel().getSelectedEvents();
1154         if (Ext.isArray(selection) && selection.length === 1) {
1155             event = selection[0];
1156         }
1157         if (event.ui) {
1158             event.ui.markDirty();
1159         }
1160         Tine.Tinebase.data.Clipboard.push(event);
1161     },
1162
1163     /**
1164      * Is called on copy to clipboard
1165      *
1166      * @param action
1167      * @param event
1168      */
1169     onCopyToEvent: function(action, event) {
1170         var panel = this.getCalendarPanel(this.activeView);
1171         var selection = panel.getSelectionModel().getSelectedEvents();
1172         if (Ext.isArray(selection) && selection.length === 1) {
1173             event = selection[0];
1174         }
1175
1176         event.isCopy = true;
1177
1178         Tine.Tinebase.data.Clipboard.push(event);
1179     },
1180     
1181     /**
1182      * is called on cancelling cut & paste
1183      */
1184     onCutCancelEvent: function() {
1185         var store = this.getStore();
1186         var ids = Tine.Tinebase.data.Clipboard.getIds('Calendar', 'Event');
1187         
1188         for (var index = 0; index < ids.length; index++) {
1189             var record = store.getAt(store.findExact('id', ids[index]));
1190             if (record && record.ui) {
1191                 record.ui.clearDirty();
1192             }
1193         }
1194
1195         Tine.Tinebase.data.Clipboard.clear('Calendar', 'Event');
1196     },
1197     
1198     /**
1199      * is called on action paste
1200      * 
1201      * @param {Date} datetime
1202      */
1203     onPasteEvent: function(datetime) {
1204         var record = Tine.Tinebase.data.Clipboard.pull('Calendar', 'Event'),
1205             isCopy = record.isCopy,
1206             sourceView = record.view,
1207             sourceRecord = record,
1208             sourceViewAttendee = sourceView.ownerCt.attendee,
1209             destinationView = this.getCalendarPanel(this.activeView).getView(),
1210             destinationViewAttendee = destinationView.ownerCt.attendee;
1211
1212         if (! record) {
1213             return;
1214         }
1215         
1216         var dtend   = record.get('dtend'),
1217             dtstart = record.get('dtstart'),
1218             eventLength = dtend - dtstart,
1219             store = this.getStore();
1220
1221         record.beginEdit();
1222
1223         if (isCopy != true) {
1224             // remove from ui before update
1225             var oldRecord = store.getAt(store.findExact('id', record.getId()));
1226             if (oldRecord && oldRecord.hasOwnProperty('ui')) {
1227                 oldRecord.ui.remove();
1228             }
1229         } else {
1230             record = Tine.Calendar.EventEditDialog.superclass.doCopyRecordToReturn.call(this, record);
1231
1232             record.set('editGrant', true);
1233             record.set('id', '');
1234
1235             // remove attender ids
1236             Ext.each(record.data.attendee, function(attender) {
1237                 delete attender.id;
1238             }, this);
1239         }
1240
1241         // @TODO move to common function with daysView::notifyDrop parts
1242         // change attendee in split view
1243         if (sourceViewAttendee || destinationViewAttendee) {
1244             var attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(sourceRecord.get('attendee')),
1245                 sourceAttendee = sourceViewAttendee ? Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, sourceViewAttendee) : false,
1246                 destinationAttendee = destinationViewAttendee ? Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, destinationViewAttendee) : false;
1247
1248             if (destinationViewAttendee && !destinationAttendee) {
1249                 destinationAttendee = new Tine.Calendar.Model.Attender(destinationViewAttendee.data);
1250
1251                 attendeeStore.remove(sourceAttendee);
1252                 attendeeStore.add(destinationAttendee);
1253
1254                 Tine.Calendar.Model.Attender.getAttendeeStore.getData(attendeeStore, record);
1255             }
1256         }
1257
1258
1259         if (datetime.is_all_day_event) {
1260             record.set('dtstart', datetime);
1261             record.set('dtend', datetime.clone().add(Date.DAY, 1).add(Date.SECOND, -1));
1262             record.set('is_all_day_event', true);
1263         } else if (datetime.date_only) {
1264             var adoptedDtStart = datetime.clone();
1265             adoptedDtStart.setHours(dtstart.getHours());
1266             adoptedDtStart.setMinutes(dtstart.getMinutes());
1267             adoptedDtStart.setSeconds(dtstart.getSeconds());
1268
1269             record.set('dtstart', adoptedDtStart);
1270             record.set('dtend', new Date(adoptedDtStart.getTime() + eventLength));
1271         } else {
1272             record.set('dtstart', datetime);
1273             record.set('dtend', new Date(datetime.getTime() + eventLength));
1274         }
1275
1276         record.endEdit();
1277
1278         if (isCopy == true) {
1279             record.isCopy = true;
1280             Tine.Tinebase.data.Clipboard.push(record);
1281             if (record.ui) {
1282                 record.ui.clearDirty();
1283             }
1284
1285             this.onAddEvent(record);
1286         } else {
1287             this.onUpdateEvent(record);
1288         }
1289     },
1290     
1291     /**
1292      * open event in new window
1293      * 
1294      * @param {String} action  add|edit|copy
1295      * @param {String} event   edit given event instead of selected event
1296      * @param {Object} plugins event dlg plugins
1297      * @param {Object} default properties for new items
1298      */
1299     onEditInNewWindow: function (action, event, plugins, defaults) {
1300         if (! event) {
1301             event = null;
1302         }
1303         // needed for addToEventPanel
1304         if (Ext.isObject(action)) {
1305             action = action.actionType;
1306         }
1307         
1308         if (action === 'edit' && ! event) {
1309             var panel = this.getCalendarPanel(this.activeView);
1310             var selection = panel.getSelectionModel().getSelectedEvents();
1311             if (Ext.isArray(selection) && selection.length === 1) {
1312                 event = selection[0];
1313             }
1314         }
1315
1316         if (action === 'edit' && (! event || event.dirty)) {
1317             return;
1318         }
1319         
1320         if (! event) {
1321             event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), defaults), 0);
1322             if (defaults && Ext.isDate(defaults.dtStart)) {
1323                 event.set('dtstart', defaults.dtStart);
1324                 event.set('dtend', defaults.dtStart.add(Date.MINUTE, Tine.Calendar.Model.Event.getMeta('defaultEventDuration')));
1325             }
1326         }
1327         
1328         Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::onEditInNewWindow() - Opening event edit dialog with action: ' + action);
1329
1330         Tine.Calendar.EventEditDialog.openWindow({
1331             plugins: plugins ? Ext.encode(plugins) : null,
1332             record: Ext.encode(event.data),
1333             recordId: event.data.id,
1334             copyRecord: (action == 'copy'),
1335             listeners: {
1336                 scope: this,
1337                 update: function (eventJson) {
1338                     var updatedEvent = Tine.Calendar.backend.recordReader({responseText: eventJson});
1339                     
1340                     updatedEvent.dirty = true;
1341                     updatedEvent.modified = {};
1342                     event.phantom = (action === 'edit');
1343                     var panel = this.getCalendarPanel(this.activeView);
1344                     var store = panel.getStore();
1345                     event = store.getById(event.id);
1346                     
1347                     if (event && action !== 'copy') {
1348                         store.replaceRecord(event, updatedEvent);
1349                     } else {
1350                         store.add(updatedEvent);
1351                     }
1352                     
1353                     this.onUpdateEvent(updatedEvent, false, event);
1354                 }
1355             }
1356         });
1357     },
1358     
1359     onKeyDown: function (e) {
1360         if (e.ctrlKey) {
1361             switch (e.getKey()) 
1362             {
1363             case e.A:
1364                 // select only current page
1365                 //this.grid.getSelectionModel().selectAll(true);
1366                 e.preventDefault();
1367                 break;
1368             case e.E:
1369                 if (!this.action_editInNewWindow.isDisabled()) {
1370                     this.onEditInNewWindow('edit');
1371                 }
1372                 e.preventDefault();
1373                 break;
1374             case e.N:
1375                 if (!this.action_addInNewWindow.isDisabled()) {
1376                     this.onEditInNewWindow('add');
1377                 }
1378                 e.preventDefault();
1379                 break;
1380             }
1381         } else if (e.getKey() === e.DELETE) {
1382             if (! this.action_deleteRecord.isDisabled()) {
1383                 this.onDeleteRecords.call(this);
1384             }
1385         }
1386     },
1387     
1388     onPrintMenuClick: function(splitBtn, e) {
1389         if (! String(this.activeView).match(/(day|week)sheet$/i)) {
1390             splitBtn.menu.hide();
1391         }
1392     },
1393     
1394     onPrint: function(printMode) {
1395         var printMode = printMode ? printMode : this.defaultPrintMode,
1396             panel = this.getCalendarPanel(this.activeView),
1397             view = panel ? panel.getView() : null;
1398             
1399         if (view && Ext.isFunction(view.print)) {
1400             view.print(printMode);
1401         } else {
1402             Ext.Msg.alert(this.app.i18n._('Could not Print'), this.app.i18n._('Sorry, your current view does not support printing.'));
1403         }
1404     },
1405     
1406     /**
1407      * called before store queries for data
1408      */
1409     onStoreBeforeload: function (store, options) {
1410         options.params = options.params || {};
1411         
1412         // define a transaction
1413         this.lastStoreTransactionId = options.transactionId = Ext.id();
1414         
1415         // allways start with an empty filter set!
1416         // this is important for paging and sort header!
1417         options.params.filter = [];
1418         
1419         if (! options.refresh && this.rendered) {
1420             // defer to have the loadMask centered in case of rendering actions
1421             this.loadMask.show.defer(50, this.loadMask);
1422         }
1423         
1424         // note, we can't use the 'normal' plugin approach here, cause we have to deal with n stores
1425         //var calendarSelectionPlugin = this.app.getMainScreen().getWestPanel().getContainerTreePanel().getFilterPlugin();
1426         //calendarSelectionPlugin.onBeforeLoad.call(calendarSelectionPlugin, store, options);
1427         
1428         this.filterToolbar.onBeforeLoad.call(this.filterToolbar, store, options);
1429         
1430         // add period filter as last filter to not conflict with first OR filter
1431         if (! options.noPeriodFilter) {
1432             options.params.filter.push({field: 'period', operator: 'within', value: this.getCalendarPanel(this.activeView).getView().getPeriod() });
1433         }
1434     },
1435     
1436     /**
1437      * fence against loading of wrong data set
1438      */
1439     onStoreBeforeLoadRecords: function(o, options, success) {
1440         return this.lastStoreTransactionId === options.transactionId;
1441     },
1442     
1443     /**
1444      * called when store loaded data
1445      */
1446     onStoreLoad: function (store, options) {
1447         if (this.rendered) {
1448             this.loadMask.hide();
1449         }
1450         
1451         // reset autoRefresh
1452         if (window.isMainWindow && this.autoRefreshInterval) {
1453             this.autoRefreshTask.delay(this.autoRefreshInterval * 1000);
1454         }
1455         
1456         // check if store is current store
1457         if (store !== this.getCalendarPanel(this.activeView).getStore()) {
1458             Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::onStoreLoad view is not active anymore');
1459             return;
1460         }
1461         
1462         if (this.rendered) {
1463             // update filtertoolbar
1464             if(this.filterToolbar) {
1465                 this.filterToolbar.setValue(store.proxy.jsonReader.jsonData.filter);
1466             }
1467             // update container tree
1468             Tine.Tinebase.appMgr.get('Calendar').getMainScreen().getWestPanel().getContainerTreePanel().getFilterPlugin().setValue(store.proxy.jsonReader.jsonData.filter);
1469             
1470             // update attendee filter grid
1471             Tine.Tinebase.appMgr.get('Calendar').getMainScreen().getWestPanel().getAttendeeFilter().setFilterValue(store.proxy.jsonReader.jsonData.filter);
1472         }
1473     },
1474     
1475     /**
1476      * on store load exception
1477      * 
1478      * @param {Tine.Tinebase.data.RecordProxy} proxy
1479      * @param {String} type
1480      * @param {Object} error
1481      * @param {Object} options
1482      */
1483     onStoreLoadException: function(proxy, type, error, options) {
1484         // reset autoRefresh
1485         if (window.isMainWindow && this.autoRefreshInterval) {
1486             this.autoRefreshTask.delay(this.autoRefreshInterval * 5000);
1487         }
1488         
1489         this.setLoading(false);
1490         this.loadMask.hide();
1491         
1492         if (! options.autoRefresh) {
1493             this.onProxyFail(error);
1494         }
1495     },
1496     
1497     onProxyFail: function(error, event, ignoreFn) {
1498         this.setLoading(false);
1499         if(this.loadMask) this.loadMask.hide();
1500         
1501         if (error.code == 901) {
1502            
1503             // resort fbInfo to combine all events of a attender
1504             var busyAttendee = [];
1505             var conflictEvents = {};
1506             var attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(error.event.attendee);
1507             
1508             Ext.each(error.freebusyinfo, function(fbinfo) {
1509                 attendeeStore.each(function(a) {
1510                     var userType = a.get('user_type');
1511                     userType = userType == 'groupmember' ? 'user' : userType;
1512                     if (userType == fbinfo.user_type && a.getUserId() == fbinfo.user_id) {
1513                         if (busyAttendee.indexOf(a) < 0) {
1514                             busyAttendee.push(a);
1515                             conflictEvents[a.id] = [];
1516                         }
1517                         conflictEvents[a.id].push(fbinfo);
1518                     }
1519                 });
1520             }, this);
1521             
1522             // generate html for each busy attender
1523             var busyAttendeeHTML = '';
1524             var denyIgnore = false;
1525
1526             Ext.each(busyAttendee, function(busyAttender) {
1527                 // TODO refactore name handling of attendee
1528                 //      -> attender model needs knowlege of how to get names!
1529                 //var attenderName = a.getName();
1530                 var attenderName = Tine.Calendar.AttendeeGridPanel.prototype.renderAttenderName.call(Tine.Calendar.AttendeeGridPanel.prototype, busyAttender.get('user_id'), false, busyAttender);
1531                 busyAttendeeHTML += '<div class="cal-conflict-attendername">' + attenderName + '</div>';
1532                 
1533                 var eventInfos = [];
1534                 Ext.each(conflictEvents[busyAttender.id], function(fbInfo) {
1535                     var format = 'H:i';
1536                     var eventInfo;
1537                     var dateFormat = Ext.form.DateField.prototype.format;
1538                     if (event.get('dtstart').format(dateFormat) != event.get('dtend').format(dateFormat) ||
1539                         Date.parseDate(fbInfo.dtstart, Date.patterns.ISO8601Long).format(dateFormat) != Date.parseDate(fbInfo.dtend, Date.patterns.ISO8601Long).format(dateFormat))
1540                     {
1541                         eventInfo = Date.parseDate(fbInfo.dtstart, Date.patterns.ISO8601Long).format(dateFormat + ' ' + format) + ' - ' + Date.parseDate(fbInfo.dtend, Date.patterns.ISO8601Long).format(dateFormat + ' ' + format);
1542                     } else {
1543                         eventInfo = Date.parseDate(fbInfo.dtstart, Date.patterns.ISO8601Long).format(dateFormat + ' ' + format) + ' - ' + Date.parseDate(fbInfo.dtend, Date.patterns.ISO8601Long).format(format);
1544                     }
1545                     if (fbInfo.event && fbInfo.event.summary) {
1546                         eventInfo += ' : ' + fbInfo.event.summary;
1547                     }
1548                     if (fbInfo.type == 'BUSY_UNAVAILABLE') {
1549                         denyIgnore = true;
1550                         eventInfo += '<span class="cal-conflict-eventinfos-unavailable">' + this.app.i18n._('Unavailable') + '</span>';
1551                     }
1552                     eventInfos.push(eventInfo);
1553                 }, this);
1554                 busyAttendeeHTML += '<div class="cal-conflict-eventinfos">' + eventInfos.join(', <br />') + '</div>';
1555                 
1556             }, this);
1557             
1558             this.conflictConfirmWin = Tine.widgets.dialog.MultiOptionsDialog.openWindow({
1559                 modal: true,
1560                 allowCancel: false,
1561                 height: 180 + 15*error.freebusyinfo.length,
1562                 title: this.app.i18n._('Scheduling Conflict'),
1563                 questionText: '<div class = "cal-conflict-heading">' +
1564                                    this.app.i18n._('The following attendee are busy at the requested time:') + 
1565                                '</div>' +
1566                                busyAttendeeHTML,
1567                 options: [
1568                     {text: this.app.i18n._('Ignore Conflict'), name: 'ignore', disabled: denyIgnore},
1569                     {text: this.app.i18n._('Edit Event'), name: 'edit', checked: true},
1570                     {text: this.app.i18n._('Cancel this action'), name: 'cancel'}
1571                 ],
1572                 scope: this,
1573                 handler: function(option) {
1574                     var panel = this.getCalendarPanel(this.activeView),
1575                         store = panel.getStore();
1576
1577                     switch (option) {
1578                         case 'ignore':
1579                             if (Ext.isFunction(ignoreFn)) {
1580                                 ignoreFn();
1581                             } else {
1582                                 this.onAddEvent(event, false, true);
1583                             }
1584                             this.conflictConfirmWin.close();
1585                             break;
1586                         
1587                         case 'edit':
1588                             
1589                             var presentationMatch = this.activeView.match(this.presentationRe),
1590                                 presentation = Ext.isArray(presentationMatch) ? presentationMatch[0] : null;
1591                             
1592                             if (presentation != 'Grid') {
1593                                 var view = panel.getView();
1594                                 view.getSelectionModel().select(event);
1595                                 // mark event as not dirty to allow edit dlg
1596                                 event.dirty = false;
1597                                 view.fireEvent('dblclick', view, event);
1598                             } else {
1599                                 // add or edit?
1600                                 this.onEditInNewWindow(null, event);
1601                             }
1602                             
1603                             this.conflictConfirmWin.close();
1604                             break;
1605                         case 'cancel':
1606                         default:
1607                             this.conflictConfirmWin.close();
1608                             this.loadMask.show();
1609                             store.load({refresh: true});
1610                             break;
1611                     }
1612                 }
1613             });
1614             
1615         } else {
1616             Tine.Tinebase.ExceptionHandler.handleRequestException(error);
1617         }
1618     },
1619     
1620     refresh: function (options) {
1621         // convert old boolean argument
1622         options = Ext.isObject(options) ? options : {
1623             refresh: !!options
1624         };
1625         Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::refresh(' + options.refresh + ')');
1626         var panel = this.getCalendarPanel(this.activeView);
1627         panel.getStore().load(options);
1628         
1629         // clear favorites
1630         Tine.Tinebase.appMgr.get('Calendar').getMainScreen().getWestPanel().getFavoritesPanel().getSelectionModel().clearSelections();
1631     },
1632     
1633     setLoading: function(bool) {
1634         if (this.rendered) {
1635             var panel = this.getCalendarPanel(this.activeView),
1636                 tbar = panel.getTopToolbar();
1637                 
1638             if (tbar && tbar.loading) {
1639                 tbar.loading[bool ? 'disable' : 'enable']();
1640             }
1641         }
1642     },
1643     
1644     setResponseStatus: function(event, status) {
1645         var myAttenderRecord = event.getMyAttenderRecord();
1646         if (myAttenderRecord) {
1647             myAttenderRecord.set('status', status);
1648             event.dirty = true;
1649             event.modified = {};
1650             
1651             var panel = this.getCalendarPanel(this.activeView);
1652             var store = panel.getStore();
1653             
1654             store.replaceRecord(event, event);
1655             
1656             this.onUpdateEvent(event);
1657         }
1658     },
1659     
1660     updateEventActions: function () {
1661         var panel = this.getCalendarPanel(this.activeView);
1662         var selection = panel.getSelectionModel().getSelectedEvents();
1663         
1664         this.actionUpdater.updateActions(selection);
1665         if (this.detailsPanel) {
1666             this.detailsPanel.onDetailsUpdate(panel.getSelectionModel());
1667         }
1668     },
1669     
1670     updateView: function (which) {
1671         Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::updateView(' + which + ')');
1672         var panel = this.getCalendarPanel(which);
1673         var period = panel.getTopToolbar().getPeriod();
1674         
1675         panel.getView().updatePeriod(period);
1676         panel.getStore().load({});
1677         //this.updateMiniCal();
1678     },
1679     
1680     /**
1681      * returns requested CalendarPanel
1682      * 
1683      * @param {String} which
1684      * @return {Tine.Calendar.CalendarPanel}
1685      */
1686     getCalendarPanel: function (which) {
1687         var whichParts = this.getViewParts(which);
1688         which = whichParts.toString();
1689         
1690         if (! this.calendarPanels[which]) {
1691             Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::getCalendarPanel creating new calender panel for view ' + which);
1692             
1693             var store = new Ext.data.JsonStore({
1694                 //autoLoad: true,
1695                 id: 'id',
1696                 fields: Tine.Calendar.Model.Event,
1697                 proxy: Tine.Calendar.backend,
1698                 reader: new Ext.data.JsonReader({}), //Tine.Calendar.backend.getReader(),
1699                 listeners: {
1700                     scope: this,
1701                     'beforeload': this.onStoreBeforeload,
1702                     'beforeloadrecords' : this.onStoreBeforeLoadRecords,
1703                     'load': this.onStoreLoad,
1704                     'loadexception': this.onStoreLoadException
1705                 },
1706                 replaceRecord: function(o, n) {
1707                     var idx = this.indexOf(o);
1708                     this.remove(o);
1709                     this.insert(idx, n);
1710                 }
1711             });
1712             
1713             var tbar = new Tine.Calendar.PagingToolbar({
1714                 view: whichParts.period,
1715                 store: store,
1716                 dtStart: this.startDate,
1717                 listeners: {
1718                     scope: this,
1719                     // NOTE: only render the button once for the toolbars
1720                     //       the buttons will be moved on chageView later
1721                     render: function (tbar) {
1722                         for (var i = 0; i < this.changeViewActions.length; i += 1) {
1723                             if (! this.changeViewActions[i].rendered) {
1724                                 tbar.addButton(this.changeViewActions[i]);
1725                             }
1726                         }
1727                     }
1728                 }
1729             });
1730             
1731             tbar.on('change', this.updateView.createDelegate(this, [which]), this, {buffer: 200});
1732             tbar.on('refresh', this.refresh.createDelegate(this, [true]), this, {buffer: 200});
1733             
1734             if (whichParts.presentation.match(/sheet/i)) {
1735                 var view;
1736                 switch (which) {
1737                     case 'daySheet':
1738                         view = new Tine.Calendar.DaysView({
1739                             store: store,
1740                             startDate: tbar.getPeriod().from,
1741                             numOfDays: 1
1742                         });
1743                         break;
1744                     case 'monthSheet':
1745                         view = new Tine.Calendar.MonthView({
1746                             store: store,
1747                             period: tbar.getPeriod()
1748                         });
1749                         break;
1750                     case 'yearSheet':
1751                         view = new Tine.Calendar.YearView({
1752                             store: store,
1753                             period: tbar.getPeriod()
1754                         });
1755                         break;
1756                     default:
1757                     case 'weekSheet':
1758                         view = new Tine.Calendar.DaysView({
1759                             store: store,
1760                             startDate: tbar.getPeriod().from,
1761                             numOfDays: 7
1762                         });
1763                         break;
1764                 }
1765                 
1766                 view.on('changeView', this.changeView, this);
1767                 view.on('changePeriod', function (period) {
1768                     this.startDate = period.from;
1769                     this.startDates[which] = this.startDate.clone();
1770                     this.updateMiniCal();
1771                 }, this);
1772                 
1773                 // quick add/update actions
1774                 view.on('addEvent', this.onAddEvent, this);
1775                 view.on('updateEvent', this.onUpdateEvent, this);
1776             
1777                 this.calendarPanels[which] = new Tine.Calendar.CalendarPanel({
1778                     tbar: tbar,
1779                     view: view,
1780                     mainScreen: this
1781                 });
1782             } else if (whichParts.presentation.match(/grid/i)) {
1783                 this.calendarPanels[which] = new Tine.Calendar.GridView({
1784                     tbar: tbar,
1785                     store: store
1786                 });
1787             }
1788             
1789             this.calendarPanels[which].on('dblclick', this.onEditInNewWindow.createDelegate(this, ["edit"]));
1790             this.calendarPanels[which].on('contextmenu', this.onContextMenu, this);
1791             this.calendarPanels[which].getSelectionModel().on('selectionchange', this.updateEventActions, this);
1792             this.calendarPanels[which].on('keydown', this.onKeyDown, this);
1793             
1794             this.calendarPanels[which].relayEvents(this, ['show', 'beforehide']);
1795         }
1796         
1797         return this.calendarPanels[which];
1798     },
1799     
1800     updateMiniCal: function () {
1801         var miniCal = Ext.getCmp('cal-mainscreen-minical');
1802         var weekNumbers = null;
1803         var period = this.getCalendarPanel(this.activeView).getView().getPeriod();
1804         
1805         switch (this.activeView) 
1806         {
1807         case 'week' :
1808             weekNumbers = [period.from.add(Date.DAY, 1).getWeekOfYear()];
1809             break;
1810         case 'month' :
1811             weekNumbers = [];
1812             var startWeek = period.from.add(Date.DAY, 1).getWeekOfYear();
1813             var numWeeks = Math.round((period.until.getTime() - period.from.getTime()) / Date.msWEEK);
1814             for (var i = 0; i < numWeeks; i += 1) {
1815                 weekNumbers.push(startWeek + i);
1816             }
1817             break;
1818         }
1819         miniCal.update(this.startDate, true, weekNumbers);
1820     }
1821 });