1289 lines
53 KiB
JavaScript
1289 lines
53 KiB
JavaScript
odoo.define('web_gantt.GanttRow', function (require) {
|
||
"use strict";
|
||
|
||
var core = require('web.core');
|
||
var config = require('web.config');
|
||
var session = require('web.session');
|
||
var Widget = require('web.Widget');
|
||
const pyUtils = require('web.py_utils');
|
||
let pyUtilsContext = null;
|
||
const fieldUtils = require('web.field_utils');
|
||
|
||
var QWeb = core.qweb;
|
||
var _t = core._t;
|
||
|
||
var GanttRow = Widget.extend({
|
||
template: 'GanttView.Row',
|
||
events: {
|
||
'mouseleave': '_onMouseLeave',
|
||
'mousemove .o_gantt_cell': '_onMouseMove',
|
||
'mouseenter .o_gantt_pill': '_onPillEntered',
|
||
'click .o_gantt_pill': '_onPillClicked',
|
||
'click': '_onRowSidebarClicked',
|
||
'click .o_gantt_cell_buttons > div > .o_gantt_cell_add': '_onButtonAddClicked',
|
||
'click .o_gantt_cell_buttons > div > .o_gantt_cell_plan': '_onButtonPlanClicked',
|
||
},
|
||
NB_GANTT_RECORD_COLORS: 12,
|
||
LEVEL_LEFT_OFFSET: 16, // 16 px per level
|
||
// This determines the pills height. It needs to be an odd number. If it is not a pill can
|
||
// be dropped between two rows without the droppables drop method being called (see tolerance: 'intersect').
|
||
LEVEL_TOP_OFFSET: 31, // 31 px per level
|
||
POPOVER_DELAY: 260,
|
||
/**
|
||
* @override
|
||
* @param {Object} pillsInfo
|
||
* @param {Object} viewInfo
|
||
* @param {Object} options
|
||
* @param {boolean} options.canCreate
|
||
* @param {boolean} options.canEdit
|
||
* @param {boolean} options.disableDragdrop Disable drag and drop for pills
|
||
* @param {boolean} options.hideSidebar Hide sidebar
|
||
* @param {boolean} options.isGroup If is group, It will display all its
|
||
* pills on one row, disable resize, don't allow to create
|
||
* new record when clicked on cell
|
||
*/
|
||
init: function (parent, pillsInfo, viewInfo, options) {
|
||
this._super.apply(this, arguments);
|
||
var self = this;
|
||
|
||
this.name = pillsInfo.groupName;
|
||
this.groupLevel = pillsInfo.groupLevel;
|
||
this.groupedByField = pillsInfo.groupedByField;
|
||
this.pills = _.map(pillsInfo.pills, _.clone);
|
||
this.resId = pillsInfo.resId;
|
||
this.progressBar = pillsInfo.progressBar;
|
||
|
||
this.viewInfo = viewInfo;
|
||
this.fieldsInfo = viewInfo.fieldsInfo;
|
||
this.state = viewInfo.state;
|
||
this.colorField = viewInfo.colorField;
|
||
|
||
this.options = options;
|
||
this.SCALES = options.scales;
|
||
this.isGroup = options.isGroup;
|
||
this.isOpen = options.isOpen;
|
||
this.rowId = options.rowId;
|
||
this.fromServer = options.fromServer;
|
||
this.pillLabel = options.pillLabel;
|
||
this.isMobile = config.device.isMobile;
|
||
this.unavailabilities = (options.unavailabilities || []).map(u => {
|
||
return {
|
||
startDate: self._convertToUserTime(u.start),
|
||
stopDate: self._convertToUserTime(u.stop)
|
||
};
|
||
});
|
||
|
||
this.consolidate = options.consolidate;
|
||
this.consolidationParams = viewInfo.consolidationParams;
|
||
|
||
this.dependencyEnabled = parent.dependencyEnabled;
|
||
|
||
if(options.thumbnail){
|
||
this.thumbnailUrl = session.url('/web/image', {
|
||
model: options.thumbnail.model,
|
||
id: this.resId,
|
||
field: this.options.thumbnail.field,
|
||
});
|
||
}
|
||
|
||
// the total row has some special behaviour
|
||
this.isTotal = this.rowId === '__total_row__';
|
||
|
||
this._adaptPills();
|
||
this._snapToGrid(this.pills);
|
||
this._calculateLevel();
|
||
if (this.isGroup && this.pills.length) {
|
||
this._aggregateGroupedPills();
|
||
} else {
|
||
this.progressField = viewInfo.progressField;
|
||
this._evaluateDecoration();
|
||
}
|
||
this._calculateMarginAndWidth();
|
||
|
||
if (this.pillLabel) {
|
||
this._generatePillLabels(this.state.scale);
|
||
}
|
||
// Add the 16px odoo window default padding.
|
||
this.leftPadding = (this.groupLevel + 1) * this.LEVEL_LEFT_OFFSET;
|
||
const standardHeight = (this.isMobile ? (this.level > 0 ? this.level : 1) : this.level) * (this.LEVEL_TOP_OFFSET + 3) + (this.level > 0 ? this.level : 0);
|
||
this.cellHeight = this.isMobile && this.level <= 1 ? standardHeight * 2 : standardHeight;
|
||
|
||
this.MIN_WIDTHS = { full: 100, half: 50, quarter: 25 };
|
||
this.PARTS = { full: 1, half: 2, quarter: 4 };
|
||
|
||
this.cellMinWidth = this.MIN_WIDTHS[this.viewInfo.activeScaleInfo.precision];
|
||
this.cellPart = this.PARTS[this.viewInfo.activeScaleInfo.precision];
|
||
|
||
this._prepareSlots();
|
||
this._insertIntoSlot();
|
||
|
||
this.childrenRows = [];
|
||
|
||
this._onButtonAddClicked = _.debounce(this._onButtonAddClicked, 500, true);
|
||
this._onButtonPlanClicked = _.debounce(this._onButtonPlanClicked, 500, true);
|
||
this._onPillClicked = _.debounce(this._onPillClicked, 500, true);
|
||
|
||
if (this.isTotal) {
|
||
const maxCount = Math.max(...this.pills.map(p => p.count));
|
||
const factor = maxCount ? (90 / maxCount) : 0;
|
||
for (let p of this.pills) {
|
||
p.totalHeight = factor * p.count;
|
||
}
|
||
}
|
||
this.isRTL = _t.database.parameters.direction === "rtl";
|
||
},
|
||
/**
|
||
* @override
|
||
*/
|
||
destroy: function () {
|
||
if (this.$el) {
|
||
const popover = Popover.getInstance(this.$('.o_gantt_pill')[0]);
|
||
if (popover) {
|
||
popover.dispose();
|
||
}
|
||
}
|
||
this._super();
|
||
},
|
||
|
||
//--------------------------------------------------------------------------
|
||
// Public
|
||
//--------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Set the row (if not total row) as droppable for the pills (but the draggables option "containment" prevents
|
||
* them from going above the row headers).
|
||
* See @_setDraggable
|
||
* @param {DOMElement} firstCell
|
||
*/
|
||
setDroppable: function (firstCell) {
|
||
if (this.isTotal) {
|
||
return;
|
||
}
|
||
var self = this;
|
||
const resizeSnappingWidth = this._getResizeSnappingWidth(firstCell);
|
||
this.$el.droppable({
|
||
accept: ".o_gantt_pill",
|
||
drop: function (event, ui) {
|
||
var diff = self._getDiff(resizeSnappingWidth, ui.position.left);
|
||
var $pill = ui.draggable;
|
||
const oldRowId = $pill.closest('.o_gantt_row')[0].dataset.rowId;
|
||
if (diff || (self.rowId !== oldRowId)) { // do not perform write if nothing change
|
||
const action = event.ctrlKey || event.metaKey ? 'copy': 'reschedule';
|
||
self._saveDragChanges($pill.data('id'), diff, oldRowId, self.rowId, action);
|
||
} else {
|
||
ui.helper.animate({
|
||
left: 0,
|
||
top: 0,
|
||
});
|
||
}
|
||
},
|
||
tolerance: 'intersect',
|
||
});
|
||
},
|
||
|
||
//--------------------------------------------------------------------------
|
||
// Private
|
||
//--------------------------------------------------------------------------
|
||
|
||
/**
|
||
* binds the popover of a specific pill
|
||
* @param target
|
||
* @private
|
||
*/
|
||
_bindPillPopover: function(target) {
|
||
var self = this;
|
||
var $target = $(target);
|
||
if (!$target.hasClass('o_gantt_pill')) {
|
||
$target = this.$(target.offsetParent);
|
||
}
|
||
$target.popover({
|
||
container: this.$el,
|
||
trigger: 'hover',
|
||
delay: {show: this.POPOVER_DELAY},
|
||
html: true,
|
||
placement: 'auto',
|
||
content: function () {
|
||
return self.viewInfo.popoverQWeb.render('gantt-popover', self._getPopoverContext($(this).data('id')));
|
||
},
|
||
}).popover("show");
|
||
},
|
||
|
||
/**
|
||
* Compute minimal levels required to display all pills without overlapping
|
||
*
|
||
* @private
|
||
*/
|
||
_calculateLevel: function () {
|
||
if (this.isGroup || !this.pills.length) {
|
||
// We want shadow pills to overlap each other
|
||
this.level = 0;
|
||
this.pills.forEach(function (pill) {
|
||
pill.level = 0;
|
||
});
|
||
} else {
|
||
// Sort pills according to start date
|
||
this.pills = _.sortBy(this.pills, 'startDate');
|
||
this.pills[0].level = 0;
|
||
var levels = [{
|
||
pills: [this.pills[0]],
|
||
maxStopDate: this.pills[0].stopDate,
|
||
}];
|
||
for (var i = 1; i < this.pills.length; i++) {
|
||
var currentPill = this.pills[i];
|
||
for (var l = 0; l < levels.length; l++) {
|
||
if (currentPill.startDate >= levels[l].maxStopDate) {
|
||
currentPill.level = l;
|
||
levels[l].pills.push(currentPill);
|
||
if (currentPill.stopDate > levels[l].maxStopDate) {
|
||
levels[l].maxStopDate = currentPill.stopDate;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
if (!currentPill.level && currentPill.level != 0) {
|
||
currentPill.level = levels.length;
|
||
levels.push({
|
||
pills: [currentPill],
|
||
maxStopDate: currentPill.stopDate,
|
||
});
|
||
}
|
||
}
|
||
this.level = levels.length;
|
||
}
|
||
},
|
||
/**
|
||
* Adapt pills to the range of current gantt view
|
||
* Disable resize feature if date is before the start of the gantt scope
|
||
* Disable resize feature for group rows
|
||
*
|
||
* @private
|
||
*/
|
||
_adaptPills: function () {
|
||
var self = this;
|
||
var dateStartField = this.state.dateStartField;
|
||
console.log("dateStartField",dateStartField)
|
||
var dateStopField = this.state.dateStopField;
|
||
console.log("dateStopField",dateStopField)
|
||
var ganttStartDate = this.state.startDate;
|
||
console.log("ganttStartDate",ganttStartDate)
|
||
var ganttStopDate = this.state.stopDate;
|
||
console.log("ganttStopDate",ganttStopDate)
|
||
this.pills.forEach(function (pill) {
|
||
var pillStartDate = self._convertToUserTime(pill[dateStartField]);
|
||
console.log("pillStartDate",pillStartDate)
|
||
var pillStopDate = self._convertToUserTime(pill[dateStopField]);
|
||
console.log("pillStopDate",pillStopDate)
|
||
if (pillStartDate < ganttStartDate) {
|
||
pill.startDate = ganttStartDate;
|
||
pill.disableStartResize = true;
|
||
} else {
|
||
pill.startDate = pillStartDate;
|
||
}
|
||
if (pillStopDate > ganttStopDate) {
|
||
pill.stopDate = ganttStopDate;
|
||
pill.disableStopResize = true;
|
||
} else {
|
||
pill.stopDate = pillStopDate;
|
||
}
|
||
// Disable resize feature for groups
|
||
if (self.isGroup) {
|
||
pill.disableStartResize = true;
|
||
pill.disableStopResize = true;
|
||
}
|
||
});
|
||
},
|
||
/**
|
||
* Aggregate overlapping pills in group rows
|
||
*
|
||
* @private
|
||
*/
|
||
_aggregateGroupedPills: function () {
|
||
var self = this;
|
||
var sortedPills = _.sortBy(_.map(this.pills, _.clone), 'startDate');
|
||
var firstPill = sortedPills[0];
|
||
firstPill.count = 1;
|
||
|
||
var timeToken = this.SCALES[this.state.scale].time;
|
||
var precision = this.viewInfo.activeScaleInfo.precision;
|
||
var cellTime = this.SCALES[this.state.scale].cellPrecisions[precision];
|
||
var intervals = _.reduce(this.viewInfo.slots, function (intervals, slotStart) {
|
||
intervals.push(slotStart);
|
||
if (precision === 'half') {
|
||
intervals.push(slotStart.clone().add(cellTime, timeToken));
|
||
}
|
||
return intervals;
|
||
}, []);
|
||
|
||
this.pills = _.reduce(intervals, function (pills, intervalStart) {
|
||
var intervalStop = intervalStart.clone().add(cellTime, timeToken);
|
||
var pillsInThisInterval = _.filter(self.pills, function (pill) {
|
||
return pill.startDate < intervalStop && pill.stopDate > intervalStart;
|
||
});
|
||
if (pillsInThisInterval.length) {
|
||
var previousPill = pills[pills.length - 1];
|
||
var isContinuous = previousPill &&
|
||
_.intersection(previousPill.aggregatedPills, pillsInThisInterval).length;
|
||
|
||
if (isContinuous && previousPill.count === pillsInThisInterval.length) {
|
||
// Enlarge previous pill so that it spans the current slot
|
||
previousPill.stopDate = intervalStop;
|
||
previousPill.aggregatedPills = previousPill.aggregatedPills.concat(pillsInThisInterval);
|
||
} else {
|
||
var newPill = {
|
||
id: 0,
|
||
count: pillsInThisInterval.length,
|
||
aggregatedPills: pillsInThisInterval,
|
||
startDate: moment.max(_.min(pillsInThisInterval, 'startDate').startDate, intervalStart),
|
||
stopDate: moment.min(_.max(pillsInThisInterval, 'stopDate').stopDate, intervalStop),
|
||
};
|
||
|
||
// Enrich the aggregates with consolidation data
|
||
if (self.consolidate && self.consolidationParams.field) {
|
||
newPill.consolidationValue = pillsInThisInterval.reduce(
|
||
function (sum, pill) {
|
||
if (!pill[self.consolidationParams.excludeField]) {
|
||
return sum + pill[self.consolidationParams.field];
|
||
}
|
||
return sum; // Don't sum this pill if it is excluded
|
||
},
|
||
0
|
||
);
|
||
newPill.consolidationMaxValue = self.consolidationParams.maxValue;
|
||
newPill.consolidationExceeded = newPill.consolidationValue > newPill.consolidationMaxValue;
|
||
}
|
||
|
||
pills.push(newPill);
|
||
}
|
||
}
|
||
return pills;
|
||
}, []);
|
||
|
||
var maxCount = _.max(this.pills, function (pill) {
|
||
return pill.count;
|
||
}).count;
|
||
var minColor = 215;
|
||
var maxColor = 100;
|
||
this.pills.forEach(function (pill) {
|
||
pill.consolidated = true;
|
||
if (self.consolidate && self.consolidationParams.maxValue) {
|
||
pill.status = pill.consolidationExceeded ? 'danger' : 'success';
|
||
pill.display_name = pill.consolidationValue;
|
||
} else {
|
||
var color = minColor - ((pill.count - 1) / maxCount) * (minColor - maxColor);
|
||
pill.style = _.str.sprintf("background-color: rgba(%s, %s, %s, 0.6)", color, color, color);
|
||
pill.display_name = self._getAggregateGroupedPillsDisplayName(pill);
|
||
}
|
||
});
|
||
},
|
||
/**
|
||
* This function will add a 'label' property to each
|
||
* non-consolidated pill included in the pills list.
|
||
* This new property is a string meant to replace
|
||
* the text displayed on a pill.
|
||
*
|
||
* @private
|
||
* @param {Object} pills
|
||
* @param {string} scale
|
||
*/
|
||
_generatePillLabels(scale) {
|
||
// as localized yearless date formats do not exists yet in momentjs,
|
||
// this is an awful surgery adapted from SO: https://stackoverflow.com/a/29641375
|
||
// The following regex chain will:
|
||
// - remove all 'Y'(ignoring case),
|
||
// - then remove duplicate consecutives separators,
|
||
// - and finally remove trailing orphaned separators left
|
||
const self = this;
|
||
this.pills.forEach((pill) => {
|
||
const dateFormat = moment.localeData().longDateFormat('l');
|
||
const yearlessDateFormat = dateFormat.replace(/Y/gi, '').replace(/(\W)\1+/g, '$1').replace(/^\W|\W$/, '');
|
||
|
||
const localStartDateTime = (pill[self.state.dateStartField] || pill.startDate).clone().local();
|
||
const localEndDateTime = (pill[self.state.dateStopField] || pill.stopDate).clone().local();
|
||
|
||
const spanAccrossDays = localStartDateTime.clone().startOf('day')
|
||
.diff(localEndDateTime.clone().startOf('day'), 'days') != 0;
|
||
|
||
const spanAccrossWeeks = localStartDateTime.clone().startOf('week')
|
||
.diff(localEndDateTime.clone().startOf('week'), 'weeks') != 0;
|
||
|
||
const spanAccrossMonths = localStartDateTime.clone().startOf('month')
|
||
.diff(localEndDateTime.clone().startOf('month'), 'months') != 0;
|
||
|
||
const labelElements = [];
|
||
|
||
// Start & End Dates
|
||
if (scale === 'year' && !spanAccrossDays) {
|
||
labelElements.push(localStartDateTime.format(yearlessDateFormat));
|
||
} else if (
|
||
(scale === 'day' && spanAccrossDays) ||
|
||
(scale === 'week' && spanAccrossWeeks) ||
|
||
(scale === 'month' && spanAccrossMonths) ||
|
||
(scale === 'year' && spanAccrossDays)
|
||
) {
|
||
labelElements.push(localStartDateTime.format(yearlessDateFormat));
|
||
labelElements.push(localEndDateTime.format(yearlessDateFormat));
|
||
}
|
||
|
||
// Start & End Times
|
||
if (pill.allocated_hours && !spanAccrossDays && ['week', 'month'].includes(scale)) {
|
||
labelElements.push(
|
||
localStartDateTime.format('LT'),
|
||
localEndDateTime.format('LT') + ' (' + fieldUtils.format.float_time(pill.allocated_hours, {}, {noLeadingZeroHour: true}).replace(/(:00|:)/g, 'h') + ')'
|
||
);
|
||
}
|
||
|
||
// Original Display Name
|
||
if (scale !== 'month' || spanAccrossDays) {
|
||
labelElements.push(pill.display_name);
|
||
}
|
||
|
||
pill.label = labelElements.filter(el => !!el).join(' - ');
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Returns the count of pill
|
||
*
|
||
* @private
|
||
* @param {Object} pill
|
||
* @returns {integer}
|
||
*/
|
||
_getAggregateGroupedPillsDisplayName(pill) {
|
||
return pill.count;
|
||
},
|
||
/**
|
||
* Calculate left margin and width for pills
|
||
*
|
||
* @private
|
||
*/
|
||
_calculateMarginAndWidth: function () {
|
||
var self = this;
|
||
var left;
|
||
var diff;
|
||
this.pills.forEach(function (pill) {
|
||
let widthPill;
|
||
let margin;
|
||
const shift_time = 8;
|
||
switch (self.state.scale) {
|
||
case 'day':
|
||
// left = pill.startDate.diff(pill.startDate.clone().startOf('hour'), 'minutes');
|
||
left = pill.startDate.diff(pill.startDate.clone().startOf('day'), 'minutes') % (shift_time * 60);
|
||
|
||
console.log('left', left)
|
||
pill.leftMargin = (left / (shift_time * 60)) * 100;
|
||
console.log('pill.leftMargin', pill.leftMargin)
|
||
diff = pill.stopDate.diff(pill.startDate, 'minutes');
|
||
console.log('diff', diff)
|
||
var gapSize = pill.stopDate.diff(pill.startDate, 'hours') - shift_time; // Eventually compensate border(s) width
|
||
console.log('gapSize', gapSize)
|
||
widthPill = (diff / (shift_time * 60)) * 100;
|
||
console.log('widthPill', widthPill)
|
||
margin = pill.aggregatedPills ? 0 : 4;
|
||
console.log('margin', margin)
|
||
pill.width = gapSize > 0 ? `calc(${widthPill}% + ${gapSize}px - ${margin}px)` : `calc(${widthPill}% - ${margin}px)`;
|
||
console.log('pill.width', pill.width)
|
||
break;
|
||
|
||
// left = pill.startDate.diff(pill.startDate.clone().startOf('hour'), 'minutes');
|
||
// console.log('left', left)
|
||
// left = (left / (8 * 60)) * 100; // 修改这里,以8小时为单位
|
||
// console.log('adjusted left', left)
|
||
// pill.leftMargin = left;
|
||
//
|
||
// diff = pill.stopDate.diff(pill.startDate, 'minutes');
|
||
// console.log('diff', diff)
|
||
// diff = (diff / (8 * 60)) * 100; // 修改这里,以8小时为单位
|
||
// console.log('adjusted diff', diff)
|
||
//
|
||
// var gapSize = pill.stopDate.diff(pill.startDate, 'hours') / 8 - 1; // 修改这里,以8小时为单位
|
||
// console.log('gapSize', gapSize)
|
||
//
|
||
// widthPill = diff;
|
||
// console.log('widthPill', widthPill)
|
||
//
|
||
// margin = pill.aggregatedPills ? 0 : 4;
|
||
// console.log('margin', margin)
|
||
//
|
||
// pill.width = gapSize > 0 ? `calc(${widthPill}% + ${gapSize}px - ${margin}px)` : `calc(${widthPill}% - ${margin}px)`;
|
||
// console.log('pill.width', pill.width)
|
||
// break;
|
||
case 'week':
|
||
case 'month':
|
||
left = pill.startDate.diff(pill.startDate.clone().startOf('day'), 'hours');
|
||
pill.leftMargin = (left / 24) * 100;
|
||
diff = pill.stopDate.diff(pill.startDate, 'hours');
|
||
var gapSize = pill.stopDate.diff(pill.startDate, 'days') - 1; // Eventually compensate border(s) width
|
||
widthPill = (diff / 24) * 100;
|
||
margin = pill.aggregatedPills ? 0 : 4;
|
||
pill.width = gapSize > 0 ? `calc(${widthPill}% + ${gapSize}px - ${margin}px)` : `calc(${widthPill}% - ${margin}px)`;
|
||
break;
|
||
case 'year':
|
||
var startDateMonthStart = pill.startDate.clone().startOf('month');
|
||
var stopDateMonthEnd = pill.stopDate.clone().endOf('month');
|
||
left = pill.startDate.diff(startDateMonthStart, 'days');
|
||
pill.leftMargin = (left / 30) * 100;
|
||
|
||
var monthsDiff = stopDateMonthEnd.diff(startDateMonthStart, 'months', true);
|
||
margin = pill.aggregatedPills ? 0 : 4;
|
||
if (monthsDiff < 1) {
|
||
// A 30th of a month slot is too small to display
|
||
// 1-day events are displayed as if they were 2-days events
|
||
diff = Math.max(Math.ceil(pill.stopDate.diff(pill.startDate, 'days', true)), 2);
|
||
pill.width = `calc(${(diff / pill.startDate.daysInMonth()) * 100}% - ${margin}px)`;
|
||
} else {
|
||
// The pill spans more than one month, so counting its
|
||
// number of days is not enough as some months have more
|
||
// days than others. We need to compute the proportion
|
||
// of each month that the pill is actually taking.
|
||
var startDateMonthEnd = pill.startDate.clone().endOf('month');
|
||
var diffMonthStart = Math.ceil(startDateMonthEnd.diff(pill.startDate, 'days', true));
|
||
var widthMonthStart = (diffMonthStart / pill.startDate.daysInMonth());
|
||
|
||
var stopDateMonthStart = pill.stopDate.clone().startOf('month');
|
||
var diffMonthStop = Math.ceil(pill.stopDate.diff(stopDateMonthStart, 'days', true));
|
||
var widthMonthStop = (diffMonthStop / pill.stopDate.daysInMonth());
|
||
|
||
var width = Math.max((widthMonthStart + widthMonthStop), (2 / 30)) * 100;
|
||
if (monthsDiff > 1) { // start and end months are already covered
|
||
// If the pill spans more than 2 months, we know
|
||
// that the middle months are fully covered
|
||
monthsDiff = Math.floor(monthsDiff)
|
||
width += (monthsDiff - 1) * 100;
|
||
}
|
||
// Added months difference in calculation in px as its width reduces inversely as we increases the width of pill
|
||
pill.width = `calc(${width}% + ${monthsDiff}px - ${margin}px)`;
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// Add 1px top-gap to events sharing the same cell.
|
||
pill.topPadding = pill.level * (self.LEVEL_TOP_OFFSET + 4) + 2;
|
||
});
|
||
},
|
||
/**
|
||
* Convert date to user timezone
|
||
*
|
||
* @private
|
||
* @param {Moment} date
|
||
* @returns {Moment} date in user timezone
|
||
*/
|
||
_convertToUserTime: function (date) {
|
||
// we need to change the original timezone (UTC) to take the user
|
||
// timezone
|
||
return date.clone().local();
|
||
},
|
||
/**
|
||
* Evaluate decoration conditions
|
||
*
|
||
* @private
|
||
*/
|
||
_evaluateDecoration: function () {
|
||
var self = this;
|
||
this.pills.forEach(function (pill) {
|
||
var pillDecorations = [];
|
||
_.each(self.viewInfo.pillDecorations, function (expr, decoration) {
|
||
if (py.PY_isTrue(py.evaluate(expr, self._getDecorationEvalContext(pill)))) {
|
||
pillDecorations.push(decoration);
|
||
}
|
||
});
|
||
pill.decorations = pillDecorations;
|
||
|
||
if (self.colorField) {
|
||
pill._color = self._getColor(pill[self.colorField]);
|
||
}
|
||
|
||
if (self.progressField) {
|
||
pill._progress = pill[self.progressField] || 0;
|
||
}
|
||
});
|
||
},
|
||
/**
|
||
* @param {integer|Array} value
|
||
* @private
|
||
*/
|
||
_getColor: function (value) {
|
||
if (_.isNumber(value)) {
|
||
return Math.round(value) % this.NB_GANTT_RECORD_COLORS;
|
||
} else if (_.isArray(value)) {
|
||
return value[0] % this.NB_GANTT_RECORD_COLORS;
|
||
}
|
||
return 0;
|
||
},
|
||
/**
|
||
* Get context to evaluate decoration
|
||
*
|
||
* @private
|
||
* @param {Object} pillData
|
||
* @returns {Object} context contains pill data, current date, user session
|
||
*/
|
||
_getDecorationEvalContext: function (pillData) {
|
||
return Object.assign(
|
||
{},
|
||
this._getPyUtilsContext(),
|
||
session.user_context,
|
||
this._getPillEvalContext(pillData),
|
||
);
|
||
},
|
||
/**
|
||
* @private
|
||
* @param {number} gridOffset
|
||
*/
|
||
_getDiff: function (resizeSnappingWidth, gridOffset) {
|
||
return Math.round(gridOffset / resizeSnappingWidth) * this.viewInfo.activeScaleInfo.interval;
|
||
},
|
||
/**
|
||
* Evaluate the pill evaluation context.
|
||
*
|
||
* @private
|
||
* @param {Object} pillData
|
||
* @returns {Object} context
|
||
*/
|
||
_getPillEvalContext: function (pillData) {
|
||
var pillContext = _.clone(pillData);
|
||
for (var fieldName in pillContext) {
|
||
const field = this.fieldsInfo[fieldName];
|
||
if (field) {
|
||
const pillCurrentField = pillContext[fieldName];
|
||
if (pillCurrentField instanceof moment) {
|
||
// Replace by ISO formatted string only, without computing it as it is already avalaible in the Moment object interns.
|
||
pillContext[fieldName] = pillCurrentField._i;
|
||
}
|
||
else if (field.type === 'date' || field.type === 'datetime') {
|
||
if (pillCurrentField) {
|
||
pillContext[fieldName] = JSON.parse(JSON.stringify(pillCurrentField));
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
return pillContext;
|
||
},
|
||
/**
|
||
* Get context to display in popover template
|
||
*
|
||
* @private
|
||
* @param {integer} pillID
|
||
* @returns {Object}
|
||
*/
|
||
_getPopoverContext: function (pillID) {
|
||
var data = _.clone(_.findWhere(this.pills, {id: pillID}));
|
||
data.userTimezoneStartDate = this._convertToUserTime(data[this.state.dateStartField]);
|
||
data.userTimezoneStopDate = this._convertToUserTime(data[this.state.dateStopField]);
|
||
return data;
|
||
},
|
||
/**
|
||
* Get pyUtils context
|
||
* When in the same tick, the same pyUtils.context in returned.
|
||
*
|
||
* @returns {Object} the pyUtils context
|
||
*/
|
||
_getPyUtilsContext() {
|
||
if (!pyUtilsContext) {
|
||
pyUtilsContext = pyUtils.context();
|
||
Promise.resolve().then(() => {
|
||
pyUtilsContext = null;
|
||
});
|
||
}
|
||
return pyUtilsContext;
|
||
},
|
||
/**
|
||
* @private
|
||
* @returns {number}
|
||
*/
|
||
_getResizeSnappingWidth: function (firstCell) {
|
||
if (!this.firstCell) {
|
||
this.firstCell = firstCell || $('.o_gantt_view .o_gantt_header_scale .o_gantt_header_cell:first')[0];
|
||
}
|
||
// jQuery (< 3.0) rounds the width value but we need the exact value
|
||
// getBoundingClientRect is costly when there are lots of rows
|
||
return this.firstCell.getBoundingClientRect().width / this.cellPart;
|
||
},
|
||
/**
|
||
* Insert the pills into the gantt row slots according to their start dates
|
||
*
|
||
* @private
|
||
*/
|
||
_insertIntoSlot: function () {
|
||
console.log('this.slots', this.slots);
|
||
var slotsToFill = this.slots;
|
||
this.pills.forEach(function (currentPill) {
|
||
var skippedSlots = [];
|
||
slotsToFill.some(function (currentSlot) {
|
||
console.log('currentPill.startDate1111111111', currentPill)
|
||
// console.log('currentSlot.stop2222222222222', currentSlot.stop)
|
||
var fitsInThisSlot = currentPill.startDate < currentSlot.stop;
|
||
console.log('fitsInThisSlot', fitsInThisSlot)
|
||
if (fitsInThisSlot) {
|
||
currentSlot.pills.push(currentPill);
|
||
console.log('currentSlot.pills', currentSlot.pills)
|
||
} else {
|
||
skippedSlots.push(currentSlot);
|
||
console.log('skippedSlots', skippedSlots)
|
||
}
|
||
return fitsInThisSlot;
|
||
});
|
||
// Pills are sorted by start date, so any slot that was skipped
|
||
// for this pill will not be suitable for any of the next pills
|
||
slotsToFill = _.difference(slotsToFill, skippedSlots);
|
||
console.log('slotsToFill', slotsToFill)
|
||
});
|
||
},
|
||
/**
|
||
* Prepare the gantt row slots
|
||
*
|
||
* @private
|
||
*/
|
||
_prepareSlots: function () {
|
||
const { interval, time, cellPrecisions } = this.SCALES[this.state.scale];
|
||
const precision = this.viewInfo.activeScaleInfo.precision;
|
||
const cellTime = cellPrecisions[precision];
|
||
|
||
function getSlotStyle(cellPart, subSlotUnavailabilities, isToday) {
|
||
function color(d) {
|
||
if (isToday) {
|
||
return d ? 'var(--Gant__DayOff-background-color)' : 'var(--Gant__DayOffToday-background-color)';
|
||
}
|
||
return d ? 'var(--Gant__DayOff-background-color)' : 'var(--Gant__Day-background-color)';
|
||
}
|
||
const sum = subSlotUnavailabilities.reduce((acc, d) => acc + d);
|
||
if (!sum) {
|
||
return '';
|
||
}
|
||
if (cellPart === sum) {
|
||
return `background: ${color(1)}`;
|
||
}
|
||
if (cellPart === 2) {
|
||
const [c0, c1] = subSlotUnavailabilities.map(color);
|
||
return `background: linear-gradient(90deg, ${c0} 49%, ${c1} 50%);`
|
||
}
|
||
if (cellPart === 4) {
|
||
const [c0, c1, c2, c3] = subSlotUnavailabilities.map(color);
|
||
return `background: linear-gradient(90deg, ${c0} 24%, ${c1} 25%, ${c1} 49%, ${c2} 50%, ${c2} 74%, ${c3} 75%);`
|
||
}
|
||
}
|
||
|
||
this.slots = [];
|
||
|
||
// We assume that the 'slots' (dates) are naturally ordered
|
||
// and that unavailabilties have been normalized
|
||
// (i.e. naturally ordered and pairwise disjoint).
|
||
// A subslot is considered unavailable (and greyed) when totally covered by
|
||
// an unavailability.
|
||
let index = 0;
|
||
for (const date of this.viewInfo.slots) {
|
||
const slotStart = date;
|
||
const slotStop = date.clone().add(8, interval);
|
||
const isToday = date.isSame(new Date(), 'day') && this.state.scale !== 'day';
|
||
|
||
let slotStyle = '';
|
||
if (!this.isGroup && this.unavailabilities.slice(index).length) {
|
||
let subSlotUnavailabilities = [];
|
||
for (let j = 0; j < this.cellPart; j++) {
|
||
const subSlotStart = date.clone().add(j * cellTime, time);
|
||
const subSlotStop = date.clone().add((j + 1) * cellTime, time).subtract(1, 'seconds');
|
||
let subSlotUnavailable = 0;
|
||
for (let i = index; i < this.unavailabilities.length; i++) {
|
||
let u = this.unavailabilities[i];
|
||
if (subSlotStop > u.stopDate) {
|
||
index++;
|
||
} else if (u.startDate <= subSlotStart) {
|
||
subSlotUnavailable = 1;
|
||
break;
|
||
}
|
||
}
|
||
subSlotUnavailabilities.push(subSlotUnavailable);
|
||
}
|
||
slotStyle = getSlotStyle(this.cellPart, subSlotUnavailabilities, isToday);
|
||
}
|
||
|
||
this.slots.push({
|
||
isToday: isToday,
|
||
style: slotStyle,
|
||
hasButtons: !this.isGroup && !this.isTotal,
|
||
start: slotStart,
|
||
stop: slotStop,
|
||
pills: [],
|
||
});
|
||
console.log('啊啊啊啊this啊啊啊啊.啊啊啊啊slots啊啊啊啊', this.slots)
|
||
}
|
||
},
|
||
/**
|
||
* Save drag changes
|
||
*
|
||
* @private
|
||
* @param {integer} pillID
|
||
* @param {integer} diff
|
||
* @param {string} oldRowId
|
||
* @param {string} newRowId
|
||
* @param {'copy'|'reschedule'} action
|
||
*/
|
||
_saveDragChanges: function (pillId, diff, oldRowId, newRowId, action) {
|
||
this.trigger_up('pill_dropped', {
|
||
pillId: pillId,
|
||
diff: diff,
|
||
oldRowId: oldRowId,
|
||
newRowId: newRowId,
|
||
groupLevel: this.groupLevel,
|
||
action: action,
|
||
});
|
||
},
|
||
/**
|
||
* Save resize changes
|
||
*
|
||
* @private
|
||
* @param {integer} pillID
|
||
* @param {integer} resizeDiff
|
||
* @param {string} direction
|
||
*/
|
||
_saveResizeChanges: function (pillID, resizeDiff, direction) {
|
||
var pill = _.findWhere(this.pills, {id: pillID});
|
||
var data = { id: pillID };
|
||
if (direction === 'left') {
|
||
data.field = this.state.dateStartField;
|
||
data.date = pill[this.state.dateStartField].clone().subtract(resizeDiff, this.viewInfo.activeScaleInfo.time);
|
||
} else {
|
||
data.field = this.state.dateStopField;
|
||
data.date = pill[this.state.dateStopField].clone().add(resizeDiff, this.viewInfo.activeScaleInfo.time);
|
||
}
|
||
this.trigger_up('pill_resized', data);
|
||
},
|
||
/**
|
||
* Set the draggable jQuery property on a $pill.
|
||
* @private
|
||
* @param {jQuery} $pill
|
||
*/
|
||
_setDraggable: function ($pill) {
|
||
if ($pill.hasClass('ui-draggable-dragging')) {
|
||
return;
|
||
}
|
||
|
||
var self = this;
|
||
var pill = _.findWhere(this.pills, { id: $pill.data('id') });
|
||
|
||
// DRAGGABLE
|
||
if (this.options.canEdit && !pill.disableStartResize && !pill.disableStopResize && !this.isGroup) {
|
||
|
||
const resizeSnappingWidth = this._getResizeSnappingWidth();
|
||
|
||
if ($pill.draggable( "instance")) {
|
||
return;
|
||
}
|
||
if (!this.$containment) {
|
||
this.$containment = $('#o_gantt_containment');
|
||
}
|
||
$pill.draggable({
|
||
containment: this.$containment,
|
||
start: function (event, ui) {
|
||
self.trigger_up('updating_pill_started');
|
||
|
||
const pillWidth = $pill[0].getBoundingClientRect().width;
|
||
ui.helper.css({ width: pillWidth });
|
||
ui.helper.removeClass('position-relative');
|
||
|
||
// The following trigger up will sometimes add the class o_hidden on the $pill.
|
||
// This is why the pill's width is computed above.
|
||
self.trigger_up('start_dragging', {
|
||
$draggedPill: $pill,
|
||
$draggedPillClone: ui.helper,
|
||
});
|
||
|
||
self.$el.addClass('o_gantt_dragging');
|
||
$pill.popover('hide');
|
||
self.$('.o_gantt_pill').popover('disable');
|
||
},
|
||
drag: function (event, ui) {
|
||
if ($(event.target).hasClass('o_gantt_pill_editing')) {
|
||
// Kill draggable if pill opened its dialog
|
||
return false;
|
||
}
|
||
var diff = self._getDiff(resizeSnappingWidth, ui.position.left);
|
||
self._updateResizeBadge(ui.helper, diff, ui);
|
||
|
||
const pointObject = { x: event.pageX, y: event.pageY };
|
||
const options = { container: document.body };
|
||
const $el = $.nearest(pointObject, '.o_gantt_hoverable', options).first();
|
||
if ($el.length) {
|
||
// remove ui-drag-hover class from other rows
|
||
$('.o_gantt_hoverable').removeClass('ui-drag-hover');
|
||
$el.addClass('ui-drag-hover');
|
||
}
|
||
},
|
||
stop: function () {
|
||
self.trigger_up('updating_pill_stopped');
|
||
self.trigger_up('stop_dragging');
|
||
|
||
self.$('.ui-drag-hover').removeClass('ui-drag-hover');
|
||
self.$el.removeClass('o_gantt_dragging');
|
||
self.$('.o_gantt_pill').popover('enable').popover('dispose');
|
||
},
|
||
helper: 'clone',
|
||
});
|
||
} else {
|
||
if ($pill.draggable( "instance")) {
|
||
return;
|
||
}
|
||
if (!this.$lockIndicator) {
|
||
this.$lockIndicator = $('<div class="fa fa-lock"/>').css({
|
||
'z-index': 20,
|
||
position: 'absolute',
|
||
top: '4px',
|
||
right: '4px',
|
||
});
|
||
}
|
||
$pill.draggable({
|
||
// prevents the pill from moving but allows to send feedback
|
||
grid: [0, 0],
|
||
start: function () {
|
||
self.trigger_up('updating_pill_started');
|
||
self.trigger_up('start_no_dragging');
|
||
$pill.popover('hide');
|
||
self.$('.o_gantt_pill').popover('disable');
|
||
self.$lockIndicator.appendTo($pill);
|
||
},
|
||
drag: function (ev) {
|
||
if ($(ev.target).hasClass('o_gantt_pill_editing')) {
|
||
// Kill draggable if pill opened its dialog
|
||
return false;
|
||
}
|
||
},
|
||
stop: function () {
|
||
self.trigger_up('updating_pill_stopped');
|
||
self.trigger_up('stop_no_dragging');
|
||
self.$('.o_gantt_pill').popover('enable').popover('dispose');
|
||
self.$lockIndicator.detach();
|
||
},
|
||
});
|
||
$pill.addClass('o_fake_draggable');
|
||
}
|
||
},
|
||
/**
|
||
* Set the resizable jQuery property on a $pill.
|
||
* @private
|
||
* @param {jQuery} $pill
|
||
*/
|
||
_setResizable: function ($pill) {
|
||
if ($pill.hasClass('ui-resizable')) {
|
||
return;
|
||
}
|
||
var self = this;
|
||
var pillHeight = this.$('.o_gantt_pill:first').height();
|
||
|
||
var pill = _.findWhere(self.pills, { id: $pill.data('id') });
|
||
|
||
const resizeSnappingWidth = this._getResizeSnappingWidth();
|
||
|
||
// RESIZABLE
|
||
var handles = [];
|
||
if (!pill.disableStartResize) {
|
||
handles.push('w');
|
||
}
|
||
if (!pill.disableStopResize) {
|
||
handles.push('e');
|
||
}
|
||
if (handles.length && !self.isGroup && self.options.canEdit) {
|
||
$pill.resizable({
|
||
handles: handles.join(', '),
|
||
odoo_isRTL: this.isRTL,
|
||
// DAM: I wanted to use a containment but there is a bug with them
|
||
// when elements are both draggable and resizable. In that case, is is no more possible
|
||
// to resize on the left side of the pill (I mean starting from left, go to left)
|
||
grid: [resizeSnappingWidth, pillHeight],
|
||
start: function () {
|
||
$pill.popover('hide');
|
||
self.$('.o_gantt_pill').popover('disable');
|
||
self.trigger_up('updating_pill_started');
|
||
self.$el.addClass('o_gantt_dragging');
|
||
},
|
||
resize: function (event, ui) {
|
||
var diff = Math.round((ui.size.width - ui.originalSize.width) / resizeSnappingWidth * self.viewInfo.activeScaleInfo.interval);
|
||
self._updateResizeBadge($pill, diff, ui);
|
||
},
|
||
stop: function (event, ui) {
|
||
// 'stop' is triggered by the mouseup event. Right after, the click is event is
|
||
// triggered. As we also listen to this event (to open a dialog to edit the pill),
|
||
// we have to delay a bit the moment where we mark the pill as no longer being
|
||
// updated, to prevent the dialog from opening when the user ends its resize
|
||
setTimeout(() => {
|
||
if (!self.isDestroyed()) {
|
||
self.trigger_up('updating_pill_stopped');
|
||
self.$el.removeClass('o_gantt_dragging');
|
||
self.$('.o_gantt_pill').popover('enable').popover('dispose');
|
||
}
|
||
});
|
||
var diff = Math.round((ui.size.width - ui.originalSize.width) / resizeSnappingWidth * self.viewInfo.activeScaleInfo.interval);
|
||
// Sometimes the difference (diff) can be falsely rounded (see planning/work entries),
|
||
// leading to changes in the start/end_dates. With the code below the difference
|
||
// will always be one of the cell precisions or 0 making the computation more robust.
|
||
var precisions = self.SCALES[self.state.scale].cellPrecisions;
|
||
var smallest_precision = Math.min(...Object.entries(precisions).map(([key, value]) => value));
|
||
if (diff % smallest_precision != 0) {
|
||
diff = Math.floor(diff/smallest_precision) * smallest_precision;
|
||
}
|
||
var direction = ui.position.left ? 'left' : 'right';
|
||
if (diff) { // do not perform write if nothing change
|
||
self._saveResizeChanges(pill.id, diff, direction);
|
||
}
|
||
},
|
||
});
|
||
}
|
||
},
|
||
/**
|
||
* Snap timespans start and stop dates on grid described by scale precision
|
||
* @params Array<Object> timeSpans objects representing timespans. They need
|
||
* to have a startDate and a stopDate properties.
|
||
*
|
||
* @private
|
||
*/
|
||
_snapToGrid: function (timeSpans) {
|
||
var self = this;
|
||
var interval = this.viewInfo.activeScaleInfo.interval;
|
||
switch (this.state.scale) {
|
||
case 'day':
|
||
timeSpans.forEach(function (span) {
|
||
var snappedStartDate = self._snapMinutes(span.startDate, interval);
|
||
var snappedStopDate = self._snapMinutes(span.stopDate, interval);
|
||
// Set min width
|
||
var minuteDiff = snappedStartDate.diff(snappedStopDate, 'minute');
|
||
if (minuteDiff === 0) {
|
||
if (snappedStartDate > span.startDate) {
|
||
span.startDate = snappedStartDate.subtract(interval, 'minute');
|
||
span.stopDate = snappedStopDate;
|
||
} else {
|
||
span.startDate = snappedStartDate;
|
||
span.stopDate = snappedStopDate.add(interval, 'minute');
|
||
}
|
||
} else {
|
||
span.startDate = snappedStartDate;
|
||
span.stopDate = snappedStopDate;
|
||
}
|
||
});
|
||
break;
|
||
case 'week':
|
||
case 'month':
|
||
timeSpans.forEach(function (span) {
|
||
var snappedStartDate = self._snapHours(span.startDate, interval);
|
||
var snappedStopDate = self._snapHours(span.stopDate, interval);
|
||
// Set min width
|
||
var hourDiff = snappedStartDate.diff(snappedStopDate, 'hour');
|
||
if (hourDiff === 0) {
|
||
if (snappedStartDate.diff(span.startDate, 'hours') > 2 && span.stopDate.diff(snappedStopDate, 'hours') > 2) {
|
||
span.startDate = snappedStartDate.subtract(interval, 'hour');
|
||
span.stopDate = snappedStopDate.add(interval, 'hour');
|
||
} else if (snappedStartDate > span.startDate) {
|
||
span.startDate = snappedStartDate.subtract(interval, 'hour');
|
||
span.stopDate = snappedStopDate;
|
||
} else {
|
||
span.startDate = snappedStartDate;
|
||
span.stopDate = snappedStopDate.add(interval, 'hour');
|
||
}
|
||
} else {
|
||
if (snappedStartDate.diff(span.startDate, 'hours') > 2) {
|
||
snappedStartDate = snappedStartDate.subtract(interval, 'hour');
|
||
}
|
||
if (span.stopDate.diff(snappedStopDate, 'hours') > 2) {
|
||
snappedStopDate = snappedStopDate.add(interval, 'hour');
|
||
}
|
||
span.startDate = snappedStartDate;
|
||
span.stopDate = snappedStopDate;
|
||
}
|
||
});
|
||
break;
|
||
case 'year':
|
||
timeSpans.forEach(function (span) {
|
||
span.startDate = span.startDate.clone().startOf('month');
|
||
span.stopDate = span.stopDate.clone().endOf('month');
|
||
});
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
},
|
||
/**
|
||
* Snap a day to given interval
|
||
*
|
||
* @private
|
||
* @param {Moment} date
|
||
* @param {integer} interval
|
||
* @returns {Moment} snapped date
|
||
*/
|
||
_snapHours: function (date, interval) {
|
||
var snappedHours = Math.round(date.clone().hour() / interval) * interval;
|
||
return date.clone().hour(snappedHours).minute(0).second(0);
|
||
},
|
||
/**
|
||
* Snap a hour to given interval
|
||
*
|
||
* @private
|
||
* @param {Moment} date
|
||
* @param {integer} interval
|
||
* @returns {Moment} snapped hour date
|
||
*/
|
||
_snapMinutes: function (date, interval) {
|
||
var snappedMinutes = Math.round(date.clone().minute() / interval) * interval;
|
||
return date.clone().minute(snappedMinutes).second(0);
|
||
},
|
||
/**
|
||
* @private
|
||
* @param {jQuert} $pill
|
||
* @param {integer} diff
|
||
* @param {Object} ui
|
||
*/
|
||
_updateResizeBadge: function ($pill, diff, ui) {
|
||
$pill.find('.o_gantt_pill_resize_badge').remove();
|
||
if (diff) {
|
||
var direction = ui.position.left ? 'left' : 'right';
|
||
$( QWeb.render('GanttView.ResizeBadge', {
|
||
diff: diff,
|
||
direction: direction,
|
||
time: this.viewInfo.activeScaleInfo.time,
|
||
} ), { css: { 'z-index': 2 } } )
|
||
.appendTo($pill);
|
||
}
|
||
},
|
||
|
||
//--------------------------------------------------------------------------
|
||
// Handlers
|
||
//--------------------------------------------------------------------------
|
||
|
||
/**
|
||
* When click on cell open dialog to create new record with prefilled fields
|
||
*
|
||
* @private
|
||
* @param {MouseEvent} ev
|
||
*/
|
||
_onButtonAddClicked: function (ev) {
|
||
var date = moment($(ev.currentTarget).closest('.o_gantt_cell').data('date'));
|
||
this.trigger_up('add_button_clicked', {
|
||
date: date,
|
||
rowId: this.rowId,
|
||
});
|
||
},
|
||
/**
|
||
* When click on cell open dialog to create new record with prefilled fields
|
||
*
|
||
* @private
|
||
* @param {MouseEvent} ev
|
||
*/
|
||
_onButtonPlanClicked: function (ev) {
|
||
var date = moment($(ev.currentTarget).closest('.o_gantt_cell').data('date'));
|
||
this.trigger_up('plan_button_clicked', {
|
||
date: date,
|
||
rowId: this.rowId,
|
||
});
|
||
},
|
||
/**
|
||
* When entering a cell, it displays some buttons (but not when resizing
|
||
* another pill, we thus can't use css rules).
|
||
*
|
||
* Note that we cannot do that on the cell mouseenter because we don't enter
|
||
* the cell we moving the mouse on a pill that spans on multiple cells.
|
||
*
|
||
* Also note that we try to *avoid using jQuery* here to reduce the time
|
||
* spent in this function so the whole view doesn't feel sluggish when there
|
||
* are a lot of records.
|
||
*
|
||
* @private
|
||
* @param {MouseEvent} ev
|
||
*/
|
||
_onMouseMove: function (ev) {
|
||
if ((this.options.canCreate || this.options.canEdit) &&
|
||
!this.$el[0].classList.contains('o_gantt_dragging')) {
|
||
// Pills are part of the cell in which they start. If a pill is
|
||
// longer than one cell, and the user is hovering on the right
|
||
// side of the pill, the browser will say that the left cell is
|
||
// hovered, since the hover event will bubble up from the pill to
|
||
// the cell which contains it, hence, the left one. The only way we
|
||
// found to target the real cell on which the user is currently
|
||
// hovering is calling the costly elementsFromPoint function.
|
||
// Besides, this function will not work in the test environment.
|
||
var elementsFromPoint = function (x, y) {
|
||
if (document.elementsFromPoint)
|
||
return document.elementsFromPoint(x, y);
|
||
if (document.msElementsFromPoint) {
|
||
return Array.prototype.slice.call(document.msElementsFromPoint(x, y));
|
||
}
|
||
};
|
||
|
||
var hoveredCell;
|
||
if (ev.target.classList.contains('o_gantt_pill') || ev.target.parentNode.classList.contains('o_gantt_pill')) {
|
||
elementsFromPoint(ev.pageX, ev.pageY).some(function (element) {
|
||
return element.classList.contains('o_gantt_cell') ? ((hoveredCell = element), true) : false;
|
||
});
|
||
} else {
|
||
hoveredCell = ev.currentTarget;
|
||
}
|
||
|
||
if (hoveredCell && hoveredCell != this.lastHoveredCell) {
|
||
if (this.lastHoveredCell) {
|
||
this.lastHoveredCell.classList.remove('o_hovered');
|
||
}
|
||
hoveredCell.classList.add('o_hovered');
|
||
this.lastHoveredCell = hoveredCell;
|
||
}
|
||
}
|
||
},
|
||
/**
|
||
* @private
|
||
*/
|
||
_onMouseLeave: function () {
|
||
// User leaves this row to enter another one
|
||
this.$(".o_gantt_cell.o_hovered").removeClass('o_hovered');
|
||
this.lastHoveredCell = undefined;
|
||
},
|
||
/**
|
||
* When click on pill open dialog to view record
|
||
*
|
||
* @private
|
||
* @param {MouseEvent} ev
|
||
*/
|
||
_onPillClicked: function (ev) {
|
||
if (!this.isGroup) {
|
||
this.trigger_up('pill_clicked', {
|
||
target: $(ev.currentTarget),
|
||
});
|
||
}
|
||
},
|
||
/**
|
||
* Set the draggable and resizable jQuery properties on a pill when the user
|
||
* enters the pill.
|
||
*
|
||
* This is only done at this time and not in `on_attach_callback` to
|
||
* optimize the rendering (creating jQuery draggable and resizable for
|
||
* potentially thousands of pills is the heaviest task).
|
||
*
|
||
* @private
|
||
* @param {MouseEvent} ev
|
||
*/
|
||
_onPillEntered: function (ev) {
|
||
var $pill = $(ev.currentTarget);
|
||
|
||
this._setResizable($pill);
|
||
if (!this.isTotal && !this.options.disableDragdrop) {
|
||
this._setDraggable($pill);
|
||
}
|
||
if (!this.isGroup && !config.device.isMobile) {
|
||
this._bindPillPopover(ev.target);
|
||
}
|
||
},
|
||
/**
|
||
* Toggle Collapse/Expand rows when user click in gantt row sidebar
|
||
*
|
||
* @private
|
||
*/
|
||
_onRowSidebarClicked: function () {
|
||
if (this.isGroup & !this.isTotal) {
|
||
if (this.isOpen) {
|
||
this.trigger_up('collapse_row', {rowId: this.rowId});
|
||
} else {
|
||
this.trigger_up('expand_row', {rowId: this.rowId});
|
||
}
|
||
}
|
||
},
|
||
});
|
||
|
||
return GanttRow;
|
||
|
||
});
|