feat: Display trends in report metrics (#4144)
This commit is contained in:
parent
5edf0f2bbe
commit
c62d74a01d
11 changed files with 235 additions and 29 deletions
|
@ -41,12 +41,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
|
||||
def summary_params
|
||||
def current_summary_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id],
|
||||
since: range[:current][:since],
|
||||
until: range[:current][:until],
|
||||
group_by: params[:group_by]
|
||||
}
|
||||
end
|
||||
|
||||
def previous_summary_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
id: params[:id],
|
||||
since: range[:previous][:since],
|
||||
until: range[:previous][:until],
|
||||
group_by: params[:group_by]
|
||||
}
|
||||
end
|
||||
|
@ -63,8 +73,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
}
|
||||
end
|
||||
|
||||
def range
|
||||
{
|
||||
current: {
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
},
|
||||
previous: {
|
||||
since: (params[:since].to_i - (params[:until].to_i - params[:since].to_i)).to_s,
|
||||
until: params[:since]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def summary_metrics
|
||||
builder = V2::ReportBuilder.new(Current.account, summary_params)
|
||||
builder.summary
|
||||
summary = V2::ReportBuilder.new(Current.account, current_summary_params).summary
|
||||
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
|
||||
summary
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,12 +20,30 @@
|
|||
color: $color-heading;
|
||||
}
|
||||
|
||||
.metric-wrap {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: $font-size-big;
|
||||
font-weight: $font-weight-feather;
|
||||
margin-top: $space-smaller;
|
||||
}
|
||||
|
||||
.metric-trend {
|
||||
font-size: $font-size-small;
|
||||
margin-left: $space-small;
|
||||
}
|
||||
|
||||
.metric-up {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
.metric-down {
|
||||
color: $alert-color;
|
||||
}
|
||||
|
||||
.desc {
|
||||
@include margin($zero);
|
||||
font-size: $font-size-small;
|
||||
|
|
|
@ -7,9 +7,12 @@
|
|||
<h3 class="heading">
|
||||
{{ heading }}
|
||||
</h3>
|
||||
<h4 class="metric">
|
||||
{{ point }}
|
||||
</h4>
|
||||
<div class="metric-wrap">
|
||||
<h4 class="metric">
|
||||
{{ point }}
|
||||
</h4>
|
||||
<span v-if="trend !== 0" :class="trendClass">{{ trendValue }}</span>
|
||||
</div>
|
||||
<p class="desc">
|
||||
{{ desc }}
|
||||
</p>
|
||||
|
@ -20,10 +23,27 @@ export default {
|
|||
props: {
|
||||
heading: { type: String, default: '' },
|
||||
point: { type: [Number, String], default: '' },
|
||||
trend: { type: Number, default: null },
|
||||
index: { type: Number, default: null },
|
||||
desc: { type: String, default: '' },
|
||||
selected: Boolean,
|
||||
onClick: { type: Function, default: () => {} },
|
||||
},
|
||||
computed: {
|
||||
trendClass() {
|
||||
if (this.trend > 0) {
|
||||
return 'metric-trend metric-up';
|
||||
}
|
||||
|
||||
return 'metric-trend metric-down';
|
||||
},
|
||||
trendValue() {
|
||||
if (this.trend > 0) {
|
||||
return `+${this.trend}%`;
|
||||
}
|
||||
|
||||
return `${this.trend}%`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
33
app/javascript/dashboard/mixins/reportMixin.js
Normal file
33
app/javascript/dashboard/mixins/reportMixin.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountSummary: 'getAccountSummary',
|
||||
}),
|
||||
calculateTrend() {
|
||||
return metric_key => {
|
||||
if (!this.accountSummary.previous[metric_key]) return 0;
|
||||
return Math.round(
|
||||
((this.accountSummary[metric_key] -
|
||||
this.accountSummary.previous[metric_key]) /
|
||||
this.accountSummary.previous[metric_key]) *
|
||||
100
|
||||
);
|
||||
};
|
||||
},
|
||||
displayMetric() {
|
||||
return metric_key => {
|
||||
if (
|
||||
['avg_first_response_time', 'avg_resolution_time'].includes(
|
||||
metric_key
|
||||
)
|
||||
) {
|
||||
return formatTime(this.accountSummary[metric_key]);
|
||||
}
|
||||
return this.accountSummary[metric_key];
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
41
app/javascript/dashboard/mixins/specs/reportMixin.spec.js
Normal file
41
app/javascript/dashboard/mixins/specs/reportMixin.spec.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import reportMixin from '../reportMixin';
|
||||
import reportFixtures from './reportMixinFixtures';
|
||||
import Vuex from 'vuex';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('reportMixin', () => {
|
||||
let getters;
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
getters = {
|
||||
getAccountSummary: () => reportFixtures.summary,
|
||||
};
|
||||
store = new Vuex.Store({ getters });
|
||||
});
|
||||
|
||||
it('display the metric', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayMetric('conversations_count')).toEqual(5);
|
||||
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
||||
'3 Min'
|
||||
);
|
||||
});
|
||||
|
||||
it('calculate the trend', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25);
|
||||
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
|
||||
});
|
||||
});
|
18
app/javascript/dashboard/mixins/specs/reportMixinFixtures.js
Normal file
18
app/javascript/dashboard/mixins/specs/reportMixinFixtures.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
export default {
|
||||
summary: {
|
||||
avg_first_response_time: '198.6666666666667',
|
||||
avg_resolution_time: '208.3333333333333',
|
||||
conversations_count: 5,
|
||||
incoming_messages_count: 5,
|
||||
outgoing_messages_count: 3,
|
||||
previous: {
|
||||
avg_first_response_time: '89.0',
|
||||
avg_resolution_time: '145.0',
|
||||
conversations_count: 4,
|
||||
incoming_messages_count: 5,
|
||||
outgoing_messages_count: 4,
|
||||
resolutions_count: 0,
|
||||
},
|
||||
resolutions_count: 3,
|
||||
},
|
||||
};
|
|
@ -24,7 +24,8 @@
|
|||
:heading="metric.NAME"
|
||||
:index="index"
|
||||
:on-click="changeSelection"
|
||||
:point="accountSummary[metric.KEY]"
|
||||
:point="displayMetric(metric.KEY)"
|
||||
:trend="calculateTrend(metric.KEY)"
|
||||
:selected="index === currentSelection"
|
||||
/>
|
||||
</div>
|
||||
|
@ -49,6 +50,7 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
|||
import format from 'date-fns/format';
|
||||
import ReportFilterSelector from './components/FilterSelector';
|
||||
import { GROUP_BY_FILTER } from './constants';
|
||||
import reportMixin from '../../../../mixins/reportMixin';
|
||||
|
||||
const REPORTS_KEYS = {
|
||||
CONVERSATIONS: 'conversations_count',
|
||||
|
@ -63,6 +65,7 @@ export default {
|
|||
components: {
|
||||
ReportFilterSelector,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
|
|
|
@ -27,7 +27,8 @@
|
|||
:heading="metric.NAME"
|
||||
:index="index"
|
||||
:on-click="changeSelection"
|
||||
:point="accountSummary[metric.KEY]"
|
||||
:point="displayMetric(metric.KEY)"
|
||||
:trend="calculateTrend(metric.KEY)"
|
||||
:selected="index === currentSelection"
|
||||
/>
|
||||
</div>
|
||||
|
@ -55,6 +56,7 @@ import ReportFilters from './ReportFilters';
|
|||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import format from 'date-fns/format';
|
||||
import { GROUP_BY_FILTER } from '../constants';
|
||||
import reportMixin from '../../../../../mixins/reportMixin';
|
||||
|
||||
const REPORTS_KEYS = {
|
||||
CONVERSATIONS: 'conversations_count',
|
||||
|
@ -68,6 +70,7 @@ export default {
|
|||
components: {
|
||||
ReportFilters,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
|
|
|
@ -5,7 +5,6 @@ import * as types from '../mutation-types';
|
|||
import Report from '../../api/reports';
|
||||
|
||||
import { downloadCsvFile } from '../../helper/downloadCsvFile';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
|
||||
const state = {
|
||||
fetchingStatus: false,
|
||||
|
@ -21,6 +20,7 @@ const state = {
|
|||
incoming_messages_count: 0,
|
||||
outgoing_messages_count: 0,
|
||||
resolutions_count: 0,
|
||||
previous: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -125,18 +125,6 @@ const mutations = {
|
|||
},
|
||||
[types.default.SET_ACCOUNT_SUMMARY](_state, summaryData) {
|
||||
_state.accountSummary = summaryData;
|
||||
// Average First Response Time
|
||||
let avgFirstResTimeInHr = 0;
|
||||
if (summaryData.avg_first_response_time) {
|
||||
avgFirstResTimeInHr = formatTime(summaryData.avg_first_response_time);
|
||||
}
|
||||
// Average Resolution Time
|
||||
let avgResolutionTimeInHr = 0;
|
||||
if (summaryData.avg_resolution_time) {
|
||||
avgResolutionTimeInHr = formatTime(summaryData.avg_resolution_time);
|
||||
}
|
||||
_state.accountSummary.avg_first_response_time = avgFirstResTimeInHr;
|
||||
_state.accountSummary.avg_resolution_time = avgResolutionTimeInHr;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,29 @@
|
|||
type: object
|
||||
properties:
|
||||
value:
|
||||
type: number
|
||||
timestamp:
|
||||
avg_first_response_time:
|
||||
type: string
|
||||
avg_resolution_time:
|
||||
type: string
|
||||
conversations_count:
|
||||
type: number
|
||||
incoming_messages_count:
|
||||
type: number
|
||||
outgoing_messages_count:
|
||||
type: number
|
||||
resolutions_count:
|
||||
type: number
|
||||
previous:
|
||||
type: object
|
||||
properties:
|
||||
avg_first_response_time:
|
||||
type: string
|
||||
avg_resolution_time:
|
||||
type: string
|
||||
conversations_count:
|
||||
type: number
|
||||
incoming_messages_count:
|
||||
type: number
|
||||
outgoing_messages_count:
|
||||
type: number
|
||||
resolutions_count:
|
||||
type: number
|
|
@ -4692,11 +4692,46 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"avg_first_response_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
"incoming_messages_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"resolutions_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"previous": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avg_first_response_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"incoming_messages_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"resolutions_count": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue