- Updated copyright headers in 3,055 core application files - Changed 'Copyright (C) 2014-2025' to 'Copyright (C) 2014-2026' - Added 123 new files from EspoCRM core updates - Removed 4 deprecated files - Total changes: 61,637 insertions, 54,283 deletions This is a routine maintenance update for the new year 2026.
1025 lines
37 KiB
JavaScript
1025 lines
37 KiB
JavaScript
/************************************************************************
|
||
* This file is part of EspoCRM.
|
||
*
|
||
* EspoCRM – Open Source CRM application.
|
||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||
* Website: https://www.espocrm.com
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU Affero General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Affero General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*
|
||
* The interactive user interfaces in modified source and object code versions
|
||
* of this program must display Appropriate Legal Notices, as required under
|
||
* Section 5 of the GNU Affero General Public License version 3.
|
||
*
|
||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||
************************************************************************/
|
||
|
||
define('crm:views/dashlets/abstract/chart', ['views/dashlets/abstract/base','lib!flotr2'], function (Dep, Flotr) {
|
||
|
||
return Dep.extend({
|
||
|
||
templateContent: '<div class="chart-container"></div><div class="legend-container"></div>',
|
||
|
||
decimalMark: '.',
|
||
thousandSeparator: ',',
|
||
|
||
defaultColorList: ['#6FA8D6', '#4E6CAD', '#EDC555', '#ED8F42', '#DE6666', '#7CC4A4', '#8A7CC2', '#D4729B'],
|
||
successColor: '#85b75f',
|
||
gridColor: '#ddd',
|
||
tickColor: '#e8eced',
|
||
textColor: '#333',
|
||
hoverColor: '#FF3F19',
|
||
legendColumnWidth: 110,
|
||
legendColumnNumber: 6,
|
||
|
||
labelFormatter: function (v) {
|
||
return '<span style="color:'+this.textColor+'">' + v + '</span>';
|
||
},
|
||
|
||
init: function () {
|
||
Dep.prototype.init.call(this);
|
||
|
||
this.fontSizeFactor = this.getThemeManager().getFontSizeFactor();
|
||
|
||
this.flotr = Flotr;
|
||
|
||
this.successColor = this.getThemeManager().getParam('chartSuccessColor') || this.successColor;
|
||
this.colorList = this.getThemeManager().getParam('chartColorList') || this.defaultColorList;
|
||
this.tickColor = this.getThemeManager().getParam('chartTickColor') || this.tickColor;
|
||
this.gridColor = this.getThemeManager().getParam('chartGridColor') || this.gridColor;
|
||
this.textColor = this.getThemeManager().getParam('textColor') || this.textColor;
|
||
this.hoverColor = this.getThemeManager().getParam('hoverColor') || this.hoverColor;
|
||
|
||
if (this.getPreferences().has('decimalMark')) {
|
||
this.decimalMark = this.getPreferences().get('decimalMark')
|
||
} else {
|
||
if (this.getConfig().has('decimalMark')) {
|
||
this.decimalMark = this.getConfig().get('decimalMark')
|
||
}
|
||
}
|
||
|
||
if (this.getPreferences().has('thousandSeparator')) {
|
||
this.thousandSeparator = this.getPreferences().get('thousandSeparator')
|
||
} else {
|
||
if (this.getConfig().has('thousandSeparator')) {
|
||
this.thousandSeparator = this.getConfig().get('thousandSeparator')
|
||
}
|
||
}
|
||
|
||
this.on('resize', () => {
|
||
if (!this.isRendered()) {
|
||
return;
|
||
}
|
||
|
||
setTimeout(() => {
|
||
this.adjustContainer();
|
||
|
||
if (this.isNoData()) {
|
||
this.showNoData();
|
||
|
||
return;
|
||
}
|
||
|
||
this.draw();
|
||
}, 50);
|
||
});
|
||
|
||
$(window).on('resize.chart' + this.id, () => {
|
||
this.adjustContainer();
|
||
|
||
if (this.isNoData()) {
|
||
this.showNoData();
|
||
|
||
return;
|
||
}
|
||
|
||
this.draw();
|
||
});
|
||
|
||
this.once('remove', () => {
|
||
$(window).off('resize.chart' + this.id)
|
||
});
|
||
},
|
||
|
||
formatNumber: function (value, isCurrency, useSiMultiplier) {
|
||
if (value !== null) {
|
||
let maxDecimalPlaces = 2;
|
||
|
||
var currencyDecimalPlaces = this.getConfig().get('currencyDecimalPlaces');
|
||
|
||
var siSuffix = '';
|
||
|
||
if (useSiMultiplier) {
|
||
if (value >= 1000000) {
|
||
siSuffix = 'M';
|
||
value = value / 1000000;
|
||
} else if (value >= 1000) {
|
||
siSuffix = 'k';
|
||
value = value / 1000;
|
||
}
|
||
}
|
||
|
||
if (isCurrency) {
|
||
if (currencyDecimalPlaces === 0) {
|
||
value = Math.round(value);
|
||
} else if (currencyDecimalPlaces) {
|
||
value = Math.round(value * Math.pow(10, currencyDecimalPlaces)) /
|
||
(Math.pow(10, currencyDecimalPlaces));
|
||
} else {
|
||
value = Math.round(value * Math.pow(10, maxDecimalPlaces)) / (Math.pow(10, maxDecimalPlaces));
|
||
}
|
||
} else {
|
||
let maxDecimalPlaces = 4;
|
||
|
||
if (useSiMultiplier) {
|
||
maxDecimalPlaces = 2;
|
||
}
|
||
|
||
value = Math.round(value * Math.pow(10, maxDecimalPlaces)) / (Math.pow(10, maxDecimalPlaces));
|
||
}
|
||
|
||
var parts = value.toString().split(".");
|
||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandSeparator);
|
||
|
||
if (isCurrency) {
|
||
if (currencyDecimalPlaces === 0) {
|
||
delete parts[1];
|
||
}
|
||
else if (currencyDecimalPlaces) {
|
||
var decimalPartLength = 0;
|
||
|
||
if (parts.length > 1) {
|
||
decimalPartLength = parts[1].length;
|
||
} else {
|
||
parts[1] = '';
|
||
}
|
||
|
||
if (currencyDecimalPlaces && decimalPartLength < currencyDecimalPlaces) {
|
||
var limit = currencyDecimalPlaces - decimalPartLength;
|
||
|
||
for (let i = 0; i < limit; i++) {
|
||
parts[1] += '0';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return parts.join(this.decimalMark) + siSuffix;
|
||
}
|
||
|
||
return '';
|
||
},
|
||
|
||
getLegendColumnNumber: function () {
|
||
const width = this.$el.closest('.panel-body').width();
|
||
|
||
const legendColumnNumber = Math.floor(width / (this.legendColumnWidth * this.fontSizeFactor));
|
||
|
||
return legendColumnNumber || this.legendColumnNumber;
|
||
},
|
||
|
||
getLegendHeight: function () {
|
||
const lineNumber = Math.ceil(this.chartData.length / this.getLegendColumnNumber());
|
||
let legendHeight = 0;
|
||
|
||
const lineHeight = (this.getThemeManager().getParam('dashletChartLegendRowHeight') || 19) *
|
||
this.fontSizeFactor;
|
||
|
||
const paddingTopHeight = (this.getThemeManager().getParam('dashletChartLegendPaddingTopHeight') || 7) *
|
||
this.fontSizeFactor;
|
||
|
||
if (lineNumber > 0) {
|
||
legendHeight = lineHeight * lineNumber + paddingTopHeight;
|
||
}
|
||
|
||
return legendHeight;
|
||
},
|
||
|
||
adjustContainer: function () {
|
||
const legendHeight = this.getLegendHeight();
|
||
const heightCss = `calc(100% - ${legendHeight.toString()}px)`;
|
||
|
||
this.$container.css('height', heightCss);
|
||
},
|
||
|
||
adjustLegend: function () {
|
||
const number = this.getLegendColumnNumber();
|
||
|
||
if (!number) {
|
||
return;
|
||
}
|
||
|
||
const dashletChartLegendBoxWidth = (this.getThemeManager().getParam('dashletChartLegendBoxWidth') || 21) *
|
||
this.fontSizeFactor;
|
||
|
||
const containerWidth = this.$legendContainer.width();
|
||
const width = Math.floor((containerWidth - dashletChartLegendBoxWidth * number) / number);
|
||
const columnNumber = this.$legendContainer.find('> table tr:first-child > td').length / 2;
|
||
|
||
const tableWidth = (width + dashletChartLegendBoxWidth) * columnNumber;
|
||
|
||
this.$legendContainer.find('> table')
|
||
.css('table-layout', 'fixed')
|
||
.attr('width', tableWidth);
|
||
|
||
this.$legendContainer.find('td.flotr-legend-label').attr('width', width);
|
||
this.$legendContainer.find('td.flotr-legend-color-box').attr('width', dashletChartLegendBoxWidth);
|
||
|
||
this.$legendContainer.find('td.flotr-legend-label > span').each((i, span) => {
|
||
span.setAttribute('title', span.textContent);
|
||
});
|
||
},
|
||
|
||
afterRender: function () {
|
||
this.$el.closest('.panel-body').css({
|
||
'overflow-y': 'visible',
|
||
'overflow-x': 'visible',
|
||
});
|
||
|
||
this.$legendContainer = this.$el.find('.legend-container');
|
||
this.$container = this.$el.find('.chart-container');
|
||
|
||
this.fetch(function (data) {
|
||
this.chartData = this.prepareData(data);
|
||
|
||
this.adjustContainer();
|
||
|
||
if (this.isNoData()) {
|
||
this.showNoData();
|
||
|
||
return;
|
||
}
|
||
|
||
setTimeout(() => {
|
||
if (!this.$container.length || !this.$container.is(":visible")) {
|
||
return;
|
||
}
|
||
|
||
this.draw();
|
||
}, 1);
|
||
});
|
||
},
|
||
|
||
isNoData: function () {
|
||
return false;
|
||
},
|
||
|
||
url: function () {},
|
||
|
||
prepareData: function (response) {
|
||
return response;
|
||
},
|
||
|
||
fetch: function (callback) {
|
||
Espo.Ajax.getRequest(this.url())
|
||
.then(response => {
|
||
callback.call(this, response);
|
||
});
|
||
},
|
||
|
||
getDateFilter: function () {
|
||
return this.getOption('dateFilter') || 'currentYear';
|
||
},
|
||
|
||
showNoData: function () {
|
||
this.$container.empty();
|
||
|
||
const $text = $('<span>').html(this.translate('No Data')).addClass('text-muted');
|
||
|
||
const $div = $('<div>')
|
||
.css('text-align', 'center')
|
||
.css('font-size', 'calc(var(--font-size-base) * 1.2)')
|
||
.css('display', 'table')
|
||
.css('width', '100%')
|
||
.css('height', '100%')
|
||
.css('user-select', 'none');
|
||
|
||
$text
|
||
.css('display', 'table-cell')
|
||
.css('vertical-align', 'middle')
|
||
.css('padding-bottom', 'calc(var(--font-size-base) * 1.5)');
|
||
|
||
|
||
$div.append($text);
|
||
|
||
this.$container.append($div);
|
||
},
|
||
});
|
||
});
|
||
|
||
/************************************************************************
|
||
* This file is part of EspoCRM.
|
||
*
|
||
* EspoCRM – Open Source CRM application.
|
||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||
* Website: https://www.espocrm.com
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU Affero General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Affero General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*
|
||
* The interactive user interfaces in modified source and object code versions
|
||
* of this program must display Appropriate Legal Notices, as required under
|
||
* Section 5 of the GNU Affero General Public License version 3.
|
||
*
|
||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||
************************************************************************/
|
||
|
||
define('crm:views/dashlets/sales-pipeline', ['crm:views/dashlets/abstract/chart', 'lib!espo-funnel-chart'],
|
||
function (Dep) {
|
||
|
||
return Dep.extend({
|
||
|
||
name: 'SalesPipeline',
|
||
|
||
setupDefaultOptions: function () {
|
||
this.defaultOptions['dateFrom'] = this.defaultOptions['dateFrom'] || moment().format('YYYY') + '-01-01';
|
||
this.defaultOptions['dateTo'] = this.defaultOptions['dateTo'] || moment().format('YYYY') + '-12-31';
|
||
},
|
||
|
||
url: function () {
|
||
var url = 'Opportunity/action/reportSalesPipeline?dateFilter='+ this.getDateFilter();
|
||
|
||
if (this.getDateFilter() === 'between') {
|
||
url += '&dateFrom=' + this.getOption('dateFrom') + '&dateTo=' + this.getOption('dateTo');
|
||
}
|
||
|
||
if (this.getOption('useLastStage')) {
|
||
url += '&useLastStage=true';
|
||
}
|
||
|
||
if (this.getOption('teamId')) {
|
||
url += '&teamId=' + this.getOption('teamId');
|
||
}
|
||
|
||
return url;
|
||
},
|
||
|
||
isNoData: function () {
|
||
return this.isEmpty;
|
||
},
|
||
|
||
prepareData: function (response) {
|
||
let list = [];
|
||
|
||
this.isEmpty = true;
|
||
|
||
response.dataList.forEach(item => {
|
||
if (item.value) {
|
||
this.isEmpty = false;
|
||
}
|
||
|
||
list.push({
|
||
stageTranslated: this.getLanguage().translateOption(item.stage, 'stage', 'Opportunity'),
|
||
value: item.value,
|
||
stage: item.stage,
|
||
});
|
||
});
|
||
|
||
return list;
|
||
},
|
||
|
||
setup: function () {
|
||
this.currency = this.getConfig().get('defaultCurrency');
|
||
this.currencySymbol = this.getMetadata().get(['app', 'currency', 'symbolMap', this.currency]) || '';
|
||
|
||
this.chartData = [];
|
||
},
|
||
|
||
draw: function () {
|
||
let colors = Espo.Utils.clone(this.colorList);
|
||
|
||
this.chartData.forEach((item, i) => {
|
||
if (i + 1 > colors.length) {
|
||
colors.push('#164');
|
||
}
|
||
|
||
if (this.chartData.length === i + 1 && item.stage === 'Closed Won') {
|
||
colors[i] = this.successColor;
|
||
}
|
||
|
||
this.chartData[i].color = colors[i];
|
||
});
|
||
|
||
this.$container.empty();
|
||
|
||
let tooltipStyleString =
|
||
'opacity:0.7;background-color:#000;color:#fff;position:absolute;'+
|
||
'padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;'
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
new EspoFunnel.Funnel(
|
||
this.$container.get(0),
|
||
{
|
||
colors: colors,
|
||
outlineColor: this.hoverColor,
|
||
callbacks: {
|
||
tooltipHtml: (i) => {
|
||
let value = this.chartData[i].value;
|
||
|
||
return this.chartData[i].stageTranslated +
|
||
'<br>' + this.currencySymbol +
|
||
'<span class="numeric-text">' +
|
||
this.formatNumber(value, true) +
|
||
'</span>';
|
||
},
|
||
},
|
||
tooltipClassName: 'flotr-mouse-value',
|
||
tooltipStyleString: tooltipStyleString,
|
||
},
|
||
this.chartData
|
||
);
|
||
|
||
this.drawLegend();
|
||
this.adjustLegend();
|
||
},
|
||
|
||
drawLegend: function () {
|
||
let number = this.getLegendColumnNumber();
|
||
let $container = this.$el.find('.legend-container');
|
||
|
||
let html = '<table style="font-size: smaller; color:' + this.textColor + '">';
|
||
|
||
this.chartData.forEach((item, i) => {
|
||
if (i % number === 0) {
|
||
if (i > 0) {
|
||
html += '</tr>';
|
||
}
|
||
|
||
html += '<tr>';
|
||
}
|
||
|
||
let stageTranslated = this.getHelper().escapeString(item.stageTranslated);
|
||
|
||
let box = '<div style="border: var(--1px) solid transparent; padding: var(--1px)">'+
|
||
'<div style="width: var(--13px); height: var(--9px); border: var(--1px) solid ' + item.color + '">'+
|
||
'<div style="width: var(--14px); height: var(--10px); background-color:' + item.color + ';"></div></div></div>';
|
||
|
||
html += '<td class="flotr-legend-color-box">' + box + '</td>';
|
||
|
||
html += '<td class="flotr-legend-label"><span title="' + stageTranslated + '">'+
|
||
stageTranslated + '</span></td>';
|
||
});
|
||
|
||
html += '</tr>';
|
||
html += '</table>';
|
||
|
||
$container.html(html);
|
||
},
|
||
});
|
||
});
|
||
|
||
/************************************************************************
|
||
* This file is part of EspoCRM.
|
||
*
|
||
* EspoCRM – Open Source CRM application.
|
||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||
* Website: https://www.espocrm.com
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU Affero General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Affero General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*
|
||
* The interactive user interfaces in modified source and object code versions
|
||
* of this program must display Appropriate Legal Notices, as required under
|
||
* Section 5 of the GNU Affero General Public License version 3.
|
||
*
|
||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||
************************************************************************/
|
||
|
||
define('crm:views/dashlets/sales-by-month', ['crm:views/dashlets/abstract/chart'], function (Dep) {
|
||
|
||
return Dep.extend({
|
||
|
||
name: 'SalesByMonth',
|
||
|
||
columnWidth: 50,
|
||
|
||
setupDefaultOptions: function () {
|
||
this.defaultOptions['dateFrom'] = this.defaultOptions['dateFrom'] || moment().format('YYYY') + '-01-01';
|
||
this.defaultOptions['dateTo'] = this.defaultOptions['dateTo'] || moment().format('YYYY') + '-12-31';
|
||
},
|
||
|
||
url: function () {
|
||
var url = 'Opportunity/action/reportSalesByMonth?dateFilter='+ this.getDateFilter();
|
||
|
||
if (this.getDateFilter() === 'between') {
|
||
url += '&dateFrom=' + this.getOption('dateFrom') + '&dateTo=' + this.getOption('dateTo');
|
||
}
|
||
|
||
return url;
|
||
},
|
||
|
||
getLegendHeight: function () {
|
||
return 0;
|
||
},
|
||
|
||
isNoData: function () {
|
||
return this.isEmpty;
|
||
},
|
||
|
||
prepareData: function (response) {
|
||
var monthList = this.monthList = response.keyList;
|
||
|
||
var dataMap = response.dataMap || {};
|
||
var values = [];
|
||
|
||
monthList.forEach(month => {
|
||
values.push(dataMap[month]);
|
||
});
|
||
|
||
this.chartData = [];
|
||
|
||
this.isEmpty = true;
|
||
|
||
var mid = 0;
|
||
|
||
if (values.length) {
|
||
mid = values.reduce((a, b) => a + b) / values.length;
|
||
}
|
||
|
||
var data = [];
|
||
var max = 0;
|
||
|
||
values.forEach((value, i) => {
|
||
if (value) {
|
||
this.isEmpty = false;
|
||
}
|
||
|
||
if (value && value > max) {
|
||
max = value;
|
||
}
|
||
|
||
data.push({
|
||
data: [[i, value]],
|
||
color: (value >= mid) ? this.successColor : this.colorBad,
|
||
});
|
||
});
|
||
|
||
this.max = max;
|
||
|
||
return data;
|
||
},
|
||
|
||
setup: function () {
|
||
this.currency = this.getConfig().get('defaultCurrency');
|
||
this.currencySymbol = this.getMetadata().get(['app', 'currency', 'symbolMap', this.currency]) || '';
|
||
|
||
this.colorBad = this.successColor;
|
||
},
|
||
|
||
getTickNumber: function () {
|
||
var containerWidth = this.$container.width();
|
||
|
||
return Math.floor(containerWidth / this.columnWidth * this.fontSizeFactor);
|
||
},
|
||
|
||
draw: function () {
|
||
var tickNumber = this.getTickNumber();
|
||
|
||
this.flotr.draw(this.$container.get(0), this.chartData, {
|
||
shadowSize: false,
|
||
bars: {
|
||
show: true,
|
||
horizontal: false,
|
||
shadowSize: 0,
|
||
lineWidth: 1 * this.fontSizeFactor,
|
||
fillOpacity: 1,
|
||
barWidth: 0.5,
|
||
},
|
||
grid: {
|
||
horizontalLines: true,
|
||
verticalLines: false,
|
||
outline: 'sw',
|
||
color: this.gridColor,
|
||
tickColor: this.tickColor,
|
||
},
|
||
yaxis: {
|
||
min: 0,
|
||
showLabels: true,
|
||
color: this.textColor,
|
||
max: this.max + 0.08 * this.max,
|
||
tickFormatter: (value) => {
|
||
value = parseFloat(value);
|
||
|
||
if (!value) {
|
||
return '';
|
||
}
|
||
|
||
if (value % 1 === 0) {
|
||
return this.currencySymbol +
|
||
'<span class="numeric-text">' +
|
||
this.formatNumber(Math.floor(value), false, true).toString() + '</span>';
|
||
}
|
||
|
||
return '';
|
||
},
|
||
},
|
||
xaxis: {
|
||
min: 0,
|
||
color: this.textColor,
|
||
noTicks: tickNumber,
|
||
tickFormatter: (value) => {
|
||
if (value % 1 === 0) {
|
||
let i = parseInt(value);
|
||
|
||
if (i in this.monthList) {
|
||
if (this.monthList.length - tickNumber > 5 && i === this.monthList.length - 1) {
|
||
return '';
|
||
}
|
||
|
||
return moment(this.monthList[i] + '-01').format('MMM YYYY');
|
||
}
|
||
}
|
||
|
||
return '';
|
||
}
|
||
},
|
||
mouse: {
|
||
track: true,
|
||
relative: true,
|
||
lineColor: this.hoverColor,
|
||
position: 's',
|
||
autoPositionVertical: true,
|
||
trackFormatter: obj => {
|
||
let i = parseInt(obj.x);
|
||
let value = '';
|
||
|
||
if (i in this.monthList) {
|
||
value += moment(this.monthList[i] + '-01').format('MMM YYYY') + '<br>';
|
||
}
|
||
|
||
return value + this.currencySymbol +
|
||
'<span class="numeric-text">' + this.formatNumber(obj.y, true) + '</span>';
|
||
}
|
||
},
|
||
})
|
||
},
|
||
});
|
||
});
|
||
|
||
/************************************************************************
|
||
* This file is part of EspoCRM.
|
||
*
|
||
* EspoCRM – Open Source CRM application.
|
||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||
* Website: https://www.espocrm.com
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU Affero General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Affero General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*
|
||
* The interactive user interfaces in modified source and object code versions
|
||
* of this program must display Appropriate Legal Notices, as required under
|
||
* Section 5 of the GNU Affero General Public License version 3.
|
||
*
|
||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||
************************************************************************/
|
||
|
||
define('crm:views/dashlets/opportunities-by-stage', ['crm:views/dashlets/abstract/chart'], function (Dep) {
|
||
|
||
return Dep.extend({
|
||
|
||
name: 'OpportunitiesByStage',
|
||
|
||
setupDefaultOptions: function () {
|
||
this.defaultOptions['dateFrom'] = this.defaultOptions['dateFrom'] || moment().format('YYYY') + '-01-01';
|
||
this.defaultOptions['dateTo'] = this.defaultOptions['dateTo'] || moment().format('YYYY') + '-12-31';
|
||
},
|
||
|
||
url: function () {
|
||
var url = 'Opportunity/action/reportByStage?dateFilter='+ this.getDateFilter();
|
||
|
||
if (this.getDateFilter() === 'between') {
|
||
url += '&dateFrom=' + this.getOption('dateFrom') + '&dateTo=' + this.getOption('dateTo');
|
||
}
|
||
|
||
return url;
|
||
},
|
||
|
||
prepareData: function (response) {
|
||
let d = [];
|
||
|
||
for (let label in response) {
|
||
var value = response[label];
|
||
|
||
d.push({
|
||
stage: label,
|
||
value: value,
|
||
});
|
||
}
|
||
|
||
this.stageList = [];
|
||
|
||
this.isEmpty = true;
|
||
|
||
var data = [];
|
||
var i = 0;
|
||
|
||
d.forEach(item => {
|
||
if (item.value) {
|
||
this.isEmpty = false;
|
||
}
|
||
|
||
var o = {
|
||
data: [[item.value, d.length - i]],
|
||
label: this.getLanguage().translateOption(item.stage, 'stage', 'Opportunity'),
|
||
}
|
||
|
||
/*if (item.stagsuccessColore === 'Closed Won') {
|
||
o.color = this.successColor;
|
||
}*/
|
||
|
||
data.push(o);
|
||
|
||
this.stageList.push(this.getLanguage().translateOption(item.stage, 'stage', 'Opportunity'));
|
||
i++;
|
||
});
|
||
|
||
let max = 0;
|
||
|
||
if (d.length) {
|
||
d.forEach(item => {
|
||
if ( item.value && item.value > max) {
|
||
max = item.value;
|
||
}
|
||
});
|
||
}
|
||
|
||
this.max = max;
|
||
|
||
return data;
|
||
},
|
||
|
||
setup: function () {
|
||
this.currency = this.getConfig().get('defaultCurrency');
|
||
this.currencySymbol = this.getMetadata().get(['app', 'currency', 'symbolMap', this.currency]) || '';
|
||
},
|
||
|
||
isNoData: function () {
|
||
return this.isEmpty;
|
||
},
|
||
|
||
draw: function () {
|
||
this.flotr.draw(this.$container.get(0), this.chartData, {
|
||
colors: this.colorList,
|
||
shadowSize: false,
|
||
bars: {
|
||
show: true,
|
||
horizontal: true,
|
||
shadowSize: 0,
|
||
lineWidth: 1 * this.fontSizeFactor,
|
||
fillOpacity: 1,
|
||
barWidth: 0.5,
|
||
},
|
||
grid: {
|
||
horizontalLines: false,
|
||
outline: 'sw',
|
||
color: this.gridColor,
|
||
tickColor: this.tickColor,
|
||
},
|
||
yaxis: {
|
||
min: 0,
|
||
showLabels: false,
|
||
color: this.textColor,
|
||
},
|
||
xaxis: {
|
||
min: 0,
|
||
color: this.textColor,
|
||
max: this.max + 0.08 * this.max,
|
||
tickFormatter: value => {
|
||
value = parseFloat(value);
|
||
|
||
if (!value) {
|
||
return '';
|
||
}
|
||
|
||
if (value % 1 === 0) {
|
||
if (value > this.max + 0.05 * this.max) {
|
||
return '';
|
||
}
|
||
|
||
return this.currencySymbol +
|
||
'<span class="numeric-text">' +
|
||
this.formatNumber(Math.floor(value), false, true).toString() +
|
||
'</span>';
|
||
}
|
||
|
||
return '';
|
||
},
|
||
},
|
||
mouse: {
|
||
track: true,
|
||
relative: true,
|
||
position: 'w',
|
||
autoPositionHorizontal: true,
|
||
lineColor: this.hoverColor,
|
||
trackFormatter: obj => {
|
||
let label = this.getHelper().escapeString(obj.series.label || this.translate('None'));
|
||
|
||
return label + '<br>' + this.currencySymbol +
|
||
'<span class="numeric-text">' +
|
||
this.formatNumber(obj.x, true) +
|
||
'</span>';
|
||
},
|
||
},
|
||
legend: {
|
||
show: true,
|
||
noColumns: this.getLegendColumnNumber(),
|
||
container: this.$el.find('.legend-container'),
|
||
labelBoxMargin: 0,
|
||
labelFormatter: this.labelFormatter.bind(this),
|
||
labelBoxBorderColor: 'transparent',
|
||
backgroundOpacity: 0,
|
||
},
|
||
});
|
||
|
||
this.adjustLegend();
|
||
},
|
||
});
|
||
});
|
||
|
||
/************************************************************************
|
||
* This file is part of EspoCRM.
|
||
*
|
||
* EspoCRM – Open Source CRM application.
|
||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||
* Website: https://www.espocrm.com
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU Affero General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Affero General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*
|
||
* The interactive user interfaces in modified source and object code versions
|
||
* of this program must display Appropriate Legal Notices, as required under
|
||
* Section 5 of the GNU Affero General Public License version 3.
|
||
*
|
||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||
************************************************************************/
|
||
|
||
define('crm:views/dashlets/opportunities-by-lead-source', ['crm:views/dashlets/abstract/chart'], function (Dep) {
|
||
|
||
return Dep.extend({
|
||
|
||
name: 'OpportunitiesByLeadSource',
|
||
|
||
url: function () {
|
||
let url = 'Opportunity/action/reportByLeadSource?dateFilter=' + this.getDateFilter();
|
||
|
||
if (this.getDateFilter() === 'between') {
|
||
url += '&dateFrom=' + this.getOption('dateFrom') + '&dateTo=' + this.getOption('dateTo');
|
||
}
|
||
|
||
return url;
|
||
},
|
||
|
||
prepareData: function (response) {
|
||
var data = [];
|
||
|
||
for (var label in response) {
|
||
var value = response[label];
|
||
|
||
data.push({
|
||
label: this.getLanguage().translateOption(label, 'source', 'Lead'),
|
||
data: [[0, value]]
|
||
});
|
||
}
|
||
|
||
return data;
|
||
},
|
||
|
||
isNoData: function () {
|
||
return !this.chartData.length;
|
||
},
|
||
|
||
setupDefaultOptions: function () {
|
||
this.defaultOptions['dateFrom'] = this.defaultOptions['dateFrom'] || moment().format('YYYY') + '-01-01';
|
||
this.defaultOptions['dateTo'] = this.defaultOptions['dateTo'] || moment().format('YYYY') + '-12-31';
|
||
},
|
||
|
||
setup: function () {
|
||
this.currency = this.getConfig().get('defaultCurrency');
|
||
this.currencySymbol = this.getMetadata().get(['app', 'currency', 'symbolMap', this.currency]) || '';
|
||
},
|
||
|
||
draw: function () {
|
||
this.flotr.draw(this.$container.get(0), this.chartData, {
|
||
colors: this.colorList,
|
||
shadowSize: false,
|
||
pie: {
|
||
show: true,
|
||
explode: 0,
|
||
lineWidth: 1 * this.fontSizeFactor,
|
||
fillOpacity: 1,
|
||
sizeRatio: 0.8,
|
||
labelFormatter: (total, value) => {
|
||
const percentage = Math.round(100 * value / total);
|
||
|
||
if (percentage < 5) {
|
||
return '';
|
||
}
|
||
|
||
return '<span class="small numeric-text" style="font-size: 0.8em;color:'+this.textColor+'">' +
|
||
percentage.toString() +'%' + '</span>';
|
||
},
|
||
},
|
||
grid: {
|
||
horizontalLines: false,
|
||
verticalLines: false,
|
||
outline: '',
|
||
tickColor: this.tickColor,
|
||
},
|
||
yaxis: {
|
||
showLabels: false,
|
||
color: this.textColor,
|
||
},
|
||
xaxis: {
|
||
showLabels: false,
|
||
color: this.textColor,
|
||
},
|
||
mouse: {
|
||
track: true,
|
||
relative: true,
|
||
lineColor: this.hoverColor,
|
||
trackFormatter: (obj) => {
|
||
const value = this.currencySymbol +
|
||
'<span class="numeric-text">' + this.formatNumber(obj.y, true) + '</span>';
|
||
|
||
const fraction = obj.fraction || 0;
|
||
const percentage = '<span class="numeric-text">' +
|
||
(100 * fraction).toFixed(2).toString() +'</span>';
|
||
|
||
const label = this.getHelper().escapeString(obj.series.label || this.translate('None'));
|
||
|
||
return label + '<br>' + value + ' / ' + percentage + '%';
|
||
},
|
||
},
|
||
legend: {
|
||
show: true,
|
||
noColumns: this.getLegendColumnNumber(),
|
||
container: this.$el.find('.legend-container'),
|
||
labelBoxMargin: 0,
|
||
labelFormatter: this.labelFormatter.bind(this),
|
||
labelBoxBorderColor: 'transparent',
|
||
backgroundOpacity: 0,
|
||
},
|
||
});
|
||
|
||
this.adjustLegend();
|
||
},
|
||
});
|
||
});
|
||
|