odoo.define('web.KanbanColumnProgressBar', function (require) { 'use strict'; const core = require('web.core'); var session = require('web.session'); var utils = require('web.utils'); var Widget = require('web.Widget'); const _t = core._t; var KanbanColumnProgressBar = Widget.extend({ template: 'KanbanView.ColumnProgressBar', events: { 'click .o_kanban_counter_progress': '_onProgressBarParentClick', 'click .progress-bar': '_onProgressBarClick', }, /** * Allows to disable animations for tests. * @type {boolean} */ ANIMATE: true, /** * @constructor */ init: function (parent, options, columnState) { this._super.apply(this, arguments); this.columnID = options.columnID; this.columnState = columnState; // attributes this.fieldName = columnState.progressBarValues.field; this.colors = _.extend({}, columnState.progressBarValues.colors, { __false: 'muted', // color to use for false value }); this.sumField = columnState.progressBarValues.sum_field; // Previous progressBar state var state = options.progressBarStates[this.columnID]; if (state) { this.groupCount = state.groupCount; this.subgroupCounts = state.subgroupCounts; this.totalCounterValue = state.totalCounterValue; this.activeFilter = state.activeFilter; } // Prepare currency (TODO this should be automatic... use a field ?) var sumFieldInfo = this.sumField && columnState.fieldsInfo.kanban[this.sumField]; var currencyField = sumFieldInfo && sumFieldInfo.options && sumFieldInfo.options.currency_field; if (currencyField && columnState.data.length) { this.currency = session.currencies[columnState.data[0].data[currencyField].res_id]; } }, /** * @override */ start: function () { var self = this; this.$bars = {}; _.each(this.colors, function (val, key) { self.$bars[key] = self.$(`.progress-bar[data-filter=${key}]`); }); this.$counter = this.$('.o_kanban_counter_side'); this.$number = this.$counter.find('b'); if (this.currency) { var $currency = $('', { text: this.currency.symbol, }); if (this.currency.position === 'before') { $currency.prependTo(this.$counter); } else { $currency.appendTo(this.$counter); } } return this._super.apply(this, arguments).then(function () { // This should be executed when the progressbar is fully rendered // and is in the DOM, this happens to be always the case with // current use of progressbars self.computeCounters(); self._notifyState(); self._render(); }); }, /** * Computes the count of each sub group and the total count */ computeCounters() { const subgroupCounts = {}; let allSubgroupCount = 0; for (const key of Object.keys(this.colors)) { const subgroupCount = this.columnState.progressBarValues.counts[key] || 0; if (this.activeFilter === key && subgroupCount === 0) { this.activeFilter = false; } subgroupCounts[key] = subgroupCount; allSubgroupCount += subgroupCount; }; subgroupCounts.__false = this.columnState.count - allSubgroupCount; this.groupCount = this.columnState.count; this.subgroupCounts = subgroupCounts; this.prevTotalCounterValue = this.totalCounterValue; this.totalCounterValue = this.sumField ? (this.columnState.aggregateValues[this.sumField] || 0) : this.columnState.count; }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Updates the rendering according to internal data. This is done without * qweb rendering because there are animations. * * @private */ _render: function () { var self = this; // Update column display according to active filter this.trigger_up('tweak_column', { callback: function ($el) { $el.removeClass('o_kanban_group_show'); _.each(self.colors, function (val, key) { $el.removeClass('o_kanban_group_show_' + val); }); if (self.activeFilter) { $el.addClass('o_kanban_group_show o_kanban_group_show_' + self.colors[self.activeFilter]); } }, }); this.trigger_up('tweak_column_records', { callback: function ($el, recordData) { var categoryValue = recordData[self.fieldName] ? recordData[self.fieldName] : '__false'; _.each(self.colors, function (val, key) { $el.removeClass('oe_kanban_card_' + val); }); if (self.colors[categoryValue]) { $el.addClass('oe_kanban_card_' + self.colors[categoryValue]); } }, }); // Display and animate the progress bars var barNumber = 0; var barMinWidth = 6; // In % const selection = self.columnState.fields[self.fieldName].selection; _.each(self.colors, function (val, key) { var $bar = self.$bars[key]; var count = self.subgroupCounts && self.subgroupCounts[key] || 0; if (!$bar) { return; } // Adapt tooltip let value; if (selection) { // progressbar on a field of type selection const option = selection.find(option => option[0] === key); value = option && option[1] || _t('Other'); } else { value = key; } $bar.attr('data-original-title', count + ' ' + value); $bar.tooltip({ delay: 0, trigger: 'hover', }); // Adapt active state $bar.toggleClass('progress-bar-animated progress-bar-striped', key === self.activeFilter); // Adapt width $bar.removeClass('o_bar_has_records transition-off'); window.getComputedStyle($bar[0]).getPropertyValue('width'); // Force reflow so that animations work if (count > 0) { $bar.addClass('o_bar_has_records'); // Make sure every bar that has records has some space // and that everything adds up to 100% var maxWidth = 100 - barMinWidth * barNumber; self.$('.progress-bar.o_bar_has_records').css('max-width', maxWidth + '%'); $bar.css('width', (count * 100 / self.groupCount) + '%'); barNumber++; $bar.attr('aria-valuemin', 0); $bar.attr('aria-valuemax', self.groupCount); $bar.attr('aria-valuenow', count); } else { $bar.css('width', ''); } }); this.$('.progress-bar').css('min-width', ''); this.$('.progress-bar.o_bar_has_records').css('min-width', barMinWidth + '%'); // Display and animate the counter number var start = this.prevTotalCounterValue; var end = this.totalCounterValue; if (this.activeFilter) { if (this.sumField) { end = 0; _.each(self.columnState.data, function (record) { var recordData = record.data; if (self.activeFilter === recordData[self.fieldName] || (self.activeFilter === '__false' && !recordData[self.fieldName])) { end += parseFloat(recordData[self.sumField]); } }); } else { end = this.subgroupCounts[this.activeFilter]; } } this.prevTotalCounterValue = end; var animationClass = start > 999 ? 'o_kanban_grow' : 'o_kanban_grow_huge'; if (start !== undefined && (end > start || this.activeFilter) && this.ANIMATE) { $({currentValue: start}).animate({currentValue: end}, { duration: 1000, start: function () { self.$counter.addClass(animationClass); }, step: function () { self.$number.html(_getCounterHTML(this.currentValue)); }, complete: function () { self.$number.html(_getCounterHTML(this.currentValue)); self.$counter.removeClass(animationClass); }, }); } else { this.$number.html(_getCounterHTML(end)); } function _getCounterHTML(value) { return utils.human_number(value, 0, 3); } }, /** * Notifies the new progressBar state so that if a full rerender occurs, the * new progressBar that would replace this one will be initialized with * current state, so that animations are correct. * * @private */ _notifyState: function () { this.trigger_up('set_progress_bar_state', { columnID: this.columnID, values: { groupCount: this.groupCount, subgroupCounts: this.subgroupCounts, totalCounterValue: this.totalCounterValue, activeFilter: this.activeFilter, }, }); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * @private * @param {Event} ev */ _onProgressBarClick: function (ev) { this.$clickedBar = $(ev.currentTarget); var filter = this.$clickedBar.data('filter'); this.activeFilter = (this.activeFilter === filter ? false : filter); this._notifyState(); this._render(); }, /** * @private * @param {Event} ev */ _onProgressBarParentClick: function (ev) { if (ev.target !== ev.currentTarget) { return; } this.activeFilter = false; this._notifyState(); this._render(); }, }); return KanbanColumnProgressBar; });