/* * Axelor Business Solutions * * Copyright (C) 2005-2019 Axelor (). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ (function() { /* global d3: true */ "use strict"; var ui = angular.module('axelor.ui'); ui.controller('CalendarViewCtrl', CalendarViewCtrl); CalendarViewCtrl.$inject = ['$scope', '$element']; function CalendarViewCtrl($scope, $element) { ui.DSViewCtrl('calendar', $scope, $element); var ds = $scope._dataSource; var view = {}; var colors = {}; var initialized = false; $scope.onShow = function(viewPromise) { if (initialized) { return $scope.refresh(); } viewPromise.then(function(){ var schema = $scope.schema; initialized = true; view = { start: schema.eventStart, stop: schema.eventStop, length: parseInt(schema.eventLength) || 0, color: schema.colorBy, title: schema.items[0].name }; $scope._viewResolver.resolve(schema, $element); $scope.updateRoute(); }); }; $scope.isAgenda = function() { var field = this.fields[view.start]; return field && field.type === "datetime"; }; var d3_colors = d3.scale.category10().range().concat( d3.scale.category20().range()); function nextColor(n) { if (n === undefined || n < 0 || n >= d3_colors.length) { n = _.random(0, d3_colors.length); } var c = d3.rgb(d3_colors[n]); return { bg: "" + c, fg: "" + c.brighter(99), bc: "" + c.darker(0.9) }; } $scope.fetchItems = function(start, end, callback) { var fields = _.pluck(this.fields, 'name'); var criteria = { operator: "and", criteria: [{ fieldName: view.start, operator: ">=", value: start }, { fieldName: view.start, operator: "<=", value: end }] }; // make sure to include items whose end date falls in current range if (view.stop) { criteria = { operator: "or", criteria: [criteria, { operator: "and", criteria: [{ fieldName: view.stop, operator: ">=", value: start }, { fieldName: view.stop, operator: "<=", value: end }] }] }; } // consider stored filter if (ds._filter) { if (ds._filter.criteria) { criteria = { operator: "and", criteria: [criteria].concat(ds._filter.criteria) }; } if (_.size(ds._filter._domains) > 0) { criteria._domains = ds._filter._domains; } } var opts = { fields: fields, filter: criteria, domain: this._domain, context: this._context, store: false, limit: -1 }; ds.search(opts).success(function(records) { var items = _.clone(records); items.sort(function (x, y) { return x.id - y.id; }); updateColors(items, true); callback(records); }); }; function updateColors(records, reset) { var colorBy = view.color; var colorField = $scope.fields[colorBy]; if (!colorField) { return colors; } if (reset) { colors = {}; } _.each(records, function(record) { var item = record[colorBy]; if (item === null || item === undefined) { return; } var key = $scope.getColorKey(record, item); var title = colorField.targetName ? item[colorField.targetName] : item; if (colorField.selectionList) { var select = _.find(colorField.selectionList, function (select) { return ("" + select.value) === ("" + title); }); if (select) { title = select.title; } } if (!colors[key]) { colors[key] = { item: item, title: title || _t('Unknown'), color: nextColor(_.size(colors)) }; } record.$colorKey = key; }); return colors; } $scope.getColors = function() { return colors; }; $scope.getColor = function(record) { var key = this.getColorKey(record); if (key && !colors[key]) { updateColors([record], false); } if (colors[key]) { return colors[key].color; } return nextColor(0); }; $scope.getColorKey = function(record, key) { if (key) { return "" + (key.id || key); } if (record) { return this.getColorKey(null, record[view.color]); } return null; }; $scope.getEventInfo = function(record) { var info = {}, value; value = record[view.start]; info.start = value ? moment(value) : moment(); value = record[view.stop]; info.end = value ? moment(value) : moment(info.start).add(view.length || 1, "hours"); var title = this.fields[view.title]; var titleText = null; if (title) { value = record[title.name]; if (title.targetName) { value = value[title.targetName]; } else if (title.selectionList) { var select = _.find(title.selectionList, function (select) { return ("" + select.value) === ("" + value); }); if (select) { titleText = select.title; } } titleText = titleText || value || _t('Unknown'); } info.title = ("" + titleText); info.allDay = isAllDay(info); info.className = info.allDay ? "calendar-event-allDay" : "calendar-event-day"; return info; }; function isAllDay(event) { if($scope.fields[view.start] && $scope.fields[view.start].type === 'date') { return true; } var start = moment(event.start); var end = moment(event.end); if (start.format("HH:mm") !== "00:00") { return false; } return !event.end || end.format("HH:mm") === "00:00"; } $scope.onEventChange = function(event, delta) { var record = _.clone(event.record); var start = event.start; var end = event.end; if (isAllDay(event)) { start = start.clone().startOf("day").local(); end = (end || start).clone().startOf("day").local(); } record[view.start] = start; record[view.stop] = end; $scope.record = record; function reset() { $scope.record = null; } function doSave() { return ds.save(record).success(function(res){ return $scope.refresh(); }); } var handler = $scope.onChangeHandler; if (handler) { var promise = handler.onChange().then(function () { return doSave(); }); promise.success = function(fn) { promise.then(fn); return promise; }; promise.error = function (fn) { promise.then(null, fn); return promise; }; return promise; } return doSave(); }; $scope.removeEvent = function(event, callback) { ds.remove(event.record).success(callback); }; $scope.select = function() { }; $scope.refresh = function() { }; $scope.getRouteOptions = function() { var args = [], query = {}; return { mode: 'calendar', args: args, query: query }; }; $scope.setRouteOptions = function(options) { var opts = options || {}; if (opts.mode === "calendar") { return $scope.updateRoute(); } var params = $scope._viewParams; if (params.viewType !== "calendar") { return $scope.show(); } }; $scope.canNext = function() { return true; }; $scope.canPrev = function() { return true; }; } angular.module('axelor.ui').directive('uiViewCalendar', ['ViewService', 'ActionService', function(ViewService, ActionService) { function link(scope, element, attrs, controller) { var main = element.children('.calendar-main'); var mini = element.find('.calendar-mini'); var legend = element.find('.calendar-legend'); var ctx = (scope._viewParams.context||{}); var params = (scope._viewParams.params||{}); var schema = scope.schema; var mode = ctx.calendarMode || params.calendarMode || schema.mode || "month"; var date = ctx.calendarDate || params.calendarDate; var editable = schema.editable === undefined ? true : schema.editable; var calRange = {}; var RecordManager = (function () { var records = [], current = []; function add(record) { if (!record || _.isArray(record)) { records = record || []; return filter(); } var found = _.findWhere(records, { id: record.id }); if (!found) { found = record; records.push(record); } if (found !== record) { _.extend(found, record); } return filter(); } function remove(record) { records = _.filter(records, function (item) { return record.id !== item.id; }); return filter(); } function filter() { var selected = []; _.each(scope.getColors(), function (color) { if (color.checked) { selected.push(scope.getColorKey(null, color.item)); } }); main.fullCalendar('removeEventSource', current); current = records; if (selected.length) { current = _.filter(records, function(record) { return _.contains(selected, record.$colorKey); }); } main.fullCalendar('addEventSource', current); adjustSize(); } function events(start, end, timezone, callback) { calRange.start = start; calRange.end = end; scope._viewPromise.then(function(){ scope.fetchItems(start, end, function(items) { callback([]); add(items); }); }); } return { add: add, remove: remove, filter: filter, events: events }; }()); if (date) { date = moment(date).toDate(); } mini.datepicker({ showOtherMonths: true, selectOtherMonths: true, onSelect: function(dateStr) { main.fullCalendar('gotoDate', mini.datepicker('getDate')); } }); if (date) { mini.datepicker('setDate', date); } var lang = axelor.config["user.lang"] || 'en'; var options = { header: false, timeFormat: 'h(:mm)t', axisFormat: 'h(:mm)t', timezone: 'local', lang: lang, editable: editable, selectable: editable, selectHelper: editable, select: function(start, end) { var event = { start: start, end: end }; // all day if (!start.hasTime() && !end.hasTime()) { event.start = start.clone().startOf("day").local(); event.end = end.clone().startOf("day").local(); } scope.$applyAsync(function(){ scope.showEditor(event); }); main.fullCalendar('unselect'); }, defaultDate: date, events: RecordManager, eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) { hideBubble(); scope.onEventChange(event, delta).error(function(){ revertFunc(); }); }, eventResize: function( event, delta, revertFunc, jsEvent, ui, view ) { scope.onEventChange(event, delta).error(function(){ revertFunc(); }); }, eventClick: function(event, jsEvent, view) { showBubble(event, jsEvent.target); }, eventDataTransform: function(record) { return updateEvent(null, record); }, viewDisplay: function(view) { hideBubble(); mini.datepicker('setDate', main.fullCalendar('getDate')); }, allDayText: _t('All Day') }; if (lang.indexOf('fr') === 0) { _.extend(options, { timeFormat: 'H:mm', axisFormat: 'H:mm', firstDay: 1, views: { week: { titleFormat: 'D MMM YYYY', columnFormat: 'ddd DD/MM' }, day: { titleFormat: 'D MMM YYYY' } } }); } main.fullCalendar(options); var editor = null; var bubble = null; function hideBubble() { if (bubble) { bubble.popover('destroy'); bubble = null; } } function showBubble(event, elem) { hideBubble(); bubble = $(elem).popover({ html: true, title: "" + event.title + "", placement: "top", container: 'body', content: function() { var html = $("
").addClass("calendar-bubble-content"); var start = event.start; var end = event.end && event.allDay ? moment(event.end).add(-1, 'second') : event.end; var singleDay = (event.allDay || !scope.isAgenda()) && (!end || moment(start).isSame(end, 'day')); var dateFormat = !scope.isAgenda() || event.allDay ? "ddd D MMM" : "ddd D MMM HH:mm"; $("").text(moment(start).format(dateFormat)).appendTo(html); if (schema.eventStop && end && !singleDay) { $(" - ").appendTo(html); $("").text(moment(end).format(dateFormat)).appendTo(html); } $("
").appendTo(html); if (scope.isEditable()) { $('').text(_t("Delete")) .appendTo(html) .click(function(e){ hideBubble(); scope.$applyAsync(function(){ scope.removeEvent(event, function() { RecordManager.remove(event.record); }); }); }); } $('') .append(_t("Edit event")).append(" ยป") .appendTo(html) .click(function(e){ hideBubble(); scope.$applyAsync(function(){ scope.showEditor(event); }); }); return html; } }); bubble.popover('show'); } $("body").on("mousedown", function(e){ var elem = $(e.target || e.srcElement); if (!bubble || bubble.is(elem) || bubble.has(elem).length) { return; } if (!elem.parents().is(".popover")) { hideBubble(); } }); function updateEvent(event, record) { if (!event || !event.id) { var color = scope.getColor(record); event = { id: record.id, record: record, backgroundColor: color.bg, borderColor: color.bc, textColor: color.fg }; } else { _.extend(event.record, record); } event = _.extend(event, scope.getEventInfo(record)); return event; } scope.editorCanSave = true; scope.showEditor = function(event) { var view = this.schema; var record = _.extend({}, event.record); record[view.eventStart] = event.start; record[view.eventStop] = event.end; if (!editor) { editor = ViewService.compile('
')(scope.$new()); editor.data('$target', element); } var popup = editor.isolateScope(); popup.show(record, function(result) { RecordManager.add(result); }); popup.waitForActions(function() { if (!record || !record.id) { popup.$broadcast("on:new"); } else { popup.setEditable(scope.isEditable()); } }); }; scope.isEditable = function() { return editable; }; scope.refresh = function(record) { if (calRange.start && calRange.end) { return RecordManager.events(calRange.start, calRange.end, options.timezone, function () {}); } return main.fullCalendar("refetchEvents"); }; scope.filterEvents = function() { RecordManager.filter(); }; scope.pagerText = function() { return main.fullCalendar("getView").title; }; scope.isMode = function(name) { return mode === name; }; scope.onMode = function(name) { mode = name; if (name === "week") { name = scope.isAgenda() ? "agendaWeek" : "basicWeek"; } if (name === "day") { name = scope.isAgenda() ? "agendaDay" : "basicDay"; } main.fullCalendar("changeView", name); }; scope.onRefresh = function () { scope.refresh(); }; scope.onNext = function() { main.fullCalendar('next'); }; scope.onPrev = function() { main.fullCalendar('prev'); }; scope.onToday = function() { main.fullCalendar('today'); }; scope.onChangeHandler = null; if (schema.onChange) { scope.onChangeHandler = ActionService.handler(scope, element, { action: schema.onChange }); } function adjustSize() { if (main.is(':hidden')) { return; } hideBubble(); main.fullCalendar('render'); main.fullCalendar('option', 'height', element.height()); legend.css("max-height", legend.parent().height() - mini.height() - (parseInt(legend.css('marginTop')) || 0) - (parseInt(legend.css('marginBottom')) || 0)); } scope.$onAdjust(adjustSize, 100); scope.$callWhen(function () { return main.is(':visible'); }, function() { element.parents('.view-container:first').css('overflow', 'inherit'); scope.onMode(mode); adjustSize(); }, 100); } return { link: function(scope, element, attrs, controller) { scope._viewPromise.then(function(){ link(scope, element, attrs, controller); }); }, replace: true, template: '
'+ '
'+ '
'+ '
'+ '
'+ ''+ '
'+ '
'+ '' }; }]); })();