diff --git a/app/javascript/dashboard/api/agentCapacityPolicies.js b/app/javascript/dashboard/api/agentCapacityPolicies.js new file mode 100644 index 000000000..7792ce469 --- /dev/null +++ b/app/javascript/dashboard/api/agentCapacityPolicies.js @@ -0,0 +1,43 @@ +/* global axios */ + +import ApiClient from './ApiClient'; + +class AgentCapacityPolicies extends ApiClient { + constructor() { + super('agent_capacity_policies', { accountScoped: true }); + } + + getUsers(policyId) { + return axios.get(`${this.url}/${policyId}/users`); + } + + addUser(policyId, userData) { + return axios.post(`${this.url}/${policyId}/users`, { + user_id: userData.id, + capacity: userData.capacity, + }); + } + + removeUser(policyId, userId) { + return axios.delete(`${this.url}/${policyId}/users/${userId}`); + } + + createInboxLimit(policyId, limitData) { + return axios.post(`${this.url}/${policyId}/inbox_limits`, { + inbox_id: limitData.inboxId, + conversation_limit: limitData.conversationLimit, + }); + } + + updateInboxLimit(policyId, limitId, limitData) { + return axios.put(`${this.url}/${policyId}/inbox_limits/${limitId}`, { + conversation_limit: limitData.conversationLimit, + }); + } + + deleteInboxLimit(policyId, limitId) { + return axios.delete(`${this.url}/${policyId}/inbox_limits/${limitId}`); + } +} + +export default new AgentCapacityPolicies(); diff --git a/app/javascript/dashboard/api/specs/agentCapacityPolicies.spec.js b/app/javascript/dashboard/api/specs/agentCapacityPolicies.spec.js new file mode 100644 index 000000000..43932aa71 --- /dev/null +++ b/app/javascript/dashboard/api/specs/agentCapacityPolicies.spec.js @@ -0,0 +1,98 @@ +import agentCapacityPolicies from '../agentCapacityPolicies'; +import ApiClient from '../ApiClient'; + +describe('#AgentCapacityPoliciesAPI', () => { + it('creates correct instance', () => { + expect(agentCapacityPolicies).toBeInstanceOf(ApiClient); + expect(agentCapacityPolicies).toHaveProperty('get'); + expect(agentCapacityPolicies).toHaveProperty('show'); + expect(agentCapacityPolicies).toHaveProperty('create'); + expect(agentCapacityPolicies).toHaveProperty('update'); + expect(agentCapacityPolicies).toHaveProperty('delete'); + expect(agentCapacityPolicies).toHaveProperty('getUsers'); + expect(agentCapacityPolicies).toHaveProperty('addUser'); + expect(agentCapacityPolicies).toHaveProperty('removeUser'); + expect(agentCapacityPolicies).toHaveProperty('createInboxLimit'); + expect(agentCapacityPolicies).toHaveProperty('updateInboxLimit'); + expect(agentCapacityPolicies).toHaveProperty('deleteInboxLimit'); + }); + + describe('API calls', () => { + const originalAxios = window.axios; + const axiosMock = { + get: vi.fn(() => Promise.resolve()), + post: vi.fn(() => Promise.resolve()), + put: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + // Mock accountIdFromRoute + Object.defineProperty(agentCapacityPolicies, 'accountIdFromRoute', { + get: () => '1', + configurable: true, + }); + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('#getUsers', () => { + agentCapacityPolicies.getUsers(123); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/accounts/1/agent_capacity_policies/123/users' + ); + }); + + it('#addUser', () => { + const userData = { id: 456, capacity: 20 }; + agentCapacityPolicies.addUser(123, userData); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/accounts/1/agent_capacity_policies/123/users', + { + user_id: 456, + capacity: 20, + } + ); + }); + + it('#removeUser', () => { + agentCapacityPolicies.removeUser(123, 456); + expect(axiosMock.delete).toHaveBeenCalledWith( + '/api/v1/accounts/1/agent_capacity_policies/123/users/456' + ); + }); + + it('#createInboxLimit', () => { + const limitData = { inboxId: 1, conversationLimit: 10 }; + agentCapacityPolicies.createInboxLimit(123, limitData); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/accounts/1/agent_capacity_policies/123/inbox_limits', + { + inbox_id: 1, + conversation_limit: 10, + } + ); + }); + + it('#updateInboxLimit', () => { + const limitData = { conversationLimit: 15 }; + agentCapacityPolicies.updateInboxLimit(123, 789, limitData); + expect(axiosMock.put).toHaveBeenCalledWith( + '/api/v1/accounts/1/agent_capacity_policies/123/inbox_limits/789', + { + conversation_limit: 15, + } + ); + }); + + it('#deleteInboxLimit', () => { + agentCapacityPolicies.deleteInboxLimit(123, 789); + expect(axiosMock.delete).toHaveBeenCalledWith( + '/api/v1/accounts/1/agent_capacity_policies/123/inbox_limits/789' + ); + }); + }); +}); diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/AgentCapacityPolicyCard/AgentCapacityPolicyCard.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/AgentCapacityPolicyCard/AgentCapacityPolicyCard.story.vue new file mode 100644 index 000000000..10f301230 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/AgentCapacityPolicyCard/AgentCapacityPolicyCard.story.vue @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/AgentCapacityPolicyCard/AgentCapacityPolicyCard.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/AgentCapacityPolicyCard/AgentCapacityPolicyCard.vue new file mode 100644 index 000000000..3c749e751 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/AgentCapacityPolicyCard/AgentCapacityPolicyCard.vue @@ -0,0 +1,86 @@ + + + + + + + + + {{ name }} + + + + + + + + + + + {{ description }} + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/CardPopover.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/CardPopover.vue index 013e6d5fe..50d7794c9 100644 --- a/app/javascript/dashboard/components-next/AssignmentPolicy/components/CardPopover.vue +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/CardPopover.vue @@ -1,6 +1,8 @@ @@ -41,5 +74,16 @@ const mockItems = [ /> + + + console.log('Fetch triggered')" + /> + + diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index fd7247396..dd2fdc904 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -563,13 +563,32 @@ } }, "DELETE_POLICY": { - "TITLE": "Delete policy", - "DESCRIPTION": "Are you sure you want to delete this policy? This action cannot be undone.", - "CONFIRM_BUTTON_LABEL": "Delete", - "CANCEL_BUTTON_LABEL": "Cancel", "SUCCESS_MESSAGE": "Assignment policy deleted successfully", "ERROR_MESSAGE": "Failed to delete assignment policy" } + }, + "AGENT_CAPACITY_POLICY": { + "INDEX": { + "HEADER": { + "TITLE": "Agent capacity", + "CREATE_POLICY": "New policy" + }, + "CARD": { + "POPOVER": "Added agents", + "EDIT": "Edit" + }, + "NO_RECORDS_FOUND": "No agent capacity policies found" + }, + "DELETE_POLICY": { + "SUCCESS_MESSAGE": "Agent capacity policy deleted successfully", + "ERROR_MESSAGE": "Failed to delete agent capacity policy" + } + }, + "DELETE_POLICY": { + "TITLE": "Delete policy", + "DESCRIPTION": "Are you sure you want to delete this policy? This action cannot be undone.", + "CONFIRM_BUTTON_LABEL": "Delete", + "CANCEL_BUTTON_LABEL": "Cancel" } } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue index b41d9990a..ab9fcf17e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue @@ -30,7 +30,7 @@ const agentAssignments = computed(() => [ ], }, { - key: 'agent_capacity_policy', + key: 'agent_capacity_policy_index', title: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.TITLE'), description: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.DESCRIPTION'), features: [ diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js index 37934f505..f09cc1485 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js @@ -5,6 +5,7 @@ import AssignmentPolicyIndex from './Index.vue'; import AgentAssignmentIndex from './pages/AgentAssignmentIndexPage.vue'; import AgentAssignmentCreate from './pages/AgentAssignmentCreatePage.vue'; import AgentAssignmentEdit from './pages/AgentAssignmentEditPage.vue'; +import AgentCapacityIndex from './pages/AgentCapacityIndexPage.vue'; export default { routes: [ @@ -54,6 +55,15 @@ export default { permissions: ['administrator'], }, }, + { + path: 'capacity', + name: 'agent_capacity_policy_index', + component: AgentCapacityIndex, + meta: { + featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2, + permissions: ['administrator'], + }, + }, ], }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityIndexPage.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityIndexPage.vue new file mode 100644 index 000000000..fb94e5fb4 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityIndexPage.vue @@ -0,0 +1,126 @@ + + + + + + + + + {{ + $t( + 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.INDEX.HEADER.CREATE_POLICY' + ) + }} + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmDeletePolicyDialog.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmDeletePolicyDialog.vue index 8cf13bcd5..2dd57f713 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmDeletePolicyDialog.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmDeletePolicyDialog.vue @@ -31,19 +31,13 @@ defineExpose({ openDialog, closeDialog }); diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 291031ffd..16bcab3f9 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -2,6 +2,7 @@ import { createStore } from 'vuex'; import accounts from './modules/accounts'; import agentBots from './modules/agentBots'; +import agentCapacityPolicies from './modules/agentCapacityPolicies'; import agents from './modules/agents'; import assignmentPolicies from './modules/assignmentPolicies'; import articles from './modules/helpCenterArticles'; @@ -63,6 +64,7 @@ export default createStore({ modules: { accounts, agentBots, + agentCapacityPolicies, agents, assignmentPolicies, articles, diff --git a/app/javascript/dashboard/store/modules/agentCapacityPolicies.js b/app/javascript/dashboard/store/modules/agentCapacityPolicies.js new file mode 100644 index 000000000..e1e766f2c --- /dev/null +++ b/app/javascript/dashboard/store/modules/agentCapacityPolicies.js @@ -0,0 +1,168 @@ +import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; +import types from '../mutation-types'; +import AgentCapacityPoliciesAPI from '../../api/agentCapacityPolicies'; +import { throwErrorMessage } from '../utils/api'; +import camelcaseKeys from 'camelcase-keys'; +import snakecaseKeys from 'snakecase-keys'; + +export const state = { + records: [], + uiFlags: { + isFetching: false, + isFetchingItem: false, + isCreating: false, + isUpdating: false, + isDeleting: false, + }, + usersUiFlags: { + isFetching: false, + isDeleting: false, + }, +}; + +export const getters = { + getAgentCapacityPolicies(_state) { + return _state.records; + }, + getUIFlags(_state) { + return _state.uiFlags; + }, + getUsersUIFlags(_state) { + return _state.usersUiFlags; + }, + getAgentCapacityPolicyById: _state => id => { + return _state.records.find(record => record.id === Number(id)) || {}; + }, +}; + +export const actions = { + get: async function get({ commit }) { + commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetching: true }); + try { + const response = await AgentCapacityPoliciesAPI.get(); + commit(types.SET_AGENT_CAPACITY_POLICIES, camelcaseKeys(response.data)); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetching: false }); + } + }, + + show: async function show({ commit }, policyId) { + commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetchingItem: true }); + try { + const response = await AgentCapacityPoliciesAPI.show(policyId); + const policy = camelcaseKeys(response.data); + commit(types.SET_AGENT_CAPACITY_POLICY, policy); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { + isFetchingItem: false, + }); + } + }, + + create: async function create({ commit }, policyObj) { + commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isCreating: true }); + try { + const response = await AgentCapacityPoliciesAPI.create( + snakecaseKeys(policyObj) + ); + commit(types.ADD_AGENT_CAPACITY_POLICY, camelcaseKeys(response.data)); + return response.data; + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isCreating: false }); + } + }, + + update: async function update({ commit }, { id, ...policyParams }) { + commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isUpdating: true }); + try { + const response = await AgentCapacityPoliciesAPI.update( + id, + snakecaseKeys(policyParams) + ); + commit(types.EDIT_AGENT_CAPACITY_POLICY, camelcaseKeys(response.data)); + return response.data; + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isUpdating: false }); + } + }, + + delete: async function deletePolicy({ commit }, policyId) { + commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isDeleting: true }); + try { + await AgentCapacityPoliciesAPI.delete(policyId); + commit(types.DELETE_AGENT_CAPACITY_POLICY, policyId); + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isDeleting: false }); + } + }, + + getUsers: async function getUsers({ commit }, policyId) { + commit(types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { + isFetching: true, + }); + try { + const response = await AgentCapacityPoliciesAPI.getUsers(policyId); + commit(types.SET_AGENT_CAPACITY_POLICIES_USERS, { + policyId, + users: camelcaseKeys(response.data), + }); + return response.data; + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit(types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { + isFetching: false, + }); + } + }, +}; + +export const mutations = { + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, + + [types.SET_AGENT_CAPACITY_POLICIES]: MutationHelpers.set, + [types.SET_AGENT_CAPACITY_POLICY]: MutationHelpers.setSingleRecord, + [types.ADD_AGENT_CAPACITY_POLICY]: MutationHelpers.create, + [types.EDIT_AGENT_CAPACITY_POLICY]: MutationHelpers.updateAttributes, + [types.DELETE_AGENT_CAPACITY_POLICY]: MutationHelpers.destroy, + + [types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG](_state, data) { + _state.usersUiFlags = { + ..._state.usersUiFlags, + ...data, + }; + }, + [types.SET_AGENT_CAPACITY_POLICIES_USERS](_state, { policyId, users }) { + const policy = _state.records.find(p => p.id === policyId); + if (policy) { + policy.users = users; + } + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/actions.spec.js b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/actions.spec.js new file mode 100644 index 000000000..0b5ece591 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/actions.spec.js @@ -0,0 +1,227 @@ +import axios from 'axios'; +import { actions } from '../../agentCapacityPolicies'; +import types from '../../../mutation-types'; +import agentCapacityPoliciesList, { camelCaseFixtures } from './fixtures'; +import camelcaseKeys from 'camelcase-keys'; +import snakecaseKeys from 'snakecase-keys'; + +const commit = vi.fn(); + +global.axios = axios; +vi.mock('axios'); +vi.mock('camelcase-keys'); +vi.mock('snakecase-keys'); +vi.mock('../../../utils/api'); + +describe('#actions', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('#get', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ data: agentCapacityPoliciesList }); + camelcaseKeys.mockReturnValue(camelCaseFixtures); + + await actions.get({ commit }); + + expect(camelcaseKeys).toHaveBeenCalledWith(agentCapacityPoliciesList); + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetching: true }], + [types.SET_AGENT_CAPACITY_POLICIES, camelCaseFixtures], + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetching: false }], + ]); + }); + + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + + await actions.get({ commit }); + + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetching: true }], + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#show', () => { + it('sends correct actions if API is success', async () => { + const policyData = agentCapacityPoliciesList[0]; + const camelCasedPolicy = camelCaseFixtures[0]; + + axios.get.mockResolvedValue({ data: policyData }); + camelcaseKeys.mockReturnValue(camelCasedPolicy); + + await actions.show({ commit }, 1); + + expect(camelcaseKeys).toHaveBeenCalledWith(policyData); + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetchingItem: true }], + [types.SET_AGENT_CAPACITY_POLICY, camelCasedPolicy], + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetchingItem: false }], + ]); + }); + + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Not found' }); + + await actions.show({ commit }, 1); + + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetchingItem: true }], + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetchingItem: false }], + ]); + }); + }); + + describe('#create', () => { + it('sends correct actions if API is success', async () => { + const newPolicy = agentCapacityPoliciesList[0]; + const camelCasedData = camelCaseFixtures[0]; + const snakeCasedPolicy = { default_capacity: 10 }; + + axios.post.mockResolvedValue({ data: newPolicy }); + camelcaseKeys.mockReturnValue(camelCasedData); + snakecaseKeys.mockReturnValue(snakeCasedPolicy); + + const result = await actions.create({ commit }, newPolicy); + + expect(snakecaseKeys).toHaveBeenCalledWith(newPolicy); + expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy); + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isCreating: true }], + [types.ADD_AGENT_CAPACITY_POLICY, camelCasedData], + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isCreating: false }], + ]); + expect(result).toEqual(newPolicy); + }); + + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue(new Error('Validation error')); + + await expect(actions.create({ commit }, {})).rejects.toThrow(Error); + + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isCreating: true }], + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isCreating: false }], + ]); + }); + }); + + describe('#update', () => { + it('sends correct actions if API is success', async () => { + const updateParams = { id: 1, name: 'Updated Policy' }; + const responseData = { + ...agentCapacityPoliciesList[0], + name: 'Updated Policy', + }; + const camelCasedData = { + ...camelCaseFixtures[0], + name: 'Updated Policy', + }; + const snakeCasedParams = { name: 'Updated Policy' }; + + axios.patch.mockResolvedValue({ data: responseData }); + camelcaseKeys.mockReturnValue(camelCasedData); + snakecaseKeys.mockReturnValue(snakeCasedParams); + + const result = await actions.update({ commit }, updateParams); + + expect(snakecaseKeys).toHaveBeenCalledWith({ name: 'Updated Policy' }); + expect(camelcaseKeys).toHaveBeenCalledWith(responseData); + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isUpdating: true }], + [types.EDIT_AGENT_CAPACITY_POLICY, camelCasedData], + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isUpdating: false }], + ]); + expect(result).toEqual(responseData); + }); + + it('sends correct actions if API is error', async () => { + axios.patch.mockRejectedValue(new Error('Validation error')); + + await expect( + actions.update({ commit }, { id: 1, name: 'Test' }) + ).rejects.toThrow(Error); + + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isUpdating: true }], + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isUpdating: false }], + ]); + }); + }); + + describe('#delete', () => { + it('sends correct actions if API is success', async () => { + const policyId = 1; + axios.delete.mockResolvedValue({}); + + await actions.delete({ commit }, policyId); + + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isDeleting: true }], + [types.DELETE_AGENT_CAPACITY_POLICY, policyId], + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isDeleting: false }], + ]); + }); + + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue(new Error('Not found')); + + await expect(actions.delete({ commit }, 1)).rejects.toThrow(Error); + + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isDeleting: true }], + [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isDeleting: false }], + ]); + }); + }); + + describe('#getUsers', () => { + it('sends correct actions if API is success', async () => { + const policyId = 1; + const userData = [ + { id: 1, name: 'Agent 1', email: 'agent1@example.com', capacity: 15 }, + { id: 2, name: 'Agent 2', email: 'agent2@example.com', capacity: 20 }, + ]; + const camelCasedUsers = [ + { id: 1, name: 'Agent 1', email: 'agent1@example.com', capacity: 15 }, + { id: 2, name: 'Agent 2', email: 'agent2@example.com', capacity: 20 }, + ]; + + axios.get.mockResolvedValue({ data: userData }); + camelcaseKeys.mockReturnValue(camelCasedUsers); + + const result = await actions.getUsers({ commit }, policyId); + + expect(camelcaseKeys).toHaveBeenCalledWith(userData); + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { isFetching: true }], + [ + types.SET_AGENT_CAPACITY_POLICIES_USERS, + { policyId, users: camelCasedUsers }, + ], + [ + types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, + { isFetching: false }, + ], + ]); + expect(result).toEqual(userData); + }); + + it('sends correct actions if API fails', async () => { + axios.get.mockRejectedValue(new Error('API Error')); + + await expect(actions.getUsers({ commit }, 1)).rejects.toThrow(Error); + + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { isFetching: true }], + [ + types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, + { isFetching: false }, + ], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/fixtures.js b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/fixtures.js new file mode 100644 index 000000000..594d5848c --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/fixtures.js @@ -0,0 +1,77 @@ +export default [ + { + id: 1, + name: 'Standard Capacity Policy', + description: 'Default capacity policy for agents', + default_capacity: 10, + enabled: true, + account_id: 1, + assigned_agent_count: 3, + created_at: '2024-01-01T10:00:00.000Z', + updated_at: '2024-01-01T10:00:00.000Z', + users: [], + }, + { + id: 2, + name: 'High Capacity Policy', + description: 'High capacity policy for senior agents', + default_capacity: 20, + enabled: true, + account_id: 1, + assigned_agent_count: 5, + created_at: '2024-01-01T11:00:00.000Z', + updated_at: '2024-01-01T11:00:00.000Z', + users: [], + }, + { + id: 3, + name: 'Disabled Policy', + description: 'Disabled capacity policy', + default_capacity: 5, + enabled: false, + account_id: 1, + assigned_agent_count: 0, + created_at: '2024-01-01T12:00:00.000Z', + updated_at: '2024-01-01T12:00:00.000Z', + users: [], + }, +]; + +export const camelCaseFixtures = [ + { + id: 1, + name: 'Standard Capacity Policy', + description: 'Default capacity policy for agents', + defaultCapacity: 10, + enabled: true, + accountId: 1, + assignedAgentCount: 3, + createdAt: '2024-01-01T10:00:00.000Z', + updatedAt: '2024-01-01T10:00:00.000Z', + users: [], + }, + { + id: 2, + name: 'High Capacity Policy', + description: 'High capacity policy for senior agents', + defaultCapacity: 20, + enabled: true, + accountId: 1, + assignedAgentCount: 5, + createdAt: '2024-01-01T11:00:00.000Z', + updatedAt: '2024-01-01T11:00:00.000Z', + users: [], + }, + { + id: 3, + name: 'Disabled Policy', + description: 'Disabled capacity policy', + defaultCapacity: 5, + enabled: false, + accountId: 1, + assignedAgentCount: 0, + createdAt: '2024-01-01T12:00:00.000Z', + updatedAt: '2024-01-01T12:00:00.000Z', + users: [], + }, +]; diff --git a/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/getters.spec.js b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/getters.spec.js new file mode 100644 index 000000000..2acd00ad4 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/getters.spec.js @@ -0,0 +1,51 @@ +import { getters } from '../../agentCapacityPolicies'; +import agentCapacityPoliciesList from './fixtures'; + +describe('#getters', () => { + it('getAgentCapacityPolicies', () => { + const state = { records: agentCapacityPoliciesList }; + expect(getters.getAgentCapacityPolicies(state)).toEqual( + agentCapacityPoliciesList + ); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { + isFetching: true, + isFetchingItem: false, + isCreating: false, + isUpdating: false, + isDeleting: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isFetching: true, + isFetchingItem: false, + isCreating: false, + isUpdating: false, + isDeleting: false, + }); + }); + + it('getUsersUIFlags', () => { + const state = { + usersUiFlags: { + isFetching: false, + isDeleting: false, + }, + }; + expect(getters.getUsersUIFlags(state)).toEqual({ + isFetching: false, + isDeleting: false, + }); + }); + + it('getAgentCapacityPolicyById', () => { + const state = { records: agentCapacityPoliciesList }; + expect(getters.getAgentCapacityPolicyById(state)(1)).toEqual( + agentCapacityPoliciesList[0] + ); + expect(getters.getAgentCapacityPolicyById(state)(4)).toEqual({}); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/mutations.spec.js new file mode 100644 index 000000000..0ab033953 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/mutations.spec.js @@ -0,0 +1,303 @@ +import { mutations } from '../../agentCapacityPolicies'; +import types from '../../../mutation-types'; +import agentCapacityPoliciesList from './fixtures'; + +describe('#mutations', () => { + describe('#SET_AGENT_CAPACITY_POLICIES_UI_FLAG', () => { + it('sets single ui flag', () => { + const state = { + uiFlags: { + isFetching: false, + isCreating: false, + }, + }; + + mutations[types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG](state, { + isFetching: true, + }); + + expect(state.uiFlags).toEqual({ + isFetching: true, + isCreating: false, + }); + }); + + it('sets multiple ui flags', () => { + const state = { + uiFlags: { + isFetching: false, + isCreating: false, + isUpdating: false, + }, + }; + + mutations[types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG](state, { + isFetching: true, + isCreating: true, + }); + + expect(state.uiFlags).toEqual({ + isFetching: true, + isCreating: true, + isUpdating: false, + }); + }); + }); + + describe('#SET_AGENT_CAPACITY_POLICIES', () => { + it('sets agent capacity policies records', () => { + const state = { records: [] }; + + mutations[types.SET_AGENT_CAPACITY_POLICIES]( + state, + agentCapacityPoliciesList + ); + + expect(state.records).toEqual(agentCapacityPoliciesList); + }); + + it('replaces existing records', () => { + const state = { records: [{ id: 999, name: 'Old Policy' }] }; + + mutations[types.SET_AGENT_CAPACITY_POLICIES]( + state, + agentCapacityPoliciesList + ); + + expect(state.records).toEqual(agentCapacityPoliciesList); + }); + }); + + describe('#SET_AGENT_CAPACITY_POLICY', () => { + it('sets single agent capacity policy record', () => { + const state = { records: [] }; + + mutations[types.SET_AGENT_CAPACITY_POLICY]( + state, + agentCapacityPoliciesList[0] + ); + + expect(state.records).toEqual([agentCapacityPoliciesList[0]]); + }); + + it('replaces existing record', () => { + const state = { records: [{ id: 1, name: 'Old Policy' }] }; + + mutations[types.SET_AGENT_CAPACITY_POLICY]( + state, + agentCapacityPoliciesList[0] + ); + + expect(state.records).toEqual([agentCapacityPoliciesList[0]]); + }); + }); + + describe('#ADD_AGENT_CAPACITY_POLICY', () => { + it('adds new policy to empty records', () => { + const state = { records: [] }; + + mutations[types.ADD_AGENT_CAPACITY_POLICY]( + state, + agentCapacityPoliciesList[0] + ); + + expect(state.records).toEqual([agentCapacityPoliciesList[0]]); + }); + + it('adds new policy to existing records', () => { + const state = { records: [agentCapacityPoliciesList[0]] }; + + mutations[types.ADD_AGENT_CAPACITY_POLICY]( + state, + agentCapacityPoliciesList[1] + ); + + expect(state.records).toEqual([ + agentCapacityPoliciesList[0], + agentCapacityPoliciesList[1], + ]); + }); + }); + + describe('#EDIT_AGENT_CAPACITY_POLICY', () => { + it('updates existing policy by id', () => { + const state = { + records: [ + { ...agentCapacityPoliciesList[0] }, + { ...agentCapacityPoliciesList[1] }, + ], + }; + + const updatedPolicy = { + ...agentCapacityPoliciesList[0], + name: 'Updated Policy Name', + description: 'Updated Description', + }; + + mutations[types.EDIT_AGENT_CAPACITY_POLICY](state, updatedPolicy); + + expect(state.records[0]).toEqual(updatedPolicy); + expect(state.records[1]).toEqual(agentCapacityPoliciesList[1]); + }); + + it('updates policy with camelCase properties', () => { + const camelCasePolicy = { + id: 1, + name: 'Camel Case Policy', + defaultCapacity: 15, + enabled: true, + }; + + const state = { + records: [camelCasePolicy], + }; + + const updatedPolicy = { + ...camelCasePolicy, + name: 'Updated Camel Case', + defaultCapacity: 25, + }; + + mutations[types.EDIT_AGENT_CAPACITY_POLICY](state, updatedPolicy); + + expect(state.records[0]).toEqual(updatedPolicy); + }); + + it('does nothing if policy id not found', () => { + const state = { + records: [agentCapacityPoliciesList[0]], + }; + + const nonExistentPolicy = { + id: 999, + name: 'Non-existent', + }; + + const originalRecords = [...state.records]; + mutations[types.EDIT_AGENT_CAPACITY_POLICY](state, nonExistentPolicy); + + expect(state.records).toEqual(originalRecords); + }); + }); + + describe('#DELETE_AGENT_CAPACITY_POLICY', () => { + it('deletes policy by id', () => { + const state = { + records: [agentCapacityPoliciesList[0], agentCapacityPoliciesList[1]], + }; + + mutations[types.DELETE_AGENT_CAPACITY_POLICY](state, 1); + + expect(state.records).toEqual([agentCapacityPoliciesList[1]]); + }); + + it('does nothing if id not found', () => { + const state = { + records: [agentCapacityPoliciesList[0]], + }; + + mutations[types.DELETE_AGENT_CAPACITY_POLICY](state, 999); + + expect(state.records).toEqual([agentCapacityPoliciesList[0]]); + }); + + it('handles empty records', () => { + const state = { records: [] }; + + mutations[types.DELETE_AGENT_CAPACITY_POLICY](state, 1); + + expect(state.records).toEqual([]); + }); + }); + + describe('#SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG', () => { + it('sets users ui flags', () => { + const state = { + usersUiFlags: { + isFetching: false, + }, + }; + + mutations[types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG](state, { + isFetching: true, + }); + + expect(state.usersUiFlags).toEqual({ + isFetching: true, + }); + }); + + it('merges with existing flags', () => { + const state = { + usersUiFlags: { + isFetching: false, + isDeleting: true, + }, + }; + + mutations[types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG](state, { + isFetching: true, + }); + + expect(state.usersUiFlags).toEqual({ + isFetching: true, + isDeleting: true, + }); + }); + }); + + describe('#SET_AGENT_CAPACITY_POLICIES_USERS', () => { + it('sets users for existing policy', () => { + const mockUsers = [ + { id: 1, name: 'Agent 1', email: 'agent1@example.com', capacity: 15 }, + { id: 2, name: 'Agent 2', email: 'agent2@example.com', capacity: 20 }, + ]; + + const state = { + records: [ + { id: 1, name: 'Policy 1', users: [] }, + { id: 2, name: 'Policy 2', users: [] }, + ], + }; + + mutations[types.SET_AGENT_CAPACITY_POLICIES_USERS](state, { + policyId: 1, + users: mockUsers, + }); + + expect(state.records[0].users).toEqual(mockUsers); + expect(state.records[1].users).toEqual([]); + }); + + it('replaces existing users', () => { + const oldUsers = [{ id: 99, name: 'Old Agent', capacity: 5 }]; + const newUsers = [{ id: 1, name: 'New Agent', capacity: 15 }]; + + const state = { + records: [{ id: 1, name: 'Policy 1', users: oldUsers }], + }; + + mutations[types.SET_AGENT_CAPACITY_POLICIES_USERS](state, { + policyId: 1, + users: newUsers, + }); + + expect(state.records[0].users).toEqual(newUsers); + }); + + it('does nothing if policy not found', () => { + const state = { + records: [{ id: 1, name: 'Policy 1', users: [] }], + }; + + const originalState = JSON.parse(JSON.stringify(state)); + + mutations[types.SET_AGENT_CAPACITY_POLICIES_USERS](state, { + policyId: 999, + users: [{ id: 1, name: 'Test' }], + }); + + expect(state).toEqual(originalState); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 3b5ba16f2..2a5948d14 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -361,4 +361,15 @@ export default { 'SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG', DELETE_ASSIGNMENT_POLICIES_INBOXES: 'DELETE_ASSIGNMENT_POLICIES_INBOXES', ADD_ASSIGNMENT_POLICIES_INBOXES: 'ADD_ASSIGNMENT_POLICIES_INBOXES', + + // Agent Capacity Policies + SET_AGENT_CAPACITY_POLICIES_UI_FLAG: 'SET_AGENT_CAPACITY_POLICIES_UI_FLAG', + SET_AGENT_CAPACITY_POLICIES: 'SET_AGENT_CAPACITY_POLICIES', + SET_AGENT_CAPACITY_POLICY: 'SET_AGENT_CAPACITY_POLICY', + ADD_AGENT_CAPACITY_POLICY: 'ADD_AGENT_CAPACITY_POLICY', + EDIT_AGENT_CAPACITY_POLICY: 'EDIT_AGENT_CAPACITY_POLICY', + DELETE_AGENT_CAPACITY_POLICY: 'DELETE_AGENT_CAPACITY_POLICY', + SET_AGENT_CAPACITY_POLICIES_USERS: 'SET_AGENT_CAPACITY_POLICIES_USERS', + SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG: + 'SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG', }; diff --git a/enterprise/app/views/api/v1/models/_agent_capacity_policy.json.jbuilder b/enterprise/app/views/api/v1/models/_agent_capacity_policy.json.jbuilder index 8f7a41aa1..2051cc1c1 100644 --- a/enterprise/app/views/api/v1/models/_agent_capacity_policy.json.jbuilder +++ b/enterprise/app/views/api/v1/models/_agent_capacity_policy.json.jbuilder @@ -5,6 +5,7 @@ json.exclusion_rules agent_capacity_policy.exclusion_rules json.created_at agent_capacity_policy.created_at.to_i json.updated_at agent_capacity_policy.updated_at.to_i json.account_id agent_capacity_policy.account_id +json.assigned_agent_count agent_capacity_policy.account_users.count json.inbox_capacity_limits agent_capacity_policy.inbox_capacity_limits do |limit| json.id limit.id
+ {{ description }} +