feat: Display trends in report metrics (#4144)

This commit is contained in:
Aswin Dev P.S 2022-03-14 18:15:27 +05:30 committed by GitHub
parent 5edf0f2bbe
commit c62d74a01d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 235 additions and 29 deletions

View file

@ -41,12 +41,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
raise Pundit::NotAuthorizedError unless Current.account_user.administrator? raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end end
def summary_params def current_summary_params
{ {
type: params[:type].to_sym, type: params[:type].to_sym,
since: params[:since],
until: params[:until],
id: params[:id], 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] group_by: params[:group_by]
} }
end end
@ -63,8 +73,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
} }
end 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 def summary_metrics
builder = V2::ReportBuilder.new(Current.account, summary_params) summary = V2::ReportBuilder.new(Current.account, current_summary_params).summary
builder.summary summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
summary
end end
end end

View file

@ -20,12 +20,30 @@
color: $color-heading; color: $color-heading;
} }
.metric-wrap {
align-items: baseline;
display: flex;
}
.metric { .metric {
font-size: $font-size-big; font-size: $font-size-big;
font-weight: $font-weight-feather; font-weight: $font-weight-feather;
margin-top: $space-smaller; 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 { .desc {
@include margin($zero); @include margin($zero);
font-size: $font-size-small; font-size: $font-size-small;

View file

@ -7,9 +7,12 @@
<h3 class="heading"> <h3 class="heading">
{{ heading }} {{ heading }}
</h3> </h3>
<h4 class="metric"> <div class="metric-wrap">
{{ point }} <h4 class="metric">
</h4> {{ point }}
</h4>
<span v-if="trend !== 0" :class="trendClass">{{ trendValue }}</span>
</div>
<p class="desc"> <p class="desc">
{{ desc }} {{ desc }}
</p> </p>
@ -20,10 +23,27 @@ export default {
props: { props: {
heading: { type: String, default: '' }, heading: { type: String, default: '' },
point: { type: [Number, String], default: '' }, point: { type: [Number, String], default: '' },
trend: { type: Number, default: null },
index: { type: Number, default: null }, index: { type: Number, default: null },
desc: { type: String, default: '' }, desc: { type: String, default: '' },
selected: Boolean, selected: Boolean,
onClick: { type: Function, default: () => {} }, 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> </script>

View 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];
};
},
},
};

View 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);
});
});

View 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,
},
};

View file

@ -24,7 +24,8 @@
:heading="metric.NAME" :heading="metric.NAME"
:index="index" :index="index"
:on-click="changeSelection" :on-click="changeSelection"
:point="accountSummary[metric.KEY]" :point="displayMetric(metric.KEY)"
:trend="calculateTrend(metric.KEY)"
:selected="index === currentSelection" :selected="index === currentSelection"
/> />
</div> </div>
@ -49,6 +50,7 @@ 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 } from './constants';
import reportMixin from '../../../../mixins/reportMixin';
const REPORTS_KEYS = { const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count', CONVERSATIONS: 'conversations_count',
@ -63,6 +65,7 @@ export default {
components: { components: {
ReportFilterSelector, ReportFilterSelector,
}, },
mixins: [reportMixin],
data() { data() {
return { return {
from: 0, from: 0,

View file

@ -27,7 +27,8 @@
:heading="metric.NAME" :heading="metric.NAME"
:index="index" :index="index"
:on-click="changeSelection" :on-click="changeSelection"
:point="accountSummary[metric.KEY]" :point="displayMetric(metric.KEY)"
:trend="calculateTrend(metric.KEY)"
:selected="index === currentSelection" :selected="index === currentSelection"
/> />
</div> </div>
@ -55,6 +56,7 @@ 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 } from '../constants';
import reportMixin from '../../../../../mixins/reportMixin';
const REPORTS_KEYS = { const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count', CONVERSATIONS: 'conversations_count',
@ -68,6 +70,7 @@ export default {
components: { components: {
ReportFilters, ReportFilters,
}, },
mixins: [reportMixin],
props: { props: {
type: { type: {
type: String, type: String,

View file

@ -5,7 +5,6 @@ import * as types from '../mutation-types';
import Report from '../../api/reports'; import Report from '../../api/reports';
import { downloadCsvFile } from '../../helper/downloadCsvFile'; import { downloadCsvFile } from '../../helper/downloadCsvFile';
import { formatTime } from '@chatwoot/utils';
const state = { const state = {
fetchingStatus: false, fetchingStatus: false,
@ -21,6 +20,7 @@ const state = {
incoming_messages_count: 0, incoming_messages_count: 0,
outgoing_messages_count: 0, outgoing_messages_count: 0,
resolutions_count: 0, resolutions_count: 0,
previous: {},
}, },
}; };
@ -125,18 +125,6 @@ const mutations = {
}, },
[types.default.SET_ACCOUNT_SUMMARY](_state, summaryData) { [types.default.SET_ACCOUNT_SUMMARY](_state, summaryData) {
_state.accountSummary = 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;
}, },
}; };

View file

@ -1,6 +1,29 @@
type: object type: object
properties: properties:
value: avg_first_response_time:
type: number
timestamp:
type: string 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

View file

@ -4692,11 +4692,46 @@
{ {
"type": "object", "type": "object",
"properties": { "properties": {
"value": { "avg_first_response_time": {
"type": "string"
},
"avg_resolution_time": {
"type": "string"
},
"conversations_count": {
"type": "number" "type": "number"
}, },
"timestamp": { "incoming_messages_count": {
"type": "string" "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"
}
}
} }
} }
} }