1218 lines
39 KiB
JavaScript
1218 lines
39 KiB
JavaScript
define("modules/crm/views/calendar/calendar", ["exports", "view", "moment", "fullcalendar", "helpers/record-modal"], function (_exports, _view, _moment, FullCalendar, _recordModal) {
|
||
"use strict";
|
||
|
||
Object.defineProperty(_exports, "__esModule", {
|
||
value: true
|
||
});
|
||
_exports.default = void 0;
|
||
_view = _interopRequireDefault(_view);
|
||
_moment = _interopRequireDefault(_moment);
|
||
FullCalendar = _interopRequireWildcard(FullCalendar);
|
||
_recordModal = _interopRequireDefault(_recordModal);
|
||
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
||
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
||
/************************************************************************
|
||
* This file is part of EspoCRM.
|
||
*
|
||
* EspoCRM – Open Source CRM application.
|
||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||
* Website: https://www.espocrm.com
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* 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 <https://www.gnu.org/licenses/>.
|
||
*
|
||
* The interactive user interfaces in modified source and object code versions
|
||
* of this program must display Appropriate Legal Notices, as required under
|
||
* Section 5 of the GNU Affero General Public License version 3.
|
||
*
|
||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||
************************************************************************/
|
||
|
||
/** @module modules/crm/views/calendar/calendar */
|
||
|
||
/**
|
||
* @typedef {import('@fullcalendar/core/internal-common.js').EventImpl} EventImpl
|
||
*/
|
||
|
||
/**
|
||
* @typedef {import('@fullcalendar/core/internal-common.d.ts').CalendarOptions} CalendarOptions
|
||
*/
|
||
|
||
/**
|
||
* @typedef {import('@fullcalendar/interaction/index.d.ts').ExtraListenerRefiners} ExtraListenerRefiners
|
||
*/
|
||
|
||
class CalendarView extends _view.default {
|
||
template = 'crm:calendar/calendar';
|
||
eventAttributes = [];
|
||
colors = {};
|
||
|
||
/**
|
||
* @private
|
||
* @type {string[]}
|
||
*/
|
||
allDayScopeList;
|
||
|
||
/**
|
||
* @private
|
||
* @type {string[]}
|
||
*/
|
||
scopeList = ['Meeting', 'Call', 'Task'];
|
||
|
||
/**
|
||
* @private
|
||
* @type {string[]}
|
||
*/
|
||
onlyDateScopeList;
|
||
|
||
/**
|
||
* @private
|
||
* @type {string[]}
|
||
*/
|
||
enabledScopeList;
|
||
header = true;
|
||
modeList = [];
|
||
fullCalendarModeList = ['month', 'agendaWeek', 'agendaDay', 'basicWeek', 'basicDay', 'listWeek'];
|
||
defaultMode = 'agendaWeek';
|
||
slotDuration = 30;
|
||
scrollToNowSlots = 6;
|
||
scrollHour = 6;
|
||
titleFormat = {
|
||
month: 'MMMM YYYY',
|
||
week: 'MMMM YYYY',
|
||
day: 'dddd, MMMM D, YYYY'
|
||
};
|
||
rangeSeparator = ' – ';
|
||
|
||
/** @private */
|
||
fetching = false;
|
||
modeViewMap = {
|
||
month: 'dayGridMonth',
|
||
agendaWeek: 'timeGridWeek',
|
||
agendaDay: 'timeGridDay',
|
||
basicWeek: 'dayGridWeek',
|
||
basicDay: 'dayGridDay',
|
||
listWeek: 'listWeek'
|
||
};
|
||
extendedProps = ['scope', 'recordId', 'dateStart', 'dateEnd', 'dateStartDate', 'dateEndDate', 'status', 'originalColor', 'duration', 'allDayCopy'];
|
||
|
||
/** @type {FullCalendar.Calendar} */
|
||
calendar;
|
||
events = {
|
||
/** @this CalendarView */
|
||
'click button[data-action="prev"]': function () {
|
||
this.actionPrevious();
|
||
},
|
||
/** @this CalendarView */
|
||
'click button[data-action="next"]': function () {
|
||
this.actionNext();
|
||
},
|
||
/** @this CalendarView */
|
||
'click button[data-action="today"]': function () {
|
||
this.actionToday();
|
||
},
|
||
/** @this CalendarView */
|
||
'click [data-action="mode"]': function (e) {
|
||
const mode = $(e.currentTarget).data('mode');
|
||
this.selectMode(mode);
|
||
},
|
||
/** @this CalendarView */
|
||
'click [data-action="refresh"]': function () {
|
||
this.actionRefresh();
|
||
},
|
||
/** @this CalendarView */
|
||
'click [data-action="toggleScopeFilter"]': function (e) {
|
||
const $target = $(e.currentTarget);
|
||
const filterName = $target.data('name');
|
||
const $check = $target.find('.filter-check-icon');
|
||
if ($check.hasClass('hidden')) {
|
||
$check.removeClass('hidden');
|
||
} else {
|
||
$check.addClass('hidden');
|
||
}
|
||
e.stopPropagation(e);
|
||
this.toggleScopeFilter(filterName);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @param {{
|
||
* userId?: string,
|
||
* userName?: string|null,
|
||
* mode?: string|null,
|
||
* date?: string|null,
|
||
* scrollToNowSlots?: boolean,
|
||
* $container?: JQuery,
|
||
* suppressLoadingAlert?: boolean,
|
||
* slotDuration?: number,
|
||
* scrollHour?: number,
|
||
* teamIdList?: string[],
|
||
* containerSelector?: string,
|
||
* height?: number,
|
||
* enabledScopeList?: string[],
|
||
* header?: boolean,
|
||
* onSave?: function(),
|
||
* }} options
|
||
*/
|
||
constructor(options) {
|
||
super(options);
|
||
this.options = options;
|
||
}
|
||
data() {
|
||
return {
|
||
mode: this.mode,
|
||
header: this.header,
|
||
isCustomViewAvailable: this.isCustomViewAvailable,
|
||
isCustomView: this.isCustomView,
|
||
todayLabel: this.translate('Today', 'labels', 'Calendar'),
|
||
todayLabelShort: this.translate('Today', 'labels', 'Calendar').slice(0, 2)
|
||
};
|
||
}
|
||
setup() {
|
||
this.wait(Espo.loader.requirePromise('lib!@fullcalendar/moment'));
|
||
this.wait(Espo.loader.requirePromise('lib!@fullcalendar/moment-timezone'));
|
||
this.suppressLoadingAlert = this.options.suppressLoadingAlert;
|
||
this.date = this.options.date || null;
|
||
this.mode = this.options.mode || this.defaultMode;
|
||
this.header = 'header' in this.options ? this.options.header : this.header;
|
||
this.scrollToNowSlots = this.options.scrollToNowSlots !== undefined ? this.options.scrollToNowSlots : this.scrollToNowSlots;
|
||
this.setupMode();
|
||
this.$container = this.options.$container;
|
||
this.colors = Espo.Utils.clone(this.getMetadata().get('clientDefs.Calendar.colors') || this.colors);
|
||
this.modeList = this.getMetadata().get('clientDefs.Calendar.modeList') || this.modeList;
|
||
this.scopeList = this.getConfig().get('calendarEntityList') || Espo.Utils.clone(this.scopeList);
|
||
this.allDayScopeList = this.getMetadata().get('clientDefs.Calendar.allDayScopeList') ?? [];
|
||
this.scopeList.forEach(scope => {
|
||
if (this.getMetadata().get(`scopes.${scope}.calendarOneDay`) && !this.allDayScopeList.includes(scope)) {
|
||
this.allDayScopeList.push(scope);
|
||
}
|
||
});
|
||
this.onlyDateScopeList = this.scopeList.filter(scope => {
|
||
return this.getMetadata().get(`entityDefs.${scope}.fields.dateStart.type`) === 'date';
|
||
});
|
||
this.slotDuration = this.options.slotDuration || this.getPreferences().get('calendarSlotDuration') || this.getMetadata().get('clientDefs.Calendar.slotDuration') || this.slotDuration;
|
||
this.setupScrollHour();
|
||
this.colors = {
|
||
...this.colors,
|
||
...this.getHelper().themeManager.getParam('calendarColors')
|
||
};
|
||
this.isCustomViewAvailable = this.getAcl().getPermissionLevel('userCalendar') !== 'no';
|
||
if (this.options.userId) {
|
||
this.isCustomViewAvailable = false;
|
||
}
|
||
const scopeList = [];
|
||
this.scopeList.forEach(scope => {
|
||
if (this.getAcl().check(scope)) {
|
||
scopeList.push(scope);
|
||
}
|
||
});
|
||
this.scopeList = scopeList;
|
||
if (this.header) {
|
||
this.enabledScopeList = this.getStoredEnabledScopeList() || Espo.Utils.clone(this.scopeList);
|
||
} else {
|
||
this.enabledScopeList = this.options.enabledScopeList || Espo.Utils.clone(this.scopeList);
|
||
}
|
||
if (Object.prototype.toString.call(this.enabledScopeList) !== '[object Array]') {
|
||
this.enabledScopeList = [];
|
||
}
|
||
this.enabledScopeList.forEach(item => {
|
||
const color = this.getMetadata().get(['clientDefs', item, 'color']);
|
||
if (color) {
|
||
this.colors[item] = color;
|
||
}
|
||
});
|
||
if (this.header) {
|
||
this.createView('modeButtons', 'crm:views/calendar/mode-buttons', {
|
||
selector: '.mode-buttons',
|
||
isCustomViewAvailable: this.isCustomViewAvailable,
|
||
modeList: this.modeList,
|
||
scopeList: this.scopeList,
|
||
mode: this.mode
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
setupScrollHour() {
|
||
if (this.options.scrollHour !== undefined) {
|
||
this.scrollHour = this.options.scrollHour;
|
||
return;
|
||
}
|
||
const scrollHour = this.getPreferences().get('calendarScrollHour');
|
||
if (scrollHour !== null) {
|
||
this.scrollHour = scrollHour;
|
||
return;
|
||
}
|
||
if (this.slotDuration < 30) {
|
||
this.scrollHour = 8;
|
||
}
|
||
}
|
||
setupMode() {
|
||
this.viewMode = this.mode;
|
||
this.isCustomView = false;
|
||
this.teamIdList = this.options.teamIdList || null;
|
||
if (this.teamIdList && !this.teamIdList.length) {
|
||
this.teamIdList = null;
|
||
}
|
||
if (~this.mode.indexOf('view-')) {
|
||
this.viewId = this.mode.slice(5);
|
||
this.isCustomView = true;
|
||
const calendarViewDataList = this.getPreferences().get('calendarViewDataList') || [];
|
||
calendarViewDataList.forEach(item => {
|
||
if (item.id === this.viewId) {
|
||
this.viewMode = item.mode;
|
||
this.teamIdList = item.teamIdList;
|
||
this.viewName = item.name;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @return {boolean}
|
||
*/
|
||
isAgendaMode() {
|
||
return this.mode.indexOf('agenda') === 0;
|
||
}
|
||
|
||
/**
|
||
* @param {string} mode
|
||
*/
|
||
selectMode(mode) {
|
||
if (this.fullCalendarModeList.includes(mode) || mode.indexOf('view-') === 0) {
|
||
const previousMode = this.mode;
|
||
if (mode.indexOf('view-') === 0 || mode.indexOf('view-') !== 0 && previousMode.indexOf('view-') === 0) {
|
||
this.trigger('change:mode', mode, true);
|
||
return;
|
||
}
|
||
this.mode = mode;
|
||
this.setupMode();
|
||
if (this.isCustomView) {
|
||
this.$el.find('button[data-action="editCustomView"]').removeClass('hidden');
|
||
} else {
|
||
this.$el.find('button[data-action="editCustomView"]').addClass('hidden');
|
||
}
|
||
this.$el.find('[data-action="mode"]').removeClass('active');
|
||
this.$el.find('[data-mode="' + mode + '"]').addClass('active');
|
||
this.calendar.changeView(this.modeViewMap[this.viewMode]);
|
||
const toAgenda = previousMode.indexOf('agenda') !== 0 && mode.indexOf('agenda') === 0;
|
||
const fromAgenda = previousMode.indexOf('agenda') === 0 && mode.indexOf('agenda') !== 0;
|
||
if (toAgenda && !this.fetching || fromAgenda && !this.fetching) {
|
||
this.calendar.refetchEvents();
|
||
}
|
||
this.updateDate();
|
||
if (this.hasView('modeButtons')) {
|
||
this.getModeButtonsView().mode = mode;
|
||
this.getModeButtonsView().reRender();
|
||
}
|
||
}
|
||
this.trigger('change:mode', mode);
|
||
}
|
||
|
||
/**
|
||
* @return {import('./mode-buttons').default}
|
||
*/
|
||
getModeButtonsView() {
|
||
return this.getView('modeButtons');
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {string} name
|
||
*/
|
||
toggleScopeFilter(name) {
|
||
const index = this.enabledScopeList.indexOf(name);
|
||
if (!~index) {
|
||
this.enabledScopeList.push(name);
|
||
} else {
|
||
this.enabledScopeList.splice(index, 1);
|
||
}
|
||
this.storeEnabledScopeList(this.enabledScopeList);
|
||
this.calendar.refetchEvents();
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @return {string[]|null}
|
||
*/
|
||
getStoredEnabledScopeList() {
|
||
const key = 'calendarEnabledScopeList';
|
||
return this.getStorage().get('state', key) || null;
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {string[]} enabledScopeList
|
||
*/
|
||
storeEnabledScopeList(enabledScopeList) {
|
||
const key = 'calendarEnabledScopeList';
|
||
this.getStorage().set('state', key, enabledScopeList);
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
updateDate() {
|
||
if (!this.header) {
|
||
return;
|
||
}
|
||
if (this.isToday()) {
|
||
this.$el.find('button[data-action="today"]').addClass('active');
|
||
} else {
|
||
this.$el.find('button[data-action="today"]').removeClass('active');
|
||
}
|
||
const title = this.getTitle();
|
||
this.$el.find('.date-title h4 span').text(title);
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @return {boolean}
|
||
*/
|
||
isToday() {
|
||
const view = this.calendar.view;
|
||
const todayUnix = (0, _moment.default)().unix();
|
||
const startUnix = (0, _moment.default)(view.activeStart).unix();
|
||
const endUnix = (0, _moment.default)(view.activeEnd).unix();
|
||
return startUnix <= todayUnix && todayUnix < endUnix;
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @return {string}
|
||
*/
|
||
getTitle() {
|
||
const view = this.calendar.view;
|
||
const map = {
|
||
timeGridWeek: 'week',
|
||
timeGridDay: 'day',
|
||
dayGridWeek: 'week',
|
||
dayGridDay: 'day',
|
||
dayGridMonth: 'month'
|
||
};
|
||
const viewName = map[view.type] || view.type;
|
||
let title;
|
||
const format = this.titleFormat[viewName];
|
||
if (viewName === 'week') {
|
||
const start = this.dateToMoment(view.currentStart).format(format);
|
||
const end = this.dateToMoment(view.currentEnd).subtract(1, 'minute').format(format);
|
||
title = start !== end ? start + this.rangeSeparator + end : start;
|
||
} else {
|
||
title = this.dateToMoment(view.currentStart).format(format);
|
||
}
|
||
if (this.options.userId && this.options.userName) {
|
||
title += ' (' + this.options.userName + ')';
|
||
}
|
||
title = this.getHelper().escapeString(title);
|
||
return title;
|
||
}
|
||
|
||
/**
|
||
* @typedef {{
|
||
* recordId,
|
||
* dateStart?: string,
|
||
* originalColor?: string,
|
||
* scope?: string,
|
||
* display: string,
|
||
* id: string,
|
||
* dateEnd?: string,
|
||
* dateStartDate?: ?string,
|
||
* title: string,
|
||
* dateEndDate?: ?string,
|
||
* status?: string,
|
||
* allDay?: boolean,
|
||
* start?: Date,
|
||
* end?: Date,
|
||
* }} module:modules/crm/views/calendar/calendar~FcEvent
|
||
*/
|
||
|
||
/**
|
||
* @private
|
||
* @param {Object.<string, *>} o
|
||
* @return {module:modules/crm/views/calendar/calendar~FcEvent}
|
||
*/
|
||
convertToFcEvent(o) {
|
||
const event = {
|
||
title: o.name || '',
|
||
scope: o.scope,
|
||
id: o.scope + '-' + o.id,
|
||
recordId: o.id,
|
||
dateStart: o.dateStart,
|
||
dateEnd: o.dateEnd,
|
||
dateStartDate: o.dateStartDate,
|
||
dateEndDate: o.dateEndDate,
|
||
status: o.status,
|
||
originalColor: o.color,
|
||
display: 'block'
|
||
};
|
||
if (o.isWorkingRange) {
|
||
event.display = 'inverse-background';
|
||
event.groupId = 'nonWorking';
|
||
event.color = this.colors['bg'];
|
||
}
|
||
if (this.teamIdList) {
|
||
event.userIdList = o.userIdList || [];
|
||
event.userNameMap = o.userNameMap || {};
|
||
event.userIdList = event.userIdList.sort((v1, v2) => {
|
||
return (event.userNameMap[v1] || '').localeCompare(event.userNameMap[v2] || '');
|
||
});
|
||
}
|
||
this.eventAttributes.forEach(attr => {
|
||
event[attr] = o[attr];
|
||
});
|
||
let start;
|
||
let end;
|
||
if (o.dateStart || o.dateStartDate) {
|
||
start = !o.dateStartDate ? this.getDateTime().toMoment(o.dateStart) : this.dateToMoment(o.dateStartDate);
|
||
}
|
||
if (o.dateEnd || o.dateEndDate) {
|
||
end = !o.dateEndDate ? this.getDateTime().toMoment(o.dateEnd) : this.dateToMoment(o.dateEndDate);
|
||
}
|
||
if (end && start) {
|
||
event.duration = end.unix() - start.unix();
|
||
}
|
||
if (start) {
|
||
event.start = start.toISOString(true);
|
||
}
|
||
if (end) {
|
||
event.end = end.toISOString(true);
|
||
}
|
||
event.allDay = false;
|
||
if (!o.isWorkingRange) {
|
||
this.handleAllDay(event);
|
||
this.fillColor(event);
|
||
this.handleStatus(event);
|
||
}
|
||
if (o.isWorkingRange && !this.isAgendaMode()) {
|
||
event.allDay = true;
|
||
}
|
||
return event;
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {string|Date} date
|
||
* @return {moment.Moment}
|
||
*/
|
||
dateToMoment(date) {
|
||
return _moment.default.tz(date, this.getDateTime().getTimeZone());
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {string} scope
|
||
* @return {string[]}
|
||
*/
|
||
getEventTypeCompletedStatusList(scope) {
|
||
return this.getMetadata().get(['scopes', scope, 'completedStatusList']) || [];
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {string} scope
|
||
* @return {string[]}
|
||
*/
|
||
getEventTypeCanceledStatusList(scope) {
|
||
return this.getMetadata().get(['scopes', scope, 'canceledStatusList']) || [];
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {Record} event
|
||
*/
|
||
fillColor(event) {
|
||
let color = this.colors[event.scope];
|
||
if (event.originalColor) {
|
||
color = event.originalColor;
|
||
}
|
||
if (!color) {
|
||
color = this.getColorFromScopeName(event.scope);
|
||
}
|
||
if (color && (this.getEventTypeCompletedStatusList(event.scope).includes(event.status) || this.getEventTypeCanceledStatusList(event.scope).includes(event.status))) {
|
||
color = this.shadeColor(color, 0.4);
|
||
}
|
||
event.color = color;
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {Object} event
|
||
*/
|
||
handleStatus(event) {
|
||
if (this.getEventTypeCanceledStatusList(event.scope).includes(event.status)) {
|
||
event.className = ['event-canceled'];
|
||
} else {
|
||
event.className = [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {string} color
|
||
* @param {number} percent
|
||
* @return {string}
|
||
*/
|
||
shadeColor(color, percent) {
|
||
if (color === 'transparent') {
|
||
return color;
|
||
}
|
||
if (this.getThemeManager().getParam('isDark')) {
|
||
percent *= -1;
|
||
}
|
||
const alpha = color.substring(7);
|
||
const f = parseInt(color.slice(1, 7), 16),
|
||
t = percent < 0 ? 0 : 255,
|
||
p = percent < 0 ? percent * -1 : percent,
|
||
R = f >> 16,
|
||
G = f >> 8 & 0x00FF,
|
||
B = f & 0x0000FF;
|
||
return "#" + (0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G) * 0x100 + (Math.round((t - B) * p) + B)).toString(16).slice(1) + alpha;
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {EventImpl} event
|
||
* @param {boolean} [afterDrop]
|
||
*/
|
||
handleAllDay(event, afterDrop) {
|
||
let start = event.start ? this.dateToMoment(event.start) : null;
|
||
const end = event.end ? this.dateToMoment(event.end) : null;
|
||
if (this.allDayScopeList.includes(event.scope)) {
|
||
event.allDay = event.allDayCopy = true;
|
||
if (!afterDrop && end) {
|
||
start = end.clone();
|
||
if (!event.dateEndDate && end.hours() === 0 && end.minutes() === 0) {
|
||
start.add(-1, 'days');
|
||
}
|
||
}
|
||
if (start.isSame(end)) {
|
||
end.add(1, 'days');
|
||
}
|
||
if (start) {
|
||
event.start = start.toDate();
|
||
}
|
||
if (end) {
|
||
event.end = end.toDate();
|
||
}
|
||
return;
|
||
}
|
||
if (event.dateStartDate && event.dateEndDate) {
|
||
event.allDay = true;
|
||
event.allDayCopy = event.allDay;
|
||
if (!afterDrop && end) {
|
||
end.add(1, 'days');
|
||
}
|
||
if (start) {
|
||
event.start = start.toDate();
|
||
}
|
||
if (end) {
|
||
event.end = end.toDate();
|
||
}
|
||
return;
|
||
}
|
||
if (!start || !end) {
|
||
event.allDay = true;
|
||
if (end) {
|
||
start = end;
|
||
}
|
||
} else if (start.format('YYYY-DD') !== end.format('YYYY-DD') && end.unix() - start.unix() >= 86400) {
|
||
event.allDay = true;
|
||
|
||
//if (!notInitial) {
|
||
if (end.hours() !== 0 || end.minutes() !== 0) {
|
||
end.add(1, 'days');
|
||
}
|
||
//}
|
||
} else {
|
||
event.allDay = false;
|
||
}
|
||
event.allDayCopy = event.allDay;
|
||
if (start) {
|
||
event.start = start.toDate();
|
||
}
|
||
if (end) {
|
||
event.end = end.toDate();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {Record[]} list
|
||
* @return {Record[]}
|
||
*/
|
||
convertToFcEvents(list) {
|
||
this.now = _moment.default.tz(this.getDateTime().getTimeZone());
|
||
const events = [];
|
||
list.forEach(o => {
|
||
const event = this.convertToFcEvent(o);
|
||
events.push(event);
|
||
});
|
||
return events;
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {string} date
|
||
* @return {string}
|
||
*/
|
||
convertDateTime(date) {
|
||
const format = this.getDateTime().internalDateTimeFormat;
|
||
const timeZone = this.getDateTime().timeZone;
|
||
const m = timeZone ? _moment.default.tz(date, null, timeZone).utc() : _moment.default.utc(date, null);
|
||
return m.format(format) + ':00';
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @return {number}
|
||
*/
|
||
getCalculatedHeight() {
|
||
if (this.$container && this.$container.length) {
|
||
return this.$container.height();
|
||
}
|
||
return this.getHelper().calculateContentContainerHeight(this.$el.find('.calendar'));
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
adjustSize() {
|
||
if (this.isRemoved()) {
|
||
return;
|
||
}
|
||
const height = this.getCalculatedHeight();
|
||
this.calendar.setOption('contentHeight', height);
|
||
this.calendar.updateSize();
|
||
}
|
||
afterRender() {
|
||
if (this.options.containerSelector) {
|
||
this.$container = $(this.options.containerSelector);
|
||
}
|
||
this.$calendar = this.$el.find('div.calendar');
|
||
const slotDuration = '00:' + this.slotDuration + ':00';
|
||
const timeFormat = this.getDateTime().timeFormat;
|
||
let slotLabelFormat = timeFormat;
|
||
if (~timeFormat.indexOf('a')) {
|
||
slotLabelFormat = 'h:mma';
|
||
} else if (~timeFormat.indexOf('A')) {
|
||
slotLabelFormat = 'h:mmA';
|
||
}
|
||
|
||
/** @type {CalendarOptions & Object.<string, *>} */
|
||
const options = {
|
||
scrollTime: this.scrollHour + ':00',
|
||
headerToolbar: false,
|
||
slotLabelFormat: slotLabelFormat,
|
||
eventTimeFormat: timeFormat,
|
||
initialView: this.modeViewMap[this.viewMode],
|
||
defaultRangeSeparator: this.rangeSeparator,
|
||
weekNumbers: true,
|
||
weekNumberCalculation: 'ISO',
|
||
editable: true,
|
||
selectable: true,
|
||
selectMirror: true,
|
||
height: this.options.height || void 0,
|
||
firstDay: this.getDateTime().weekStart,
|
||
slotEventOverlap: true,
|
||
slotDuration: slotDuration,
|
||
slotLabelInterval: '01:00',
|
||
snapDuration: this.slotDuration * 60 * 1000,
|
||
timeZone: this.getDateTime().timeZone || undefined,
|
||
longPressDelay: 300,
|
||
eventColor: this.colors[''],
|
||
nowIndicator: true,
|
||
allDayText: '',
|
||
weekText: '',
|
||
views: {
|
||
week: {
|
||
dayHeaderFormat: 'ddd DD'
|
||
},
|
||
day: {
|
||
dayHeaderFormat: 'ddd DD'
|
||
},
|
||
month: {
|
||
dayHeaderFormat: 'ddd'
|
||
}
|
||
},
|
||
windowResize: () => {
|
||
this.adjustSize();
|
||
},
|
||
select: info => {
|
||
const start = info.startStr;
|
||
const end = info.endStr;
|
||
const allDay = info.allDay;
|
||
let dateEndDate = null;
|
||
let dateStartDate = null;
|
||
const dateStart = this.convertDateTime(start);
|
||
const dateEnd = this.convertDateTime(end);
|
||
if (allDay) {
|
||
dateStartDate = (0, _moment.default)(start).format('YYYY-MM-DD');
|
||
dateEndDate = (0, _moment.default)(end).clone().add(-1, 'days').format('YYYY-MM-DD');
|
||
}
|
||
this.createEvent({
|
||
dateStart: dateStart,
|
||
dateEnd: dateEnd,
|
||
allDay: allDay,
|
||
dateStartDate: dateStartDate,
|
||
dateEndDate: dateEndDate
|
||
});
|
||
this.calendar.unselect();
|
||
},
|
||
eventClick: async info => {
|
||
const event = info.event;
|
||
|
||
/** @type {string} */
|
||
const scope = event.extendedProps.scope;
|
||
/** @type {string} */
|
||
const recordId = event.extendedProps.recordId;
|
||
const helper = new _recordModal.default();
|
||
|
||
/** @type {import('views/modals/detail').default} */
|
||
let modalView;
|
||
modalView = await helper.showDetail(this, {
|
||
entityType: scope,
|
||
id: recordId,
|
||
removeDisabled: false,
|
||
beforeSave: () => {
|
||
if (this.options.onSave) {
|
||
this.options.onSave();
|
||
}
|
||
},
|
||
beforeDestroy: () => {
|
||
if (this.options.onSave) {
|
||
this.options.onSave();
|
||
}
|
||
},
|
||
afterSave: (model, o) => {
|
||
if (!o.bypassClose) {
|
||
modalView.close();
|
||
}
|
||
this.updateModel(model);
|
||
},
|
||
afterDestroy: model => {
|
||
this.removeModel(model);
|
||
}
|
||
});
|
||
},
|
||
datesSet: () => {
|
||
this.date = this.dateToMoment(this.calendar.getDate()).format('YYYY-MM-DD');
|
||
this.trigger('view', this.date, this.mode);
|
||
},
|
||
events: (info, callback) => {
|
||
const dateTimeFormat = this.getDateTime().internalDateTimeFormat;
|
||
const from = _moment.default.tz(info.startStr, info.timeZone);
|
||
const to = _moment.default.tz(info.endStr, info.timeZone);
|
||
const fromStr = from.utc().format(dateTimeFormat);
|
||
const toStr = to.utc().format(dateTimeFormat);
|
||
this.fetchEvents(fromStr, toStr, callback);
|
||
},
|
||
eventDrop: async info => {
|
||
const event = /** @type {EventImpl} */info.event;
|
||
const delta = info.delta;
|
||
const scope = event.extendedProps.scope;
|
||
if (this.onlyDateScopeList.includes(scope)) {
|
||
info.revert();
|
||
return;
|
||
}
|
||
if (!event.allDay && event.extendedProps.allDayCopy) {
|
||
info.revert();
|
||
return;
|
||
}
|
||
if (event.allDay && !event.extendedProps.allDayCopy) {
|
||
info.revert();
|
||
return;
|
||
}
|
||
const start = event.start;
|
||
const end = event.end;
|
||
const dateStart = event.extendedProps.dateStart;
|
||
const dateEnd = event.extendedProps.dateEnd;
|
||
const dateStartDate = event.extendedProps.dateStartDate;
|
||
const dateEndDate = event.extendedProps.dateEndDate;
|
||
const attributes = {};
|
||
if (dateStart) {
|
||
const dateString = this.getDateTime().toMoment(dateStart).add(delta).format(this.getDateTime().internalDateTimeFormat);
|
||
attributes.dateStart = this.convertDateTime(dateString);
|
||
}
|
||
if (dateEnd) {
|
||
const dateString = this.getDateTime().toMoment(dateEnd).add(delta).format(this.getDateTime().internalDateTimeFormat);
|
||
attributes.dateEnd = this.convertDateTime(dateString);
|
||
}
|
||
if (dateStartDate) {
|
||
const m = this.dateToMoment(dateStartDate).add(delta);
|
||
attributes.dateStartDate = m.format(this.getDateTime().internalDateFormat);
|
||
}
|
||
if (dateEndDate) {
|
||
const m = this.dateToMoment(dateEndDate).add(delta);
|
||
attributes.dateEndDate = m.format(this.getDateTime().internalDateFormat);
|
||
}
|
||
const props = this.obtainPropsFromEvent(event);
|
||
if (!end && !this.allDayScopeList.includes(scope)) {
|
||
props.end = _moment.default.tz(start.toISOString(), null, this.getDateTime().timeZone).clone().add(event.extendedProps.duration, 's').toDate();
|
||
}
|
||
props.allDay = false;
|
||
props.dateStart = attributes.dateStart;
|
||
props.dateEnd = attributes.dateEnd;
|
||
props.dateStartDate = attributes.dateStartDate;
|
||
props.dateEndDate = attributes.dateEndDate;
|
||
this.handleAllDay(props, true);
|
||
this.fillColor(props);
|
||
Espo.Ui.notify(this.translate('saving', 'messages'));
|
||
const model = await this.getModelFactory().create(scope);
|
||
model.id = props.recordId;
|
||
if (this.options.onSave) {
|
||
this.options.onSave();
|
||
}
|
||
try {
|
||
await model.save(attributes, {
|
||
patch: true
|
||
});
|
||
} catch (e) {
|
||
info.revert();
|
||
return;
|
||
}
|
||
Espo.Ui.notify();
|
||
this.applyPropsToEvent(event, props);
|
||
},
|
||
eventResize: async info => {
|
||
const event = info.event;
|
||
const attributes = {
|
||
dateEnd: this.convertDateTime(event.endStr)
|
||
};
|
||
const duration = (0, _moment.default)(event.end).unix() - (0, _moment.default)(event.start).unix();
|
||
Espo.Ui.notify(this.translate('saving', 'messages'));
|
||
const model = await this.getModelFactory().create(event.extendedProps.scope);
|
||
model.id = event.extendedProps.recordId;
|
||
if (this.options.onSave) {
|
||
this.options.onSave();
|
||
}
|
||
try {
|
||
await model.save(attributes, {
|
||
patch: true
|
||
});
|
||
} catch (e) {
|
||
info.revert();
|
||
return;
|
||
}
|
||
Espo.Ui.notify();
|
||
event.setExtendedProp('dateEnd', attributes.dateEnd);
|
||
event.setExtendedProp('duration', duration);
|
||
},
|
||
eventAllow: (info, event) => {
|
||
if (event.allDay && !info.allDay) {
|
||
return false;
|
||
}
|
||
if (!event.allDay && info.allDay) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
};
|
||
if (this.teamIdList) {
|
||
options.eventContent = arg => {
|
||
const event = /** @type {EventImpl} */arg.event;
|
||
const $content = $('<div>');
|
||
$content.append($('<div>').append($('<div>').addClass('fc-event-main-frame').append(arg.timeText ? $('<div>').addClass('fc-event-time').text(arg.timeText) : undefined).append($('<div>').addClass('fc-event-title').text(event.title))));
|
||
const userIdList = event.extendedProps.userIdList || [];
|
||
userIdList.forEach(userId => {
|
||
const userName = event.extendedProps.userNameMap[userId] || '';
|
||
let avatarHtml = this.getHelper().getAvatarHtml(userId, 'small', 13);
|
||
if (avatarHtml) {
|
||
avatarHtml += ' ';
|
||
}
|
||
const $div = $('<div>').addClass('user').css({
|
||
overflow: 'hidden'
|
||
}).append(avatarHtml).append($('<span>').text(userName));
|
||
$content.append($div);
|
||
});
|
||
return {
|
||
html: $content.get(0).innerHTML
|
||
};
|
||
};
|
||
}
|
||
if (!this.options.height) {
|
||
options.contentHeight = this.getCalculatedHeight();
|
||
} else {
|
||
options.aspectRatio = 1.62;
|
||
}
|
||
if (this.date) {
|
||
options.initialDate = this.date;
|
||
} else {
|
||
this.$el.find('button[data-action="today"]').addClass('active');
|
||
}
|
||
setTimeout(() => {
|
||
this.calendar = new FullCalendar.Calendar(this.$calendar.get(0), options);
|
||
this.calendar.render();
|
||
this.handleScrollToNow();
|
||
this.updateDate();
|
||
if (this.$container && this.$container.length) {
|
||
this.adjustSize();
|
||
}
|
||
}, 150);
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
handleScrollToNow() {
|
||
if (!(this.mode === 'agendaWeek' || this.mode === 'agendaDay')) {
|
||
return;
|
||
}
|
||
if (!this.isToday()) {
|
||
return;
|
||
}
|
||
const scrollHour = this.getDateTime().getNowMoment().hours() - Math.floor(this.slotDuration * this.scrollToNowSlots / 60);
|
||
if (scrollHour < 0) {
|
||
return;
|
||
}
|
||
this.calendar.scrollToTime(scrollHour + ':00');
|
||
}
|
||
|
||
/**
|
||
* @param {{
|
||
* [allDay]: boolean,
|
||
* [dateStart]: string,
|
||
* [dateEnd]: string,
|
||
* [dateStartDate]: ?string,
|
||
* [dateEndDate]: ?string,
|
||
* }} [values]
|
||
*/
|
||
async createEvent(values) {
|
||
values = values || {};
|
||
if (!values.dateStart && this.date !== this.getDateTime().getToday() && (this.mode === 'day' || this.mode === 'agendaDay')) {
|
||
values.allDay = true;
|
||
values.dateStartDate = this.date;
|
||
values.dateEndDate = this.date;
|
||
}
|
||
const attributes = {};
|
||
if (this.options.userId) {
|
||
attributes.assignedUserId = this.options.userId;
|
||
attributes.assignedUserName = this.options.userName || this.options.userId;
|
||
}
|
||
const scopeList = this.enabledScopeList.filter(it => !this.onlyDateScopeList.includes(it));
|
||
Espo.Ui.notifyWait();
|
||
const view = await this.createView('dialog', 'crm:views/calendar/modals/edit', {
|
||
attributes: attributes,
|
||
enabledScopeList: scopeList,
|
||
scopeList: this.scopeList,
|
||
allDay: values.allDay,
|
||
dateStartDate: values.dateStartDate,
|
||
dateEndDate: values.dateEndDate,
|
||
dateStart: values.dateStart,
|
||
dateEnd: values.dateEnd
|
||
});
|
||
let added = false;
|
||
this.listenTo(view, 'before:save', () => {
|
||
if (this.options.onSave) {
|
||
this.options.onSave();
|
||
}
|
||
});
|
||
this.listenTo(view, 'after:save', model => {
|
||
if (!added) {
|
||
this.addModel(model);
|
||
added = true;
|
||
return;
|
||
}
|
||
this.updateModel(model);
|
||
});
|
||
await view.render();
|
||
Espo.Ui.notify();
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {string} from
|
||
* @param {string} to
|
||
* @param {function} callback
|
||
*/
|
||
fetchEvents(from, to, callback) {
|
||
let url = `Activities?from=${from}&to=${to}`;
|
||
if (this.options.userId) {
|
||
url += '&userId=' + this.options.userId;
|
||
}
|
||
url += '&scopeList=' + encodeURIComponent(this.enabledScopeList.join(','));
|
||
if (this.teamIdList && this.teamIdList.length) {
|
||
url += '&teamIdList=' + encodeURIComponent(this.teamIdList.join(','));
|
||
}
|
||
const agenda = this.mode === 'agendaWeek' || this.mode === 'agendaDay';
|
||
url += '&agenda=' + encodeURIComponent(agenda);
|
||
if (!this.suppressLoadingAlert) {
|
||
Espo.Ui.notifyWait();
|
||
}
|
||
Espo.Ajax.getRequest(url).then(data => {
|
||
const events = this.convertToFcEvents(data);
|
||
callback(events);
|
||
Espo.Ui.notify(false);
|
||
});
|
||
this.fetching = true;
|
||
this.suppressLoadingAlert = false;
|
||
setTimeout(() => this.fetching = false, 50);
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {import('model').default} model
|
||
*/
|
||
addModel(model) {
|
||
const attributes = model.getClonedAttributes();
|
||
attributes.scope = model.entityType;
|
||
const event = this.convertToFcEvent(attributes);
|
||
|
||
// true passed to prevent duplicates after re-fetch.
|
||
this.calendar.addEvent(event, true);
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {import('model').default} model
|
||
*/
|
||
updateModel(model) {
|
||
const eventId = model.entityType + '-' + model.id;
|
||
const event = this.calendar.getEventById(eventId);
|
||
if (!event) {
|
||
return;
|
||
}
|
||
const attributes = model.getClonedAttributes();
|
||
attributes.scope = model.entityType;
|
||
const data = this.convertToFcEvent(attributes);
|
||
this.applyPropsToEvent(event, data);
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {EventImpl} event
|
||
* @return {module:modules/crm/views/calendar/calendar~FcEvent}
|
||
*/
|
||
obtainPropsFromEvent(event) {
|
||
const props = {};
|
||
for (const key in event.extendedProps) {
|
||
props[key] = event.extendedProps[key];
|
||
}
|
||
props.allDay = event.allDay;
|
||
props.start = event.start;
|
||
props.end = event.end;
|
||
props.title = event.title;
|
||
props.id = event.id;
|
||
props.color = event.color;
|
||
return props;
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {EventImpl} event
|
||
* @param {{start?: Date, end?: Date, allDay: boolean} & Record} props
|
||
*/
|
||
applyPropsToEvent(event, props) {
|
||
if ('start' in props) {
|
||
if (!props.allDay && props.end && props.end.getTime() === props.start.getTime()) {
|
||
// Otherwise, 0-duration event would disappear.
|
||
props.end = (0, _moment.default)(props.end).add(1, 'hour').toDate();
|
||
}
|
||
event.setDates(props.start, props.end, {
|
||
allDay: props.allDay
|
||
});
|
||
}
|
||
for (const key in props) {
|
||
const value = props[key];
|
||
if (key === 'start' || key === 'end' || key === 'allDay') {
|
||
continue;
|
||
}
|
||
if (key === 'className') {
|
||
event.setProp('classNames', value);
|
||
continue;
|
||
}
|
||
if (this.extendedProps.includes(key)) {
|
||
event.setExtendedProp(key, value);
|
||
continue;
|
||
}
|
||
event.setProp(key, value);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {import('model').default} model
|
||
*/
|
||
removeModel(model) {
|
||
const event = this.calendar.getEventById(model.entityType + '-' + model.id);
|
||
if (!event) {
|
||
return;
|
||
}
|
||
event.remove();
|
||
}
|
||
|
||
/**
|
||
* @param {{suppressLoadingAlert: boolean}} [options]
|
||
*/
|
||
actionRefresh(options) {
|
||
if (options && options.suppressLoadingAlert) {
|
||
this.suppressLoadingAlert = true;
|
||
}
|
||
this.calendar.refetchEvents();
|
||
}
|
||
actionPrevious() {
|
||
this.calendar.prev();
|
||
this.handleScrollToNow();
|
||
this.updateDate();
|
||
}
|
||
actionNext() {
|
||
this.calendar.next();
|
||
this.handleScrollToNow();
|
||
this.updateDate();
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {string} scope
|
||
* @return {string|undefined}
|
||
*/
|
||
getColorFromScopeName(scope) {
|
||
const additionalColorList = this.getMetadata().get('clientDefs.Calendar.additionalColorList') || [];
|
||
if (!additionalColorList.length) {
|
||
return;
|
||
}
|
||
const colors = this.getMetadata().get('clientDefs.Calendar.colors') || {};
|
||
const scopeList = this.getConfig().get('calendarEntityList') || [];
|
||
let index = 0;
|
||
let j = 0;
|
||
for (let i = 0; i < scopeList.length; i++) {
|
||
if (scopeList[i] in colors) {
|
||
continue;
|
||
}
|
||
if (scopeList[i] === scope) {
|
||
index = j;
|
||
break;
|
||
}
|
||
j++;
|
||
}
|
||
index = index % additionalColorList.length;
|
||
this.colors[scope] = additionalColorList[index];
|
||
return this.colors[scope];
|
||
}
|
||
actionToday() {
|
||
if (this.isToday()) {
|
||
this.actionRefresh();
|
||
return;
|
||
}
|
||
this.calendar.today();
|
||
this.handleScrollToNow();
|
||
this.updateDate();
|
||
}
|
||
}
|
||
var _default = _exports.default = CalendarView;
|
||
});
|
||
|