feat: Display how many conversations are considered for the metric calculation (#4273)

* feat: Display how many conversations are considered for the metric calculation
This commit is contained in:
Aswin Dev P.S 2022-03-28 13:08:23 +05:30 committed by GitHub
parent ba0188aefc
commit 0ba6e772a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 379 additions and 47 deletions

View file

@ -18,10 +18,16 @@ class V2::ReportBuilder
# For backward compatible with old report # For backward compatible with old report
def build def build
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| timeseries.each_with_object([]) do |p, arr|
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i } arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
end end
end end
end
def summary def summary
{ {
@ -68,7 +74,7 @@ class V2::ReportBuilder
end end
def get_grouped_values(object_scope) def get_grouped_values(object_scope)
object_scope.group_by_period( @grouped_values = object_scope.group_by_period(
params[:group_by] || DEFAULT_GROUP_BY, params[:group_by] || DEFAULT_GROUP_BY,
:created_at, :created_at,
default_value: 0, default_value: 0,

View file

@ -18,6 +18,13 @@
font-size: $font-size-small; font-size: $font-size-small;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
color: $color-heading; color: $color-heading;
display: flex;
align-items: center;
}
.info-icon {
color: var(--b-400);
margin-left: var(--space-micro);
} }
.metric-wrap { .metric-wrap {

View file

@ -5,7 +5,14 @@
@click="onClick(index)" @click="onClick(index)"
> >
<h3 class="heading"> <h3 class="heading">
{{ heading }} <span>{{ heading }}</span>
<fluent-icon
v-if="infoText"
v-tooltip="infoText"
size="14"
icon="info"
class="info-icon"
/>
</h3> </h3>
<div class="metric-wrap"> <div class="metric-wrap">
<h4 class="metric"> <h4 class="metric">
@ -22,6 +29,7 @@
export default { export default {
props: { props: {
heading: { type: String, default: '' }, heading: { type: String, default: '' },
infoText: { type: String, default: '' },
point: { type: [Number, String], default: '' }, point: { type: [Number, String], default: '' },
trend: { type: Number, default: null }, trend: { type: Number, default: null },
index: { type: Number, default: null }, index: { type: Number, default: null },

View file

@ -3,7 +3,7 @@ import { Bar } from 'vue-chartjs';
const fontFamily = const fontFamily =
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
const chartOptions = { const defaultChartOptions = {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
legend: { legend: {
@ -11,10 +11,14 @@ const chartOptions = {
fontFamily, fontFamily,
}, },
}, },
datasets: {
bar: {
barPercentage: 1.0,
},
},
scales: { scales: {
xAxes: [ xAxes: [
{ {
barPercentage: 1.1,
ticks: { ticks: {
fontFamily, fontFamily,
}, },
@ -39,8 +43,20 @@ const chartOptions = {
export default { export default {
extends: Bar, extends: Bar,
props: ['collection'], props: {
collection: {
type: Object,
default: () => {},
},
chartOptions: {
type: Object,
default: () => {},
},
},
mounted() { mounted() {
this.renderChart(this.collection, chartOptions); this.renderChart(this.collection, {
...defaultChartOptions,
...this.chartOptions,
});
}, },
}; };

View file

@ -18,12 +18,14 @@
"DESC": "( Total )" "DESC": "( Total )"
}, },
"FIRST_RESPONSE_TIME": { "FIRST_RESPONSE_TIME": {
"NAME": "First response time", "NAME": "First Response Time",
"DESC": "( Avg )" "DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:"
}, },
"RESOLUTION_TIME": { "RESOLUTION_TIME": {
"NAME": "Resolution Time", "NAME": "Resolution Time",
"DESC": "( Avg )" "DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:"
}, },
"RESOLUTION_COUNT": { "RESOLUTION_COUNT": {
"NAME": "Resolution Count", "NAME": "Resolution Count",
@ -98,12 +100,14 @@
"DESC": "( Total )" "DESC": "( Total )"
}, },
"FIRST_RESPONSE_TIME": { "FIRST_RESPONSE_TIME": {
"NAME": "First response time", "NAME": "First Response Time",
"DESC": "( Avg )" "DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:"
}, },
"RESOLUTION_TIME": { "RESOLUTION_TIME": {
"NAME": "Resolution Time", "NAME": "Resolution Time",
"DESC": "( Avg )" "DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:"
}, },
"RESOLUTION_COUNT": { "RESOLUTION_COUNT": {
"NAME": "Resolution Count", "NAME": "Resolution Count",
@ -161,12 +165,14 @@
"DESC": "( Total )" "DESC": "( Total )"
}, },
"FIRST_RESPONSE_TIME": { "FIRST_RESPONSE_TIME": {
"NAME": "First response time", "NAME": "First Response Time",
"DESC": "( Avg )" "DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:"
}, },
"RESOLUTION_TIME": { "RESOLUTION_TIME": {
"NAME": "Resolution Time", "NAME": "Resolution Time",
"DESC": "( Avg )" "DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:"
}, },
"RESOLUTION_COUNT": { "RESOLUTION_COUNT": {
"NAME": "Resolution Count", "NAME": "Resolution Count",
@ -224,12 +230,14 @@
"DESC": "( Total )" "DESC": "( Total )"
}, },
"FIRST_RESPONSE_TIME": { "FIRST_RESPONSE_TIME": {
"NAME": "First response time", "NAME": "First Response Time",
"DESC": "( Avg )" "DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:"
}, },
"RESOLUTION_TIME": { "RESOLUTION_TIME": {
"NAME": "Resolution Time", "NAME": "Resolution Time",
"DESC": "( Avg )" "DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:"
}, },
"RESOLUTION_COUNT": { "RESOLUTION_COUNT": {
"NAME": "Resolution Count", "NAME": "Resolution Count",
@ -287,12 +295,14 @@
"DESC": "( Total )" "DESC": "( Total )"
}, },
"FIRST_RESPONSE_TIME": { "FIRST_RESPONSE_TIME": {
"NAME": "First response time", "NAME": "First Response Time",
"DESC": "( Avg )" "DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:"
}, },
"RESOLUTION_TIME": { "RESOLUTION_TIME": {
"NAME": "Resolution Time", "NAME": "Resolution Time",
"DESC": "( Avg )" "DESC": "( Avg )",
"INFO_TEXT": "Total number of conversations used for computation:"
}, },
"RESOLUTION_COUNT": { "RESOLUTION_COUNT": {
"NAME": "Resolution Count", "NAME": "Resolution Count",

View file

@ -5,6 +5,7 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
accountSummary: 'getAccountSummary', accountSummary: 'getAccountSummary',
accountReport: 'getAccountReports',
}), }),
calculateTrend() { calculateTrend() {
return metric_key => { return metric_key => {
@ -19,15 +20,32 @@ export default {
}, },
displayMetric() { displayMetric() {
return metric_key => { return metric_key => {
if ( if (this.isAverageMetricType(metric_key)) {
['avg_first_response_time', 'avg_resolution_time'].includes(
metric_key
)
) {
return formatTime(this.accountSummary[metric_key]); return formatTime(this.accountSummary[metric_key]);
} }
return 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
);
};
},
}, },
}; };

View file

@ -11,6 +11,7 @@ describe('reportMixin', () => {
beforeEach(() => { beforeEach(() => {
getters = { getters = {
getAccountSummary: () => reportFixtures.summary, getAccountSummary: () => reportFixtures.summary,
getAccountReports: () => reportFixtures.report,
}; };
store = new Vuex.Store({ getters }); store = new Vuex.Store({ getters });
}); });
@ -38,4 +39,67 @@ describe('reportMixin', () => {
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25); expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25);
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0); 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('');
});
}); });

View file

@ -15,4 +15,15 @@ export default {
}, },
resolutions_count: 3, 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 },
],
},
}; };

View file

@ -22,6 +22,7 @@
:key="metric.NAME" :key="metric.NAME"
:desc="metric.DESC" :desc="metric.DESC"
:heading="metric.NAME" :heading="metric.NAME"
:info-text="displayInfoText(metric.KEY)"
:index="index" :index="index"
:on-click="changeSelection" :on-click="changeSelection"
:point="displayMetric(metric.KEY)" :point="displayMetric(metric.KEY)"
@ -35,7 +36,11 @@
:message="$t('REPORT.LOADING_CHART')" :message="$t('REPORT.LOADING_CHART')"
/> />
<div v-else class="chart-container"> <div v-else class="chart-container">
<woot-bar v-if="accountReport.data.length" :collection="collection" /> <woot-bar
v-if="accountReport.data.length"
:collection="collection"
:chart-options="chartOptions"
/>
<span v-else class="empty-state"> <span v-else class="empty-state">
{{ $t('REPORT.NO_ENOUGH_DATA') }} {{ $t('REPORT.NO_ENOUGH_DATA') }}
</span> </span>
@ -49,7 +54,7 @@ import { mapGetters } from 'vuex';
import fromUnixTime from 'date-fns/fromUnixTime'; import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format'; import format from 'date-fns/format';
import ReportFilterSelector from './components/FilterSelector'; import ReportFilterSelector from './components/FilterSelector';
import { GROUP_BY_FILTER } from './constants'; import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
import reportMixin from '../../../../mixins/reportMixin'; import reportMixin from '../../../../mixins/reportMixin';
const REPORTS_KEYS = { const REPORTS_KEYS = {
@ -108,16 +113,38 @@ export default {
} }
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy'); 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 { return {
labels, labels,
datasets: [ datasets,
{ };
label: this.metrics[this.currentSelection].NAME,
backgroundColor: '#1f93ff',
data,
}, },
], chartOptions() {
return {
scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales,
}; };
}, },
metrics() { metrics() {
@ -133,6 +160,7 @@ export default {
NAME: this.$t(`REPORT.METRICS.${key}.NAME`), NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
KEY: REPORTS_KEYS[key], KEY: REPORTS_KEYS[key],
DESC: this.$t(`REPORT.METRICS.${key}.DESC`), DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
INFO_TEXT: this.$t(`REPORT.METRICS.${key}.INFO_TEXT`),
})); }));
}, },
}, },

