mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	feat: Display trends in report metrics (#4144)
This commit is contained in:
		| @@ -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> | ||||
|     <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": { | ||||
|               "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" | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Aswin Dev P.S
					Aswin Dev P.S