diff --git a/app/javascript/dashboard/composables/spec/fixtures/reportFixtures.js b/app/javascript/dashboard/composables/spec/fixtures/reportFixtures.js new file mode 100644 index 000000000..93880e1ca --- /dev/null +++ b/app/javascript/dashboard/composables/spec/fixtures/reportFixtures.js @@ -0,0 +1,37 @@ +export const summary = { + avg_first_response_time: '198.6666666666667', + avg_resolution_time: '208.3333333333333', + conversations_count: 5000, + 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, +}; + +export const botSummary = { + bot_resolutions_count: 10, + bot_handoffs_count: 20, + previous: { + bot_resolutions_count: 8, + bot_handoffs_count: 5, + }, +}; + +export const report = { + data: [ + { value: '0.00', timestamp: 1647541800, count: 0 }, + { value: '0.00', timestamp: 1647628200, count: 0 }, + { value: '0.00', timestamp: 1647714600, count: 0 }, + { value: '0.00', timestamp: 1647801000, count: 0 }, + { value: '0.01', timestamp: 1647887400, count: 4 }, + { value: '0.00', timestamp: 1647973800, count: 0 }, + { value: '0.00', timestamp: 1648060200, count: 0 }, + ], +}; diff --git a/app/javascript/dashboard/composables/spec/useReportMetrics.spec.js b/app/javascript/dashboard/composables/spec/useReportMetrics.spec.js new file mode 100644 index 000000000..194f07e75 --- /dev/null +++ b/app/javascript/dashboard/composables/spec/useReportMetrics.spec.js @@ -0,0 +1,61 @@ +import { ref } from 'vue'; +import { useReportMetrics } from '../useReportMetrics'; +import { useMapGetter } from 'dashboard/composables/store'; +import { summary, botSummary } from './fixtures/reportFixtures'; + +vi.mock('dashboard/composables/store'); +vi.mock('@chatwoot/utils', () => ({ + formatTime: vi.fn(time => `formatted_${time}`), +})); + +describe('useReportMetrics', () => { + beforeEach(() => { + vi.clearAllMocks(); + useMapGetter.mockReturnValue(ref(summary)); + }); + + it('calculates trend correctly', () => { + const { calculateTrend } = useReportMetrics(); + + expect(calculateTrend('conversations_count')).toBe(124900); + expect(calculateTrend('incoming_messages_count')).toBe(0); + expect(calculateTrend('avg_first_response_time')).toBe(123); + }); + + it('returns 0 for trend when previous value is not available', () => { + const { calculateTrend } = useReportMetrics(); + + expect(calculateTrend('non_existent_key')).toBe(0); + }); + + it('identifies average metric types correctly', () => { + const { isAverageMetricType } = useReportMetrics(); + + expect(isAverageMetricType('avg_first_response_time')).toBe(true); + expect(isAverageMetricType('avg_resolution_time')).toBe(true); + expect(isAverageMetricType('reply_time')).toBe(true); + expect(isAverageMetricType('conversations_count')).toBe(false); + }); + + it('displays metrics correctly for account', () => { + const { displayMetric } = useReportMetrics(); + + expect(displayMetric('conversations_count')).toBe('5,000'); + expect(displayMetric('incoming_messages_count')).toBe('5'); + }); + + it('displays the metric for bot', () => { + const customKey = 'getBotSummary'; + useMapGetter.mockReturnValue(ref(botSummary)); + const { displayMetric } = useReportMetrics(customKey); + + expect(displayMetric('bot_resolutions_count')).toBe('10'); + expect(displayMetric('bot_handoffs_count')).toBe('20'); + }); + + it('handles non-existent metrics', () => { + const { displayMetric } = useReportMetrics(); + + expect(displayMetric('non_existent_key')).toBe('0'); + }); +}); diff --git a/app/javascript/dashboard/composables/useReportMetrics.js b/app/javascript/dashboard/composables/useReportMetrics.js new file mode 100644 index 000000000..37168e256 --- /dev/null +++ b/app/javascript/dashboard/composables/useReportMetrics.js @@ -0,0 +1,57 @@ +import { useMapGetter } from 'dashboard/composables/store'; +import { formatTime } from '@chatwoot/utils'; + +/** + * A composable function for report metrics calculations and display. + * + * @param {string} [accountSummaryKey='getAccountSummary'] - The key for accessing account summary data. + * @returns {Object} An object containing utility functions for report metrics. + */ +export function useReportMetrics(accountSummaryKey = 'getAccountSummary') { + const accountSummary = useMapGetter(accountSummaryKey); + + /** + * Calculates the trend percentage for a given metric. + * + * @param {string} key - The key of the metric to calculate trend for. + * @returns {number} The calculated trend percentage, rounded to the nearest integer. + */ + const calculateTrend = key => { + if (!accountSummary.value.previous[key]) return 0; + const diff = accountSummary.value[key] - accountSummary.value.previous[key]; + return Math.round((diff / accountSummary.value.previous[key]) * 100); + }; + + /** + * Checks if a given metric key represents an average metric type. + * + * @param {string} key - The key of the metric to check. + * @returns {boolean} True if the metric is an average type, false otherwise. + */ + const isAverageMetricType = key => { + return [ + 'avg_first_response_time', + 'avg_resolution_time', + 'reply_time', + ].includes(key); + }; + + /** + * Formats and displays a metric value based on its type. + * + * @param {string} key - The key of the metric to display. + * @returns {string} The formatted metric value as a string. + */ + const displayMetric = key => { + if (isAverageMetricType(key)) { + return formatTime(accountSummary.value[key]); + } + return Number(accountSummary.value[key] || '').toLocaleString(); + }; + + return { + calculateTrend, + isAverageMetricType, + displayMetric, + }; +} diff --git a/app/javascript/dashboard/mixins/reportMixin.js b/app/javascript/dashboard/mixins/reportMixin.js deleted file mode 100644 index d57af1ad2..000000000 --- a/app/javascript/dashboard/mixins/reportMixin.js +++ /dev/null @@ -1,51 +0,0 @@ -import { mapGetters } from 'vuex'; -import { formatTime } from '@chatwoot/utils'; - -export default { - props: { - accountSummaryKey: { - type: String, - default: 'getAccountSummary', - }, - }, - computed: { - ...mapGetters({ - accountReport: 'getAccountReports', - }), - accountSummary() { - return this.$store.getters[this.accountSummaryKey]; - }, - }, - methods: { - calculateTrend(key) { - if (!this.accountSummary.previous[key]) return 0; - const diff = this.accountSummary[key] - this.accountSummary.previous[key]; - return Math.round((diff / this.accountSummary.previous[key]) * 100); - }, - displayMetric(key) { - if (this.isAverageMetricType(key)) { - return formatTime(this.accountSummary[key]); - } - return Number(this.accountSummary[key] || '').toLocaleString(); - }, - displayInfoText(key) { - if (this.metrics[this.currentSelection].KEY !== key) { - return ''; - } - if (this.isAverageMetricType(key)) { - const total = this.accountReport.data - .map(item => item.count) - .reduce((prev, curr) => prev + curr, 0); - return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`; - } - return ''; - }, - isAverageMetricType(key) { - return [ - 'avg_first_response_time', - 'avg_resolution_time', - 'reply_time', - ].includes(key); - }, - }, -}; diff --git a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js deleted file mode 100644 index c0bc1e15f..000000000 --- a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js +++ /dev/null @@ -1,136 +0,0 @@ -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, - 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() {}, - title: 'TestComponent', - mixins: [reportMixin], - }; - const wrapper = shallowMount(Component, { store, localVue }); - expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000'); - expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual( - '3 Min 18 Sec' - ); - }); - - it('calculate the trend', () => { - const Component = { - render() {}, - title: 'TestComponent', - mixins: [reportMixin], - }; - const wrapper = shallowMount(Component, { store, localVue }); - expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(124900); - expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0); - }); - - it('display info text', () => { - const Component = { - render() {}, - title: 'TestComponent', - mixins: [reportMixin], - data() { - return { - currentSelection: 0, - }; - }, - computed: { - metrics() { - return [ - { - DESC: '( Avg )', - INFO_TEXT: 'Total number of conversations used for computation:', - KEY: 'avg_first_response_time', - NAME: 'First Response Time', - }, - ]; - }, - }, - }; - const wrapper = shallowMount(Component, { store, localVue }); - expect(wrapper.vm.displayInfoText('avg_first_response_time')).toEqual( - 'Total number of conversations used for computation: 4' - ); - }); - - it('do not display info text', () => { - const Component = { - render() {}, - title: 'TestComponent', - mixins: [reportMixin], - data() { - return { - currentSelection: 0, - }; - }, - computed: { - metrics() { - return [ - { - DESC: '( Total )', - INFO_TEXT: '', - KEY: 'conversation_count', - NAME: 'Conversations', - }, - { - DESC: '( Avg )', - INFO_TEXT: 'Total number of conversations used for computation:', - KEY: 'avg_first_response_time', - NAME: 'First Response Time', - }, - ]; - }, - }, - }; - const wrapper = shallowMount(Component, { store, localVue }); - expect(wrapper.vm.displayInfoText('conversation_count')).toEqual(''); - expect(wrapper.vm.displayInfoText('incoming_messages_count')).toEqual(''); - }); -}); diff --git a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js deleted file mode 100644 index 591bf7c1f..000000000 --- a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js +++ /dev/null @@ -1,37 +0,0 @@ -export default { - summary: { - avg_first_response_time: '198.6666666666667', - avg_resolution_time: '208.3333333333333', - conversations_count: 5000, - 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, - }, - 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 }, - { value: '0.00', timestamp: 1647628200, count: 0 }, - { value: '0.00', timestamp: 1647714600, count: 0 }, - { value: '0.00', timestamp: 1647801000, count: 0 }, - { value: '0.01', timestamp: 1647887400, count: 4 }, - { value: '0.00', timestamp: 1647973800, count: 0 }, - { value: '0.00', timestamp: 1648060200, count: 0 }, - ], - }, -}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue index 492775542..78b433a42 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue @@ -3,7 +3,6 @@ import { useAlert } from 'dashboard/composables'; import BotMetrics from './components/BotMetrics.vue'; import ReportFilterSelector from './components/FilterSelector.vue'; import { GROUP_BY_FILTER } from './constants'; -import reportMixin from 'dashboard/mixins/reportMixin'; import ReportContainer from './ReportContainer.vue'; import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; @@ -14,7 +13,6 @@ export default { ReportFilterSelector, ReportContainer, }, - mixins: [reportMixin], data() { return { from: 0, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue index 1f96b161f..da2f2f232 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue @@ -4,7 +4,6 @@ import fromUnixTime from 'date-fns/fromUnixTime'; import format from 'date-fns/format'; import ReportFilterSelector from './components/FilterSelector.vue'; import { GROUP_BY_FILTER } from './constants'; -import reportMixin from 'dashboard/mixins/reportMixin'; import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; import ReportContainer from './ReportContainer.vue'; @@ -24,7 +23,6 @@ export default { ReportFilterSelector, ReportContainer, }, - mixins: [reportMixin], data() { return { from: 0, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue index f90c3730c..fc323d037 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue @@ -1,19 +1,23 @@ @@ -29,7 +34,7 @@ export default { {{ metric.NAME }}