View file

@ -25,6 +25,7 @@
:key="metric.NAME" :key="metric.NAME"
:desc="metric.DESC" :desc="metric.DESC"
:heading="metric.NAME" :heading="metric.NAME"
:info-text="displayInfoText(metric.KEY)"
:index="index" :index="index"
:on-click="changeSelection" :on-click="changeSelection"
:point="displayMetric(metric.KEY)" :point="displayMetric(metric.KEY)"
@ -41,6 +42,7 @@
<woot-bar <woot-bar
v-if="accountReport.data.length && filterItemsList.length" v-if="accountReport.data.length && filterItemsList.length"
:collection="collection" :collection="collection"
:chart-options="chartOptions"
/> />
<span v-else class="empty-state"> <span v-else class="empty-state">
{{ $t('REPORT.NO_ENOUGH_DATA') }} {{ $t('REPORT.NO_ENOUGH_DATA') }}
@ -55,7 +57,7 @@
import ReportFilters from './ReportFilters'; import ReportFilters from './ReportFilters';
import fromUnixTime from 'date-fns/fromUnixTime'; import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format'; 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'; import reportMixin from '../../../../../mixins/reportMixin';
const REPORTS_KEYS = { const REPORTS_KEYS = {
@ -137,16 +139,38 @@ export default {
} }
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy'); 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 { return {
labels, labels,
datasets: [ datasets,
{ };
label: this.metrics[this.currentSelection].NAME,
backgroundColor: '#1f93ff',
data,
}, },
], chartOptions() {
return {
scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales,
}; };
}, },
metrics() { metrics() {
@ -168,6 +192,7 @@ export default {
NAME: this.$t(`REPORT.METRICS.${key}.NAME`), NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
KEY: REPORTS_KEYS[key], KEY: REPORTS_KEYS[key],
DESC: this.$t(`REPORT.METRICS.${key}.DESC`), DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
INFO_TEXT: this.$t(`REPORT.METRICS.${key}.INFO_TEXT`),
})); }));
}, },
}, },

View file

@ -4,3 +4,142 @@ export const GROUP_BY_FILTER = {
3: { id: 3, period: 'month' }, 3: { id: 3, period: 'month' },
4: { id: 4, period: 'year' }, 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,
};