diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index a83611676..efa09a43c 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -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 diff --git a/app/javascript/dashboard/assets/scss/widgets/_report.scss b/app/javascript/dashboard/assets/scss/widgets/_report.scss index c62eba70e..6e8b2fe5c 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_report.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_report.scss @@ -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; diff --git a/app/javascript/dashboard/components/widgets/ReportStatsCard.vue b/app/javascript/dashboard/components/widgets/ReportStatsCard.vue index 309627220..9bc46d021 100644 --- a/app/javascript/dashboard/components/widgets/ReportStatsCard.vue +++ b/app/javascript/dashboard/components/widgets/ReportStatsCard.vue @@ -7,9 +7,12 @@
{{ desc }}
@@ -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}%`; + }, + }, }; diff --git a/app/javascript/dashboard/mixins/reportMixin.js b/app/javascript/dashboard/mixins/reportMixin.js new file mode 100644 index 000000000..8ef7fdfe5 --- /dev/null +++ b/app/javascript/dashboard/mixins/reportMixin.js @@ -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]; + }; + }, + }, +}; diff --git a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js new file mode 100644 index 000000000..003295f95 --- /dev/null +++ b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js @@ -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); + }); +}); diff --git a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js new file mode 100644 index 000000000..5c8315ab1 --- /dev/null +++ b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js @@ -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, + }, +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue index 9d90bb9e2..fe0240d74 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue @@ -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" /> @@ -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, 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 449b0383f..8ce264080 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue @@ -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" /> @@ -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, diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index f2967945a..cb1efe3ff 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -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; }, }; diff --git a/swagger/definitions/resource/report.yml b/swagger/definitions/resource/report.yml index 9a4bffe9c..8dcbf3f0e 100644 --- a/swagger/definitions/resource/report.yml +++ b/swagger/definitions/resource/report.yml @@ -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 \ No newline at end of file diff --git a/swagger/swagger.json b/swagger/swagger.json index 39cc9f95a..8e574fedc 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -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" + } + } } } }