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', 'mousewheel .o_gantt_hoverable': '_onColMouseWheel', 'click .o_gantt_hoverable': '_onColClicked', }, 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; // let isDelay = false // if(pill.state != 'processing' && pill.state != 'finished') { // 判断待加工 // isDelay = pill.order_deadline.isBefore(new Date()) // } pill.exState = '' if (self.colorField){ // console.log(self.colorField, self, pill, '颜色') // pill._color = self._getColor(pill[self.colorField]); // 设置pill背景颜色2 修改时间2024年6月25日17:09:43 let isDelay = false if(pill.state != 'processing' && pill.state != 'finished') { // 判断待加工 isDelay = pill.order_deadline.isBefore(new Date()) } if(isDelay) { pill.disableDragdrop = true } pill._color = self._getColor2(isDelay ? 'delay' : pill.state); } 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; }, _getColor2 (state) { return { 'finished': 'ccc', 'delay': 9, 'processing': 13 // 绿色 }[state] }, /** * 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); let slotStop; if (interval === "hour") { slotStop = date.clone().add(8, "hour"); } else { slotStop = date.clone().add(1, 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') }); if(pill.state == 'finished'){ // 已完成状态不能拖拽 return; } // 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 = $('
').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