/************************************************************************ * 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 . * * 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: '
', 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 '' + v + ''; }, 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 = $('').html(this.translate('No Data')).addClass('text-muted'); const $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 . * * 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 + '
' + this.currencySymbol + '' + this.formatNumber(value, true) + ''; }, }, 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 = ''; this.chartData.forEach((item, i) => { if (i % number === 0) { if (i > 0) { html += ''; } html += ''; } let stageTranslated = this.getHelper().escapeString(item.stageTranslated); let box = '
'+ '
'+ '
'; html += ''; html += ''; }); html += ''; html += '
' + box + ''+ stageTranslated + '
'; $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 . * * 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 + '' + this.formatNumber(Math.floor(value), false, true).toString() + ''; } 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') + '
'; } return value + this.currencySymbol + '' + this.formatNumber(obj.y, true) + ''; } }, }) }, }); }); /************************************************************************ * 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 . * * 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 + '' + this.formatNumber(Math.floor(value), false, true).toString() + ''; } 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 + '
' + this.currencySymbol + '' + this.formatNumber(obj.x, true) + ''; }, }, 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 . * * 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 '' + percentage.toString() +'%' + ''; }, }, 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 + '' + this.formatNumber(obj.y, true) + ''; const fraction = obj.fraction || 0; const percentage = '' + (100 * fraction).toFixed(2).toString() +''; const label = this.getHelper().escapeString(obj.series.label || this.translate('None')); return label + '
' + 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(); }, }); });