diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index c45226397..b1c09cb63 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -18,8 +18,14 @@ class V2::ReportBuilder # For backward compatible with old report def build - timeseries.each_with_object([]) do |p, arr| - arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i } + if %w[avg_first_response_time avg_resolution_time].include?(params[:metric]) + timeseries.each_with_object([]) do |p, arr| + arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] } + end + else + timeseries.each_with_object([]) do |p, arr| + arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i } + end end end @@ -68,7 +74,7 @@ class V2::ReportBuilder end def get_grouped_values(object_scope) - object_scope.group_by_period( + @grouped_values = object_scope.group_by_period( params[:group_by] || DEFAULT_GROUP_BY, :created_at, default_value: 0, diff --git a/app/javascript/dashboard/assets/scss/widgets/_report.scss b/app/javascript/dashboard/assets/scss/widgets/_report.scss index 6e8b2fe5c..444dccb7c 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_report.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_report.scss @@ -18,6 +18,13 @@ font-size: $font-size-small; font-weight: $font-weight-bold; color: $color-heading; + display: flex; + align-items: center; + } + + .info-icon { + color: var(--b-400); + margin-left: var(--space-micro); } .metric-wrap { diff --git a/app/javascript/dashboard/components/widgets/ReportStatsCard.vue b/app/javascript/dashboard/components/widgets/ReportStatsCard.vue index 9bc46d021..a074ca857 100644 --- a/app/javascript/dashboard/components/widgets/ReportStatsCard.vue +++ b/app/javascript/dashboard/components/widgets/ReportStatsCard.vue @@ -5,7 +5,14 @@ @click="onClick(index)" >

- {{ heading }} + {{ heading }} +

