diff --git a/app/javascript/dashboard/api/slaReports.js b/app/javascript/dashboard/api/slaReports.js new file mode 100644 index 000000000..c187c58a3 --- /dev/null +++ b/app/javascript/dashboard/api/slaReports.js @@ -0,0 +1,72 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class SLAReportsAPI extends ApiClient { + constructor() { + super('applied_slas', { accountScoped: true }); + } + + get({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + page, + } = {}) { + return axios.get(this.url, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + page, + }, + }); + } + + download({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + } = {}) { + return axios.get(`${this.url}/download`, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + }, + }); + } + + getMetrics({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + } = {}) { + return axios.get(`${this.url}/metrics`, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + }, + }); + } +} + +export default new SLAReportsAPI(); diff --git a/app/javascript/dashboard/api/specs/slaReports.spec.js b/app/javascript/dashboard/api/specs/slaReports.spec.js new file mode 100644 index 000000000..51ac8bfe4 --- /dev/null +++ b/app/javascript/dashboard/api/specs/slaReports.spec.js @@ -0,0 +1,98 @@ +import SLAReportsAPI from '../slaReports'; +import ApiClient from '../ApiClient'; + +describe('#SLAReports API', () => { + it('creates correct instance', () => { + expect(SLAReportsAPI).toBeInstanceOf(ApiClient); + expect(SLAReportsAPI.apiVersion).toBe('/api/v1'); + expect(SLAReportsAPI).toHaveProperty('get'); + expect(SLAReportsAPI).toHaveProperty('getMetrics'); + }); + + describe('API calls', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + get: jest.fn(() => Promise.resolve()), + patch: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('#get', () => { + SLAReportsAPI.get({ + page: 1, + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }); + expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/applied_slas', { + params: { + page: 1, + since: 1622485800, + until: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }, + }); + }); + it('#getMetrics', () => { + SLAReportsAPI.getMetrics({ + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/applied_slas/metrics', + { + params: { + since: 1622485800, + until: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }, + } + ); + }); + it('#download', () => { + SLAReportsAPI.download({ + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/applied_slas/download', + { + params: { + since: 1622485800, + until: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + }, + } + ); + }); + }); +}); diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 91b84ddd1..2f8a9c1e5 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -44,6 +44,7 @@ import teams from './modules/teams'; import userNotificationSettings from './modules/userNotificationSettings'; import webhooks from './modules/webhooks'; import draftMessages from './modules/draftMessages'; +import SLAReports from './modules/SLAReports'; import LogRocket from 'logrocket'; import createPlugin from 'logrocket-vuex'; @@ -111,6 +112,7 @@ export default new Vuex.Store({ webhooks, draftMessages, sla, + slaReports: SLAReports, }, plugins, }); diff --git a/app/javascript/dashboard/store/modules/SLAReports.js b/app/javascript/dashboard/store/modules/SLAReports.js new file mode 100644 index 000000000..44c11fd0c --- /dev/null +++ b/app/javascript/dashboard/store/modules/SLAReports.js @@ -0,0 +1,99 @@ +import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; +import types from '../mutation-types'; +import SLAReportsAPI from '../../api/slaReports'; + +export const state = { + records: [], + metrics: { + numberOfSLABreaches: 0, + hitRate: '0%', + }, + uiFlags: { + isFetching: false, + isFetchingMetrics: false, + }, + meta: { + count: 0, + currentPage: 1, + }, +}; + +export const getters = { + getAll(_state) { + return _state.records; + }, + getMeta(_state) { + return _state.meta; + }, + getMetrics(_state) { + return _state.metrics; + }, + getUIFlags(_state) { + return _state.uiFlags; + }, +}; + +export const actions = { + get: async function getResponses({ commit }, params) { + commit(types.SET_SLA_REPORTS_UI_FLAG, { isFetching: true }); + try { + const response = await SLAReportsAPI.get(params); + const { payload, meta } = response.data; + + commit(types.SET_SLA_REPORTS, payload); + commit(types.SET_SLA_REPORTS_META, meta); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_SLA_REPORTS_UI_FLAG, { isFetching: false }); + } + }, + getMetrics: async function getMetrics({ commit }, params) { + commit(types.SET_SLA_REPORTS_UI_FLAG, { isFetchingMetrics: true }); + try { + const response = await SLAReportsAPI.getMetrics(params); + commit(types.SET_SLA_REPORTS_METRICS, response.data); + } catch (error) { + // Ignore error + } finally { + commit(types.SET_SLA_REPORTS_UI_FLAG, { isFetchingMetrics: false }); + } + }, +}; + +export const mutations = { + [types.SET_SLA_REPORTS_UI_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, + + [types.SET_SLA_REPORTS]: MutationHelpers.set, + [types.SET_SLA_REPORTS_METRICS]( + _state, + { number_of_sla_breaches: numberOfSLABreaches, hit_rate: hitRate } + ) { + _state.metrics = { + numberOfSLABreaches, + hitRate, + }; + }, + [types.SET_SLA_REPORTS_META]( + _state, + { total_applied_slas: totalAppliedSLAs, current_page: currentPage } + ) { + _state.meta = { + count: totalAppliedSLAs, + currentPage, + }; + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/specs/slaReports/actions.spec.js b/app/javascript/dashboard/store/modules/specs/slaReports/actions.spec.js new file mode 100644 index 000000000..fb350c937 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/slaReports/actions.spec.js @@ -0,0 +1,55 @@ +import axios from 'axios'; +import { actions } from '../../SLAReports'; +import appliedSlas from './fixtures'; +import types from '../../../mutation-types'; + +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#get', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ + data: { payload: appliedSlas, meta: { count: 1 } }, + }); + await actions.get({ commit }, {}); + expect(commit.mock.calls).toEqual([ + [types.SET_SLA_REPORTS_UI_FLAG, { isFetching: true }], + [types.SET_SLA_REPORTS, appliedSlas], + [types.SET_SLA_REPORTS_META, { count: 1 }], + [types.SET_SLA_REPORTS_UI_FLAG, { isFetching: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.get({ commit }, { teamId: 1 })).rejects.toThrow( + Error + ); + expect(commit.mock.calls).toEqual([ + [types.SET_SLA_REPORTS_UI_FLAG, { isFetching: true }], + [types.SET_SLA_REPORTS_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#getMetrics', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ data: { metrics: { count: 1 } } }); + await actions.getMetrics({ commit }, {}); + expect(commit.mock.calls).toEqual([ + [types.SET_SLA_REPORTS_UI_FLAG, { isFetchingMetrics: true }], + [types.SET_SLA_REPORTS_METRICS, { metrics: { count: 1 } }], + [types.SET_SLA_REPORTS_UI_FLAG, { isFetchingMetrics: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.getMetrics({ commit }, { teamId: 1 }); + expect(commit.mock.calls).toEqual([ + [types.SET_SLA_REPORTS_UI_FLAG, { isFetchingMetrics: true }], + [types.SET_SLA_REPORTS_UI_FLAG, { isFetchingMetrics: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/slaReports/fixtures.js b/app/javascript/dashboard/store/modules/specs/slaReports/fixtures.js new file mode 100644 index 000000000..799ad74a7 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/slaReports/fixtures.js @@ -0,0 +1,52 @@ +export default [ + { + id: 23, + sla_policy_id: 7, + conversation_id: 152, + sla_status: 'active_with_misses', + created_at: '2024-03-31T07:50:53.518Z', + updated_at: '2024-03-31T07:55:06.451Z', + conversation: { + id: 152, + uuid: '2f9a988d-418f-47d9-b4dc-c441f28da7c2', + account_id: 1, + }, + sla_events: [ + { + id: 14, + event_type: 'frt', + meta: {}, + updated_at: 1711871706, + created_at: 1711871706, + }, + { + id: 15, + event_type: 'rt', + meta: {}, + updated_at: 1711871706, + created_at: 1711871706, + }, + ], + }, + { + id: 24, + sla_policy_id: 7, + conversation_id: 153, + sla_status: 'active_with_misses', + created_at: '2024-03-31T07:57:49.659Z', + updated_at: '2024-03-31T08:00:31.627Z', + conversation: { + id: 153, + uuid: 'd5d97961-4341-469e-accf-f13f25a14c3c', + }, + sla_events: [ + { + id: 16, + event_type: 'rt', + meta: {}, + updated_at: 1711872031, + created_at: 1711872031, + }, + ], + }, +]; diff --git a/app/javascript/dashboard/store/modules/specs/slaReports/getters.spec.js b/app/javascript/dashboard/store/modules/specs/slaReports/getters.spec.js new file mode 100644 index 000000000..6eddb3f60 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/slaReports/getters.spec.js @@ -0,0 +1,24 @@ +import { getters } from '../../SLAReports'; +import appliedSlas from './fixtures'; + +describe('#getters', () => { + it('getAppliedSlas', () => { + const state = { + records: [appliedSlas[0]], + }; + expect(getters.getAll(state)).toEqual([appliedSlas[0]]); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { + isFetching: false, + isFetchingMetrics: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isFetching: false, + isFetchingMetrics: false, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/slaReports/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/slaReports/mutations.spec.js new file mode 100644 index 000000000..04e171889 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/slaReports/mutations.spec.js @@ -0,0 +1,49 @@ +import { mutations } from '../../SLAReports'; +import appliedSlas from './fixtures'; +import types from '../../../mutation-types'; + +describe('#mutations', () => { + describe('#SET_SLA_REPORTS', () => { + it('Adds sla reports', () => { + const state = { records: {} }; + mutations[types.SET_SLA_REPORTS](state, appliedSlas); + expect(state.records).toEqual(appliedSlas); + }); + }); + + describe('#SET_SLA_REPORTS_UI_FLAG', () => { + it('set ui flags', () => { + const state = { uiFlags: {} }; + mutations[types.SET_SLA_REPORTS_UI_FLAG](state, { isFetching: true }); + expect(state.uiFlags).toEqual({ isFetching: true }); + }); + }); + + describe('#SET_SLA_REPORTS_METRICS', () => { + it('set metrics', () => { + const state = { metrics: {} }; + mutations[types.SET_SLA_REPORTS_METRICS](state, { + number_of_sla_breaches: 1, + hit_rate: '100%', + }); + expect(state.metrics).toEqual({ + numberOfSLABreaches: 1, + hitRate: '100%', + }); + }); + }); + + describe('#SET_SLA_REPORTS_META', () => { + it('set meta', () => { + const state = { meta: {} }; + mutations[types.SET_SLA_REPORTS_META](state, { + total_applied_slas: 1, + current_page: 1, + }); + expect(state.meta).toEqual({ + count: 1, + currentPage: 1, + }); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 6f7d36cf7..c21fb44e2 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -309,4 +309,10 @@ export default { ADD_SLA: 'ADD_SLA', EDIT_SLA: 'EDIT_SLA', DELETE_SLA: 'DELETE_SLA', + + // SLA Reports + SET_SLA_REPORTS_UI_FLAG: 'SET_SLA_REPORTS_UI_FLAG', + SET_SLA_REPORTS: 'SET_SLA_REPORTS', + SET_SLA_REPORTS_METRICS: 'SET_SLA_REPORTS_METRICS', + SET_SLA_REPORTS_META: 'SET_SLA_REPORTS_META', };