/* * Tine 2.0 * * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3 * @author Cornelius Weiss * @copyright Copyright (c) 2017 Metaways Infosystems GmbH (http://www.metaways.de) */ Ext.ns('Tine.Calendar'); /** * @namespace Tine.Calendar * @class Tine.Calendar.TimelinePanel * @extends Ext.Panel * * Calendar timeline view * * @author Cornelius Weiss * @constructor * @param {Object} config */ Tine.Calendar.TimelinePanel = Ext.extend(Ext.Panel, { /** * @cfg {Object} period * period to display */ period: null, /** * @cfg {Ext.data.Store} store */ store: null, /** * @cfg {String} viewType */ viewType: 'month', /** * @cfg {Number} labelWidth * width of left label column */ labelWidth: 150, /** * @cfg {String} dayFormatString * i18n._('{0}, the {1}. of {2}') */ dayFormatString: '{0}, the {1}. of {2}', /** * @property {Number} scalingFactor * percentage per millisecond */ scalingFactor: null, groupingMetadataCache: null, layout: 'vbox', border: false, layoutConfig: { align : 'stretch', pack : 'start' }, initComponent: function() { this.app = Tine.Tinebase.appMgr.get('Calendar'); this.dayFormatString = this.app.i18n._hidden(this.dayFormatString); this.groupingMetadataCache = {}; this.labels = []; this.items = [{ ref: 'headerRow', height: 24, layout: 'hbox', border: false, xtype: 'container', layoutConfig: { align : 'stretch', pack : 'start' }, items: [{ ref: '../periodLabel', cls: 'cal-timelinepanel-header-label', width: this.labelWidth, border: false, xtype: 'container' }, { ref: '../timelineHeading', flex: 1, layout: 'fit', border: false, xtype: 'container' }] }, { ref: 'vScroller', cls: 'cal-timelinepanel-vscroller', flex: 1, layout: 'hbox', border: false, xtype: 'container', layoutConfig: { align : 'stretch', pack : 'start' }, items: [{ ref: '../timelineLabels', cls: 'cal-timelinepanel-yscroller-labels', width: this.labelWidth, border: false, xtype: 'container', autoHeight: true }, { ref: '../timelines', cls: 'cal-timelinepanel-yscroller-timelines', flex: 1, border: false, xtype: 'container', autoHeight: true }] }]; this.groupCollection = new Tine.Tinebase.data.GroupedStoreCollection({ store: this.store, group: this.groupingFn.createDelegate(this), groupOnLoad: false, listeners: { scope: this, add: this.onGroupAdd, remove: this.onGroupRemove } }); this.store.on('beforeload', this.onStoreBeforeLoad, this); this.store.on('load', this.onStoreLoad, this); this.initTemplates(); if (! this.selModel) { this.selModel = this.selModel || new Tine.Calendar.EventSelectionModel(); } Tine.Calendar.TimelinePanel.superclass.initComponent.apply(this, arguments); }, onStoreBeforeLoad: function(store, options) { this.resolveGroupingItemsPromise = Promise.resolve(); // resolve members for memberOf filters const attendeeFilter = Tine.Calendar.AttendeeFilterGrid.prototype.extractFilterValue(options.params.filter); const memberIds = _.reduce(attendeeFilter, (memberIds, attendee) => { return memberIds.concat(memberIds, _.get(attendee, 'user_type') === 'memberOf' ? _.get(attendee, 'user_id.members', []) : []); }, []); if (memberIds.length) { this.resolveGroupingItemsPromise = Tine.Addressbook.searchContacts([{ field: 'id', operator: 'in', value: _.compact(_.uniq(memberIds)) }]); } }, onStoreLoad: async function(store, records, options) { this.groupingMetadataCache = {}; var fixedGroups = [], attendeeFilterValue = Tine.Calendar.AttendeeFilterGrid.prototype.extractFilterValue(options.params.filter), attendeeStore = attendeeFilterValue ? Tine.Calendar.Model.Attender.getAttendeeStore(attendeeFilterValue) : null; options.keepLoadMask = true; const members = await this.resolveGroupingItemsPromise; if (attendeeStore) { attendeeStore.each(async (attendee) => { if (attendee.get('user_type') === 'memberOf') { // add each list member to fixedGroups const Attender = Tine.Calendar.Model.Attender; _.each(_.get(attendee, 'data.user_id.members', []), (memberId) => { const memberContact = _.find(_.get(members, 'results', {}), {id: memberId}); if (memberContact) { const memberAttendee = new Attender(Attender.getDefaultData({user_type: 'user', user_id: memberContact})); const groupName = memberAttendee.getCompoundId(); fixedGroups.push(groupName); this.groupingMetadataCache[groupName] = memberAttendee; } }); } else { var groupName = attendee.getCompoundId(); fixedGroups.push(groupName); this.groupingMetadataCache[groupName] = attendee; } }); } this.groupCollection.setFixedGroups(_.uniq(fixedGroups)); this.mainScreen.loadMask.hide(); }, groupingFn: function(event) { var groups = [], attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(event.get('attendee')); attendeeStore.each(function(attendee) { var groupName = this.getAttendeeType(attendee) + '-' + attendee.getUserId(); if (! this.groupingMetadataCache.hasOwnProperty(groupName)) { this.groupingMetadataCache[groupName] = attendee; } groups.push(groupName); }, this); return groups; }, getAttendeeType: function(attendee) { var attendeeType = attendee.get('user_type'); return attendeeType === 'groupmember' ? 'user' : attendeeType; }, onGroupAdd: function(idx, groupStore, groupName) { var _ = window.lodash, attendee = this.groupingMetadataCache[groupName], name = Tine.Calendar.AttendeeGridPanel.prototype.renderAttenderName.call(Tine.Calendar.AttendeeGridPanel.prototype, attendee.get('user_id'), false, attendee), type = this.getAttendeeType(attendee); var label = new Tine.Calendar.TimelineLabel({ groupName: groupName, label: name, iconCls: type === 'user' ? 'tine-grid-row-action-icon renderer_typeAccountIcon' : attendee.getIconCls(), groupSortOrder: Tine.Calendar.Model.Attender.getSortOrder(type) }); this.labels.push(label); this.labels = _.sortBy(this.labels, ['groupSortOrder', 'label']); var idx = this.labels.indexOf(label); this.timelineLabels.insert(idx, label); this.timelineLabels.doLayout(); var view = new Tine.Calendar.TimelineView({ groupName: groupName, store: groupStore, period: this.period, label: label }); this.timelines.insert(idx, view); this.timelines.doLayout(); this.relayEvents(view, ['click', 'dblclick', 'contextmenu']); view.getSelectionModel().on('selectionchange', this.onSelectionChange.createDelegate(this, [view])); }, onGroupRemove: function(groupStore, groupName) { this.timelineLabels.remove(this.timelineLabels.find('groupName', groupName)[0]); this.timelines.remove(this.timelines.find('groupName', groupName)[0]); }, getSelectionModel: function() { return this.selModel; }, onSelectionChange: function(view) { if (this.selectionChangeLocked) return; this.selectionChangeLocked = true; view = Ext.isString(view) ? this.timelines.items.get(view) : view; this.activeView = view; // clear selections from other views this.timelines.items.each(function(v) { if (v !== view) { v.getSelectionModel().clearSelections(true); } }, this); var selections = view.getSelectionModel().getSelectedEvents(); this.getSelectionModel().clearSelections(); Ext.each(selections, function(viewEvent) { var event = this.store.getById(viewEvent.id); // @TODO: have own (grouping) ui if (event) { event.ui = viewEvent.ui; } this.getSelectionModel().select(event, true); }, this); this.selectionChangeLocked = false; }, getPeriod: function() { return this.period; }, updatePeriod: function(period) { this.period = period; var tbar = this.getTopToolbar(); if (tbar) { tbar.periodPicker.update(period.from); this.period = tbar.periodPicker.getPeriod(); this.viewType = tbar.view; } // @TODO do this on period change only this.timelines.items.each(function(view) { view.updatePeriod(this.period); }, this); this.onLayout(); }, onLayout: function() { Tine.Calendar.TimelinePanel.superclass.onLayout.apply(this, arguments); // update header & scaling var msTotal = this.period.until.getTime() - this.period.from.getTime(); this.scalingFactor = 100 / msTotal; // this.periodLabel.update(this.period.from.format()) this.setHeaders(); }, setHeaders: function() { var items = [], item = null, itemStart = this.period.from, periodEndTs = this.period.until.getTime(), unit = this.viewType == 'day' ? Date.HOUR : Date.DAY, formatString = this.viewType == 'day' ? 'G' : 'd', itemEnd = itemStart.add(unit, 1); if (this.viewType != 'day') { itemEnd.setHours(0); } itemEnd.setMinutes(0); itemEnd.setSeconds(0); itemEnd.setMilliseconds(0); while (itemEnd <= this.period.until) { item = { header: itemStart.format(formatString), width: this.scalingFactor * (Math.min(periodEndTs, itemEnd.getTime()) - itemStart.getTime()) }; item.headerLong = this.viewType == 'day' ? item.header : String.format(this.dayFormatString, itemStart.format('l'), itemStart.format('j'), itemStart.format('F')); items.push(item); itemStart = itemEnd; itemEnd = itemStart.add(unit, 1); } this.timelineHeading.update(this.templates.header.apply(items)); }, onResize: function(w, h) { Tine.Calendar.TimelinePanel.superclass.onResize.apply(this, arguments); }, getTargetEvent: function(e) { var viewEl = e.getTarget('.cal-timeline-view'), id = viewEl ? viewEl.id : undefined, view = this.timelines.items.get(id); return view.getTargetEvent(e); }, getTargetDateTime: Ext.emptyFn, print: function() { Ext.ux.Printer.print(this.grid); }, getView: function() { return this; }, getStore: function() { return this.store; }, initTemplates: function() { var ts = this.templates || {}; ts.header = new Ext.XTemplate( '
', '', '
cal-timelinepanel-header-long" style="width: {width}%;">', '{header}', '{headerLong}', '
', '
', '
' ); for(var k in ts){ var t = ts[k]; if(t && typeof t.compile == 'function' && !t.compiled){ t.disableFormats = true; t.compile(); } } this.templates = ts; } });