odoo.define('web.CalendarRenderer', function (require) {
"use strict";
var AbstractRenderer = require('web.AbstractRenderer');
var CalendarPopover = require('web.CalendarPopover');
var core = require('web.core');
var Dialog = require('web.Dialog');
var field_utils = require('web.field_utils');
var FieldManagerMixin = require('web.FieldManagerMixin');
var relational_fields = require('web.relational_fields');
var session = require('web.session');
var Widget = require('web.Widget');
const { createYearCalendarView } = require('/web/static/src/js/libs/fullcalendar.js');
var _t = core._t;
var qweb = core.qweb;
var SidebarFilterM2O = relational_fields.FieldMany2One.extend({
_getSearchBlacklist: function () {
return this._super.apply(this, arguments).concat(this.filter_ids || []);
},
});
var SidebarFilter = Widget.extend(FieldManagerMixin, {
template: 'CalendarView.sidebar.filter',
custom_events: _.extend({}, FieldManagerMixin.custom_events, {
field_changed: '_onFieldChanged',
}),
/**
* @constructor
* @param {Widget} parent
* @param {Object} options
* @param {string} options.fieldName
* @param {Object[]} options.filters A filter is an object with the
* following keys: id, value, label, active, avatar_model, color,
* can_be_removed
* @param {Object} [options.favorite] this is an object with the following
* keys: fieldName, model, fieldModel
*/
init: function (parent, options) {
this._super.apply(this, arguments);
FieldManagerMixin.init.call(this);
this.title = options.title;
this.fields = options.fields;
this.fieldName = options.fieldName;
this.write_model = options.write_model;
this.write_field = options.write_field;
this.avatar_field = options.avatar_field;
this.avatar_model = options.avatar_model;
this.filters = options.filters;
this.label = options.label;
this.getColor = options.getColor;
},
/**
* @override
*/
willStart: function () {
var self = this;
var defs = [this._super.apply(this, arguments)];
if (this.write_model || this.write_field) {
var def = this.model.makeRecord(this.write_model, [{
name: this.write_field,
relation: this.fields[this.fieldName].relation,
type: 'many2one',
}]).then(function (recordID) {
self.many2one = new SidebarFilterM2O(self,
self.write_field,
self.model.get(recordID),
{
mode: 'edit',
attrs: {
string: _t(self.fields[self.fieldName].string),
placeholder: "+ " + _.str.sprintf(_t("Add %s"), self.title),
can_create: false
},
});
});
defs.push(def);
}
return Promise.all(defs);
},
/**
* @override
*/
start: function () {
this._super();
if (this.many2one) {
this.many2one.appendTo(this.$el);
this.many2one.filter_ids = _.without(_.pluck(this.filters, 'value'), 'all');
}
this.$el.on('click', '.o_remove', this._onFilterRemove.bind(this));
this.$el.on('click', '.o_calendar_filter_items input', this._onFilterActive.bind(this));
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {OdooEvent} event
*/
_onFieldChanged: function (event) {
var self = this;
event.stopPropagation();
var createValues = {'user_id': session.uid};
var value = event.data.changes[this.write_field].id;
createValues[this.write_field] = value;
this._rpc({
model: this.write_model,
method: 'create',
args: [createValues],
})
.then(function () {
self.trigger_up('changeFilter', {
'fieldName': self.fieldName,
'value': value,
'active': true,
});
});
},
/**
* @private
* @param {MouseEvent} e
*/
_onFilterActive: function (e) {
var $input = $(e.currentTarget);
this.trigger_up('changeFilter', {
'fieldName': this.fieldName,
'value': $input.closest('.o_calendar_filter_item').data('value'),
'active': $input.prop('checked'),
});
},
/**
* @private
* @param {MouseEvent} e
*/
_onFilterRemove: function (e) {
var self = this;
var $filter = $(e.currentTarget).closest('.o_calendar_filter_item');
Dialog.confirm(this, _t("Do you really want to delete this filter from favorites ?"), {
confirm_callback: function () {
self._rpc({
model: self.write_model,
method: 'unlink',
args: [[$filter.data('id')]],
})
.then(function () {
self.trigger_up('changeFilter', {
'fieldName': self.fieldName,
'id': $filter.data('id'),
'active': false,
'value': $filter.data('value'),
});
});
},
});
},
});
return AbstractRenderer.extend({
template: "CalendarView",
config: {
CalendarPopover: CalendarPopover,
},
custom_events: _.extend({}, AbstractRenderer.prototype.custom_events || {}, {
edit_event: '_onEditEvent',
delete_event: '_onDeleteEvent',
}),
/**
* @constructor
* @param {Widget} parent
* @param {Object} state
* @param {Object} params
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.displayFields = params.displayFields;
this.model = params.model;
this.filters = [];
this.color_map = {};
this.hideDate = params.hideDate;
this.hideTime = params.hideTime;
this.canDelete = params.canDelete;
this.canCreate = params.canCreate;
this.scalesInfo = params.scalesInfo;
this._isInDOM = false;
},
/**
* @override
* @returns {Promise}
*/
start: function () {
this._initSidebar();
this._initCalendar();
return this._super();
},
/**
* @override
*/
on_attach_callback: function () {
this._super(...arguments);
this._isInDOM = true;
// BUG Test ????
// this.$el.height($(window).height() - this.$el.offset().top);
this.calendar.render();
this._renderCalendar();
window.addEventListener('click', this._onWindowClick.bind(this));
},
/**
* Called when the field is detached from the DOM.
*/
on_detach_callback: function () {
this._super(...arguments);
this._isInDOM = false;
window.removeEventListener('click', this._onWindowClick);
},
/**
* @override
*/
destroy: function () {
if (this.calendar) {
this.calendar.destroy();
}
if (this.$small_calendar) {
this.$small_calendar.datepicker('destroy');
$('#ui-datepicker-div:empty').remove();
}
this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Note: this is not dead code, it is called by the calendar-box template
*
* @param {any} record
* @param {any} fieldName
* @param {any} imageField
* @returns {string[]}
*/
getAvatars: function (record, fieldName, imageField) {
var field = this.state.fields[fieldName];
if (!record[fieldName]) {
return [];
}
if (field.type === 'one2many' || field.type === 'many2many') {
return _.map(record[fieldName], function (id) {
return '';
});
} else if (field.type === 'many2one') {
return ['
'];
} else {
var value = this._format(record, fieldName);
var color = this.getColor(value);
if (isNaN(color)) {
return [''];
}
else {
return [''];
}
}
},
/**
* Note: this is not dead code, it is called by two template
*
* @param {any} key
* @returns {integer}
*/
getColor: function (key) {
if (!key) {
return;
}
if (this.color_map[key]) {
return this.color_map[key];
}
// check if the key is a css color
if (typeof key === 'string' && key.match(/^((#[A-F0-9]{3})|(#[A-F0-9]{6})|((hsl|rgb)a?\(\s*(?:(\s*\d{1,3}%?\s*),?){3}(\s*,[0-9.]{1,4})?\))|)$/i)) {
return this.color_map[key] = key;
}
if (typeof key === 'number' && !(key in this.color_map)) {
return this.color_map[key] = key;
}
var index = (((_.keys(this.color_map).length + 1) * 5) % 24) + 1;
this.color_map[key] = index;
return index;
},
/**
* @override
*/
getLocalState: function () {
var fcScroller = this.calendarElement.querySelector('.fc-scroller');
return {
scrollPosition: fcScroller.scrollTop,
};
},
/**
* @override
*/
setLocalState: function (localState) {
if (localState.scrollPosition) {
var fcScroller = this.calendarElement.querySelector('.fc-scroller');
fcScroller.scrollTop = localState.scrollPosition;
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Convert the new format of Event from FullCalendar V4 to a Event FullCalendar V3
* @param fc4Event
* @return {Object} FullCalendar V3 Object Event
* @private
*/
_convertEventToFC3Event: function (fc4Event) {
var event = fc4Event;
if (!moment.isMoment(fc4Event.start)) {
event = {
id: fc4Event.id,
title: fc4Event.title,
start: moment(fc4Event.start).utcOffset(0, true),
end: fc4Event.end && moment(fc4Event.end).utcOffset(0, true),
allDay: fc4Event.allDay,
color: fc4Event.color,
};
if (fc4Event.extendedProps) {
event = Object.assign({}, event, {
r_start: fc4Event.extendedProps.r_start && moment(fc4Event.extendedProps.r_start).utcOffset(0, true),
r_end: fc4Event.extendedProps.r_end && moment(fc4Event.extendedProps.r_end).utcOffset(0, true),
record: fc4Event.extendedProps.record,
attendees: fc4Event.extendedProps.attendees,
});
}
}
return event;
},
/**
* @param {any} event
* @returns {string} the html for the rendered event
*/
_eventRender: function (event) {
var qweb_context = {
event: event,
record: event.extendedProps.record,
color: this.getColor(event.extendedProps.color_index),
showTime: !self.hideTime && event.extendedProps.showTime,
};
this.qweb_context = qweb_context;
if (_.isEmpty(qweb_context.record)) {
return '';
} else {
return qweb.render("calendar-box", qweb_context);
}
},
/**
* @private
* @param {any} record
* @param {any} fieldName
* @returns {string}
*/
_format: function (record, fieldName) {
var field = this.state.fields[fieldName];
if (field.type === "one2many" || field.type === "many2many") {
return field_utils.format[field.type]({data: record[fieldName]}, field);
} else {
return field_utils.format[field.type](record[fieldName], field, {forceString: true});
}
},
/**
* Return the Object options for FullCalendar
*
* @private
* @param {Object} fcOptions
* @return {Object}
*/
_getFullCalendarOptions: function (fcOptions) {
var self = this;
const options = Object.assign({}, this.state.fc_options, {
plugins: [
'moment',
'interaction',
'dayGrid',
'timeGrid'
],
eventDrop: function (eventDropInfo) {
var event = self._convertEventToFC3Event(eventDropInfo.event);
self.trigger_up('dropRecord', event);
},
eventResize: function (eventResizeInfo) {
self._unselectEvent();
var event = self._convertEventToFC3Event(eventResizeInfo.event);
self.trigger_up('updateRecord', event);
},
eventClick: function (eventClickInfo) {
eventClickInfo.jsEvent.preventDefault();
eventClickInfo.jsEvent.stopPropagation();
var eventData = eventClickInfo.event;
self._unselectEvent();
$(self.calendarElement).find(_.str.sprintf('[data-event-id=%s]', eventData.id)).addClass('o_cw_custom_highlight');
self._renderEventPopover(eventData, $(eventClickInfo.el));
},
yearDateClick: function (info) {
self._unselectEvent();
info.view.unselect();
if (!info.events.length) {
if (info.selectable) {
const data = {
start: info.date,
allDay: true,
};
if (self.state.context.default_name) {
data.title = self.state.context.default_name;
}
self.trigger_up('openCreate', self._convertEventToFC3Event(data));
}
} else {
self._renderYearEventPopover(info.date, info.events, $(info.dayEl));
}
},
select: function (selectionInfo) {
// Clicking on the view, dispose any visible popover. Otherwise create a new event.
if (self.$('.o_cw_popover').length) {
self._unselectEvent();
}
var data = {start: selectionInfo.start, end: selectionInfo.end, allDay: selectionInfo.allDay};
if (self.state.context.default_name) {
data.title = self.state.context.default_name;
}
self.trigger_up('openCreate', self._convertEventToFC3Event(data));
if (self.state.scale === 'year') {
self.calendar.view.unselect();
} else {
self.calendar.unselect();
}
},
eventRender: function (info) {
var event = info.event;
var element = $(info.el);
var view = info.view;
element.attr('data-event-id', event.id);
if (view.type === 'dayGridYear') {
const color = this.getColor(event.extendedProps.color_index);
if (typeof color === 'string') {
element.css({
backgroundColor: color,
});
} else if (typeof color === 'number') {
element.addClass(`o_calendar_color_${color}`);
} else {
element.addClass('o_calendar_color_1');
}
} else {
var $render = $(self._eventRender(event));
element.find('.fc-content').html($render.html());
element.addClass($render.attr('class'));
// Add background if doesn't exist
if (!element.find('.fc-bg').length) {
element.find('.fc-content').after($('