@@ -22,6 +29,7 @@ export default { props: { heading: { type: String, default: '' }, + infoText: { type: String, default: '' }, point: { type: [Number, String], default: '' }, trend: { type: Number, default: null }, index: { type: Number, default: null }, diff --git a/app/javascript/dashboard/components/widgets/chart/BarChart.js b/app/javascript/dashboard/components/widgets/chart/BarChart.js index fbe42bc5c..a4dca263b 100644 --- a/app/javascript/dashboard/components/widgets/chart/BarChart.js +++ b/app/javascript/dashboard/components/widgets/chart/BarChart.js @@ -3,7 +3,7 @@ import { Bar } from 'vue-chartjs'; const fontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; -const chartOptions = { +const defaultChartOptions = { responsive: true, maintainAspectRatio: false, legend: { @@ -11,10 +11,14 @@ const chartOptions = { fontFamily, }, }, + datasets: { + bar: { + barPercentage: 1.0, + }, + }, scales: { xAxes: [ { - barPercentage: 1.1, ticks: { fontFamily, }, @@ -39,8 +43,20 @@ const chartOptions = { export default { extends: Bar, - props: ['collection'], + props: { + collection: { + type: Object, + default: () => {}, + }, + chartOptions: { + type: Object, + default: () => {}, + }, + }, mounted() { - this.renderChart(this.collection, chartOptions); + this.renderChart(this.collection, { + ...defaultChartOptions, + ...this.chartOptions, + }); }, }; diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index 053c730e1..ed730d798 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -18,12 +18,14 @@ "DESC": "( Total )" }, "FIRST_RESPONSE_TIME": { - "NAME": "First response time", - "DESC": "( Avg )" + "NAME": "First Response Time", + "DESC": "( Avg )", + "INFO_TEXT": "Total number of conversations used for computation:" }, "RESOLUTION_TIME": { "NAME": "Resolution Time", - "DESC": "( Avg )" + "DESC": "( Avg )", + "INFO_TEXT": "Total number of conversations used for computation:" }, "RESOLUTION_COUNT": { "NAME": "Resolution Count", @@ -98,12 +100,14 @@ "DESC": "( Total )" }, "FIRST_RESPONSE_TIME": { - "NAME": "First response time", - "DESC": "( Avg )" + "NAME": "First Response Time", + "DESC": "( Avg )", + "INFO_TEXT": "Total number of conversations used for computation:" }, "RESOLUTION_TIME": { "NAME": "Resolution Time", - "DESC": "( Avg )" + "DESC": "( Avg )", + "INFO_TEXT": "Total number of conversations used for computation:" }, "RESOLUTION_COUNT": { "NAME": "Resolution Count", @@ -161,12 +165,14 @@ "DESC": "( Total )" }, "FIRST_RESPONSE_TIME": { - "NAME": "First response time", - "DESC": "( Avg )" + "NAME": "First Response Time", + "DESC": "( Avg )", + "INFO_TEXT": "Total number of conversations used for computation:" }, "RESOLUTION_TIME": { "NAME": "Resolution Time", - "DESC": "( Avg )" + "DESC": "( Avg )", + "INFO_TEXT": "Total number of conversations used for computation:" }, "RESOLUTION_COUNT": { "NAME": "Resolution Count", @@ -224,12 +230,14 @@ "DESC": "( Total )" }, "FIRST_RESPONSE_TIME": { - "NAME": "First response time", - "DESC": "( Avg )" + "NAME": "First Response Time", + "DESC": "( Avg )", + "INFO_TEXT": "Total number of conversations used for computation:" }, "RESOLUTION_TIME": { "NAME": "Resolution Time", - "DESC": "( Avg )" + "DESC": "( Avg )", + "INFO_TEXT": "Total number of conversations used for computation:" }, "RESOLUTION_COUNT": { "NAME": "Resolution Count", @@ -287,12 +295,14 @@ "DESC": "( Total )" }, "FIRST_RESPONSE_TIME": { - "NAME": "First response time", - "DESC": "( Avg )" + "NAME": "First Response Time", + "DESC": "( Avg )", + "INFO_TEXT": "Total number of conversations used for computation:" }, "RESOLUTION_TIME": { "NAME": "Resolution Time", - "DESC": "( Avg )" + "DESC": "( Avg )", + "INFO_TEXT": "Total number of conversations used for computation:" }, "RESOLUTION_COUNT": { "NAME": "Resolution Count", diff --git a/app/javascript/dashboard/mixins/reportMixin.js b/app/javascript/dashboard/mixins/reportMixin.js index 8ef7fdfe5..d5bb8c9cc 100644 --- a/app/javascript/dashboard/mixins/reportMixin.js +++ b/app/javascript/dashboard/mixins/reportMixin.js @@ -5,6 +5,7 @@ export default { computed: { ...mapGetters({ accountSummary: 'getAccountSummary', + accountReport: 'getAccountReports', }), calculateTrend() { return metric_key => { @@ -19,15 +20,32 @@ export default { }, displayMetric() { return metric_key => { - if ( - ['avg_first_response_time', 'avg_resolution_time'].includes( - metric_key - ) - ) { + if (this.isAverageMetricType(metric_key)) { return formatTime(this.accountSummary[metric_key]); } return this.accountSummary[metric_key]; }; }, + displayInfoText() { + return metric_key => { + if (this.metrics[this.currentSelection].KEY !== metric_key) { + return ''; + } + if (this.isAverageMetricType(metric_key)) { + const total = this.accountReport.data + .map(item => item.count) + .reduce((prev, curr) => prev + curr, 0); + return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`; + } + return ''; + }; + }, + isAverageMetricType() { + return metric_key => { + return ['avg_first_response_time', 'avg_resolution_time'].includes( + metric_key + ); + }; + }, }, }; diff --git a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js index 003295f95..ca1c45751 100644 --- a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js +++ b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js @@ -11,6 +11,7 @@ describe('reportMixin', () => { beforeEach(() => { getters = { getAccountSummary: () => reportFixtures.summary, + getAccountReports: () => reportFixtures.report, }; store = new Vuex.Store({ getters }); }); @@ -38,4 +39,67 @@ describe('reportMixin', () => { expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25); expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0); }); + + it('display info text', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [reportMixin], + data() { + return { + currentSelection: 0, + }; + }, + computed: { + metrics() { + return [ + { + DESC: '( Avg )', + INFO_TEXT: 'Total number of conversations used for computation:', + KEY: 'avg_first_response_time', + NAME: 'First Response Time', + }, + ]; + }, + }, + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect(wrapper.vm.displayInfoText('avg_first_response_time')).toEqual( + 'Total number of conversations used for computation: 4' + ); + }); + + it('do not display info text', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [reportMixin], + data() { + return { + currentSelection: 0, + }; + }, + computed: { + metrics() { + return [ + { + DESC: '( Total )', + INFO_TEXT: '', + KEY: 'conversation_count', + NAME: 'Conversations', + }, + { + DESC: '( Avg )', + INFO_TEXT: 'Total number of conversations used for computation:', + KEY: 'avg_first_response_time', + NAME: 'First Response Time', + }, + ]; + }, + }, + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect(wrapper.vm.displayInfoText('conversation_count')).toEqual(''); + expect(wrapper.vm.displayInfoText('incoming_messages_count')).toEqual(''); + }); }); diff --git a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js index 5c8315ab1..8402c3940 100644 --- a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js +++ b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js @@ -15,4 +15,15 @@ export default { }, resolutions_count: 3, }, + report: { + data: [ + { value: '0.00', timestamp: 1647541800, count: 0 }, + { value: '0.00', timestamp: 1647628200, count: 0 }, + { value: '0.00', timestamp: 1647714600, count: 0 }, + { value: '0.00', timestamp: 1647801000, count: 0 }, + { value: '0.01', timestamp: 1647887400, count: 4 }, + { value: '0.00', timestamp: 1647973800, count: 0 }, + { value: '0.00', timestamp: 1648060200, count: 0 }, + ], + }, }; diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue index fe0240d74..1b46c474a 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue @@ -22,6 +22,7 @@ :key="metric.NAME" :desc="metric.DESC" :heading="metric.NAME" + :info-text="displayInfoText(metric.KEY)" :index="index" :on-click="changeSelection" :point="displayMetric(metric.KEY)" @@ -35,7 +36,11 @@ :message="$t('REPORT.LOADING_CHART')" />
- + {{ $t('REPORT.NO_ENOUGH_DATA') }} @@ -49,7 +54,7 @@ import { mapGetters } from 'vuex'; import fromUnixTime from 'date-fns/fromUnixTime'; import format from 'date-fns/format'; import ReportFilterSelector from './components/FilterSelector'; -import { GROUP_BY_FILTER } from './constants'; +import { GROUP_BY_FILTER, METRIC_CHART } from './constants'; import reportMixin from '../../../../mixins/reportMixin'; const REPORTS_KEYS = { @@ -108,16 +113,38 @@ export default { } return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy'); }); - const data = this.accountReport.data.map(element => element.value); + + const datasets = METRIC_CHART[ + this.metrics[this.currentSelection].KEY + ].datasets.map(dataset => { + switch (dataset.type) { + case 'bar': + return { + ...dataset, + yAxisID: 'y-left', + label: this.metrics[this.currentSelection].NAME, + data: this.accountReport.data.map(element => element.value), + }; + case 'line': + return { + ...dataset, + yAxisID: 'y-right', + label: this.metrics[0].NAME, + data: this.accountReport.data.map(element => element.count), + }; + default: + return dataset; + } + }); + return { labels, - datasets: [ - { - label: this.metrics[this.currentSelection].NAME, - backgroundColor: '#1f93ff', - data, - }, - ], + datasets, + }; + }, + chartOptions() { + return { + scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales, }; }, metrics() { @@ -133,6 +160,7 @@ export default { NAME: this.$t(`REPORT.METRICS.${key}.NAME`), KEY: REPORTS_KEYS[key], DESC: this.$t(`REPORT.METRICS.${key}.DESC`), + INFO_TEXT: this.$t(`REPORT.METRICS.${key}.INFO_TEXT`), })); }, }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue index 8ce264080..c5cf3993b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue @@ -25,6 +25,7 @@ :key="metric.NAME" :desc="metric.DESC" :heading="metric.NAME" + :info-text="displayInfoText(metric.KEY)" :index="index" :on-click="changeSelection" :point="displayMetric(metric.KEY)" @@ -41,6 +42,7 @@ {{ $t('REPORT.NO_ENOUGH_DATA') }} @@ -55,7 +57,7 @@ import ReportFilters from './ReportFilters'; import fromUnixTime from 'date-fns/fromUnixTime'; import format from 'date-fns/format'; -import { GROUP_BY_FILTER } from '../constants'; +import { GROUP_BY_FILTER, METRIC_CHART } from '../constants'; import reportMixin from '../../../../../mixins/reportMixin'; const REPORTS_KEYS = { @@ -137,16 +139,38 @@ export default { } return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy'); }); - const data = this.accountReport.data.map(element => element.value); + + const datasets = METRIC_CHART[ + this.metrics[this.currentSelection].KEY + ].datasets.map(dataset => { + switch (dataset.type) { + case 'bar': + return { + ...dataset, + yAxisID: 'y-left', + label: this.metrics[this.currentSelection].NAME, + data: this.accountReport.data.map(element => element.value), + }; + case 'line': + return { + ...dataset, + yAxisID: 'y-right', + label: this.metrics[0].NAME, + data: this.accountReport.data.map(element => element.count), + }; + default: + return dataset; + } + }); + return { labels, - datasets: [ - { - label: this.metrics[this.currentSelection].NAME, - backgroundColor: '#1f93ff', - data, - }, - ], + datasets, + }; + }, + chartOptions() { + return { + scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales, }; }, metrics() { @@ -168,6 +192,7 @@ export default { NAME: this.$t(`REPORT.METRICS.${key}.NAME`), KEY: REPORTS_KEYS[key], DESC: this.$t(`REPORT.METRICS.${key}.DESC`), + INFO_TEXT: this.$t(`REPORT.METRICS.${key}.INFO_TEXT`), })); }, }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js index f285061cb..91348543e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js @@ -4,3 +4,142 @@ export const GROUP_BY_FILTER = { 3: { id: 3, period: 'month' }, 4: { id: 4, period: 'year' }, }; + +export const CHART_FONT_FAMILY = + '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; + +export const DEFAULT_LINE_CHART = { + type: 'line', + fill: false, + borderColor: '#779BBB', + pointBackgroundColor: '#779BBB', +}; + +export const DEFAULT_BAR_CHART = { + type: 'bar', + backgroundColor: 'rgb(31, 147, 255, 0.5)', +}; + +export const DEFAULT_CHART = { + datasets: [DEFAULT_BAR_CHART], + scales: { + xAxes: [ + { + ticks: { + fontFamily: CHART_FONT_FAMILY, + }, + gridLines: { + drawOnChartArea: false, + }, + }, + ], + yAxes: [ + { + id: 'y-left', + type: 'linear', + position: 'left', + ticks: { + fontFamily: CHART_FONT_FAMILY, + beginAtZero: true, + stepSize: 1, + }, + gridLines: { + drawOnChartArea: false, + }, + }, + ], + }, +}; + +export const METRIC_CHART = { + conversations_count: DEFAULT_CHART, + incoming_messages_count: DEFAULT_CHART, + outgoing_messages_count: DEFAULT_CHART, + avg_first_response_time: { + datasets: [DEFAULT_BAR_CHART, DEFAULT_LINE_CHART], + scales: { + xAxes: [ + { + ticks: { + fontFamily: CHART_FONT_FAMILY, + }, + gridLines: { + drawOnChartArea: false, + }, + }, + ], + yAxes: [ + { + id: 'y-left', + type: 'linear', + position: 'left', + ticks: { + fontFamily: CHART_FONT_FAMILY, + beginAtZero: true, + precision: 2, + }, + gridLines: { + drawOnChartArea: false, + }, + }, + { + id: 'y-right', + type: 'linear', + position: 'right', + ticks: { + fontFamily: CHART_FONT_FAMILY, + beginAtZero: true, + stepSize: 1, + }, + gridLines: { + drawOnChartArea: false, + }, + }, + ], + }, + }, + avg_resolution_time: { + datasets: [DEFAULT_BAR_CHART, DEFAULT_LINE_CHART], + scales: { + xAxes: [ + { + ticks: { + fontFamily: CHART_FONT_FAMILY, + }, + gridLines: { + drawOnChartArea: false, + }, + }, + ], + yAxes: [ + { + id: 'y-left', + type: 'linear', + position: 'left', + ticks: { + fontFamily: CHART_FONT_FAMILY, + beginAtZero: true, + precision: 2, + }, + gridLines: { + drawOnChartArea: false, + }, + }, + { + id: 'y-right', + type: 'linear', + position: 'right', + ticks: { + fontFamily: CHART_FONT_FAMILY, + beginAtZero: true, + stepSize: 1, + }, + gridLines: { + drawOnChartArea: false, + }, + }, + ], + }, + }, + resolutions_count: DEFAULT_CHART, +};