From 89d0b2cb6eb4f65410b2e6ef225ac74685864e7f Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 15 Mar 2024 11:34:14 +0530 Subject: [PATCH] feat: Add the bot performance reports UI (#9036) Co-authored-by: Pranav Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- app/javascript/dashboard/api/reports.js | 18 +++ .../dashboard/api/specs/reports.spec.js | 34 ++++++ .../layout/config/sidebarItems/reports.js | 10 ++ app/javascript/dashboard/featureFlags.js | 1 + .../dashboard/i18n/locale/en/report.json | 76 +++++++++++-- .../dashboard/i18n/locale/en/settings.json | 1 + .../dashboard/mixins/reportMixin.js | 10 +- .../mixins/specs/reportMixin.spec.js | 31 +++++ .../mixins/specs/reportMixinFixtures.js | 8 ++ .../dashboard/settings/reports/BotReports.vue | 106 ++++++++++++++++++ .../settings/reports/ReportContainer.vue | 2 +- .../reports/components/BotMetrics.vue | 68 +++++++++++ .../components/ChartElements/ChartStats.vue | 6 +- .../reports/components/CsatMetrics.vue | 3 + .../reports/components/ReportMetricCard.vue | 2 +- .../dashboard/settings/reports/constants.js | 2 + .../settings/reports/reports.routes.js | 18 +++ .../dashboard/store/modules/reports.js | 31 +++++ .../dashboard/store/mutation-types.js | 1 + 19 files changed, 414 insertions(+), 14 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js index 987b69701..52fa7f444 100644 --- a/app/javascript/dashboard/api/reports.js +++ b/app/javascript/dashboard/api/reports.js @@ -84,6 +84,24 @@ class ReportsAPI extends ApiClient { params: { since, until, business_hours: businessHours }, }); } + + getBotMetrics({ from, to } = {}) { + return axios.get(`${this.url}/bot_metrics`, { + params: { since: from, until: to }, + }); + } + + getBotSummary({ from, to, groupBy, businessHours } = {}) { + return axios.get(`${this.url}/bot_summary`, { + params: { + since: from, + until: to, + type: 'account', + group_by: groupBy, + business_hours: businessHours, + }, + }); + } } export default new ReportsAPI(); diff --git a/app/javascript/dashboard/api/specs/reports.spec.js b/app/javascript/dashboard/api/specs/reports.spec.js index 7822dad8f..05d4a152c 100644 --- a/app/javascript/dashboard/api/specs/reports.spec.js +++ b/app/javascript/dashboard/api/specs/reports.spec.js @@ -111,6 +111,40 @@ describe('#Reports API', () => { }); }); + it('#getBotMetrics', () => { + reportsAPI.getBotMetrics({ from: 1621103400, to: 1621621800 }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v2/reports/bot_metrics', + { + params: { + since: 1621103400, + until: 1621621800, + }, + } + ); + }); + + it('#getBotSummary', () => { + reportsAPI.getBotSummary({ + from: 1621103400, + to: 1621621800, + groupBy: 'date', + businessHours: true, + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v2/reports/bot_summary', + { + params: { + since: 1621103400, + until: 1621621800, + type: 'account', + group_by: 'date', + business_hours: true, + }, + } + ); + }); + it('#getConversationMetric', () => { reportsAPI.getConversationMetric('account'); expect(axiosMock.get).toHaveBeenCalledWith( diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js b/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js index 967ee44ed..551256c74 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js @@ -1,3 +1,4 @@ +import { FEATURE_FLAGS } from '../../../../featureFlags'; import { frontendURL } from '../../../../helper/URLHelper'; const reports = accountId => ({ @@ -6,6 +7,7 @@ const reports = accountId => ({ 'account_overview_reports', 'conversation_reports', 'csat_reports', + 'bot_reports', 'agent_reports', 'label_reports', 'inbox_reports', @@ -33,6 +35,14 @@ const reports = accountId => ({ toState: frontendURL(`accounts/${accountId}/reports/csat`), toStateName: 'csat_reports', }, + { + icon: 'bot', + label: 'REPORTS_BOT', + hasSubMenu: false, + featureFlag: FEATURE_FLAGS.RESPONSE_BOT, + toState: frontendURL(`accounts/${accountId}/reports/bot`), + toStateName: 'bot_reports', + }, { icon: 'people', label: 'REPORTS_AGENT', diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 2936d22ea..f2fd3e757 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -19,4 +19,5 @@ export const FEATURE_FLAGS = { INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply', INBOX_VIEW: 'inbox_view', SLA: 'sla', + RESPONSE_BOT: 'response_bot', }; diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index a6d476bc9..56ca21773 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -35,6 +35,14 @@ "NAME": "Resolution Count", "DESC": "( Total )" }, + "BOT_RESOLUTION_COUNT": { + "NAME": "Resolution Count", + "DESC": "( Total )" + }, + "BOT_HANDOFF_COUNT": { + "NAME": "Handoff Count", + "DESC": "( Total )" + }, "REPLY_TIME": { "NAME": "Customer waiting time", "TOOLTIP_TEXT": "Waiting time is %{metricValue} (based on %{conversationCount} replies)" @@ -86,20 +94,49 @@ "MONTH": "Month", "YEAR": "Year" }, - "GROUP_BY_DAY_OPTIONS": [{ "id": 1, "groupBy": "Day" }], + "GROUP_BY_DAY_OPTIONS": [ + { + "id": 1, + "groupBy": "Day" + } + ], "GROUP_BY_WEEK_OPTIONS": [ - { "id": 1, "groupBy": "Day" }, - { "id": 2, "groupBy": "Week" } + { + "id": 1, + "groupBy": "Day" + }, + { + "id": 2, + "groupBy": "Week" + } ], "GROUP_BY_MONTH_OPTIONS": [ - { "id": 1, "groupBy": "Day" }, - { "id": 2, "groupBy": "Week" }, - { "id": 3, "groupBy": "Month" } + { + "id": 1, + "groupBy": "Day" + }, + { + "id": 2, + "groupBy": "Week" + }, + { + "id": 3, + "groupBy": "Month" + } ], "GROUP_BY_YEAR_OPTIONS": [ - { "id": 2, "groupBy": "Week" }, - { "id": 3, "groupBy": "Month" }, - { "id": 4, "groupBy": "Year" } + { + "id": 2, + "groupBy": "Week" + }, + { + "id": 3, + "groupBy": "Month" + }, + { + "id": 4, + "groupBy": "Year" + } ], "BUSINESS_HOURS": "Business Hours" }, @@ -404,6 +441,27 @@ } } }, + "BOT_REPORTS": { + "HEADER": "Bot Reports", + "METRIC": { + "TOTAL_CONVERSATIONS": { + "LABEL": "No. of Conversations", + "TOOLTIP": "Total number of conversations handled by the bot" + }, + "TOTAL_RESPONSES": { + "LABEL": "Total Responses", + "TOOLTIP": "Total number of responses sent by the bot" + }, + "RESOLUTION_RATE": { + "LABEL": "Resolution Rate", + "TOOLTIP": "Total number of conversations resolved by the bot / Total number of conversations handled by the bot * 100" + }, + "HANDOFF_RATE": { + "LABEL": "Handoff Rate", + "TOOLTIP": "Total number of conversations handed off to agents / Total number of conversations handled by the bot * 100" + } + } + }, "OVERVIEW_REPORTS": { "HEADER": "Overview", "LIVE": "Live", diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 63725f949..9a4bde2c8 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -234,6 +234,7 @@ "CAMPAIGNS": "Campaigns", "ONGOING": "Ongoing", "ONE_OFF": "One off", + "REPORTS_BOT": "Bot", "REPORTS_AGENT": "Agents", "REPORTS_LABEL": "Labels", "REPORTS_INBOX": "Inbox", diff --git a/app/javascript/dashboard/mixins/reportMixin.js b/app/javascript/dashboard/mixins/reportMixin.js index 2b8a5f87d..d57af1ad2 100644 --- a/app/javascript/dashboard/mixins/reportMixin.js +++ b/app/javascript/dashboard/mixins/reportMixin.js @@ -2,11 +2,19 @@ import { mapGetters } from 'vuex'; import { formatTime } from '@chatwoot/utils'; export default { + props: { + accountSummaryKey: { + type: String, + default: 'getAccountSummary', + }, + }, computed: { ...mapGetters({ - accountSummary: 'getAccountSummary', accountReport: 'getAccountReports', }), + accountSummary() { + return this.$store.getters[this.accountSummaryKey]; + }, }, methods: { calculateTrend(key) { diff --git a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js index d981de4e9..c0bc1e15f 100644 --- a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js +++ b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js @@ -11,11 +11,42 @@ describe('reportMixin', () => { beforeEach(() => { getters = { getAccountSummary: () => reportFixtures.summary, + getBotSummary: () => reportFixtures.botSummary, getAccountReports: () => reportFixtures.report, }; store = new Vuex.Store({ getters }); }); + it('display the metric for account', async () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [reportMixin], + }; + const wrapper = shallowMount(Component, { store, localVue }); + await wrapper.setProps({ + accountSummaryKey: 'getAccountSummary', + }); + expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000'); + expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual( + '3 Min 18 Sec' + ); + }); + + it('display the metric for bot', async () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [reportMixin], + }; + const wrapper = shallowMount(Component, { store, localVue }); + await wrapper.setProps({ + accountSummaryKey: 'getBotSummary', + }); + expect(wrapper.vm.displayMetric('bot_resolutions_count')).toEqual('10'); + expect(wrapper.vm.displayMetric('bot_handoffs_count')).toEqual('20'); + }); + it('display the metric', () => { const Component = { render() {}, diff --git a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js index ab6b6fecf..591bf7c1f 100644 --- a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js +++ b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js @@ -15,6 +15,14 @@ export default { }, resolutions_count: 3, }, + botSummary: { + bot_resolutions_count: 10, + bot_handoffs_count: 20, + previous: { + bot_resolutions_count: 8, + bot_handoffs_count: 5, + }, + }, report: { data: [ { value: '0.00', timestamp: 1647541800, count: 0 }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue new file mode 100644 index 000000000..a75051aaf --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue @@ -0,0 +1,106 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue index c601b4277..273f814b7 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue @@ -7,7 +7,7 @@ :key="metric.KEY" class="p-4 rounded-md mb-3" > - +
+import { ref, watch, onMounted } from 'vue'; +import ReportMetricCard from './ReportMetricCard.vue'; +import ReportsAPI from 'dashboard/api/reports'; + +const props = defineProps({ + filters: { + type: Object, + required: true, + }, +}); + +const conversationCount = ref('0'); +const messageCount = ref('0'); +const resolutionRate = ref('0'); +const handoffRate = ref('0'); + +const formatToPercent = value => { + return value ? `${value}%` : '--'; +}; + +const fetchMetrics = () => { + if (!props.filters.to || !props.filters.from) { + return; + } + ReportsAPI.getBotMetrics(props.filters).then(response => { + conversationCount.value = response.data.conversation_count.toLocaleString(); + messageCount.value = response.data.message_count.toLocaleString(); + resolutionRate.value = response.data.resolution_rate.toString(); + handoffRate.value = response.data.handoff_rate.toString(); + }); +}; + +watch(() => props.filters, fetchMetrics, { deep: true }); + +onMounted(fetchMetrics); + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue index 94fe1359d..4faf4de2b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue @@ -1,6 +1,8 @@