From ca579bd62a3831c9813d3dd8ccc9840691cc5b68 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:42:55 +0530 Subject: [PATCH] feat: Agent capacity policy Create/Edit pages (#12424) # Pull Request Template ## Description Fixes https://linear.app/chatwoot/issue/CW-5573/feat-createedit-agent-capacity-policy-page ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Loom video https://www.loom.com/share/8de9e3c5d8824cd998d242636540dd18?sid=1314536f-c8d6-41fd-8139-cae9bf94f942 ### Screenshots **Light mode** **Dark mode** ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth --- .../components/AddDataDropdown.vue | 27 +- .../AssignmentPolicy/components/BaseInfo.vue | 3 +- .../AssignmentPolicy/components/DataTable.vue | 20 +- .../components/ExclusionRules.vue | 149 ++++++++ .../components/InboxCapacityLimits.vue | 163 +++++++++ .../story/AddDataDropdown.story.vue | 38 +- .../components/story/DataTable.story.vue | 27 +- .../components/story/ExclusionRules.story.vue | 67 ++++ .../story/InboxCapacityLimits.story.vue | 108 ++++++ .../Contacts/ContactLabels/ContactLabels.vue | 4 +- .../components-next/Label/LabelItem.vue | 3 +- .../dashboard/i18n/locale/en/settings.json | 86 +++++ .../assignmentPolicy.routes.js | 20 ++ .../pages/AgentCapacityCreatePage.vue | 87 +++++ .../pages/AgentCapacityEditPage.vue | 179 ++++++++++ .../components/AgentCapacityPolicyForm.vue | 214 ++++++++++++ .../store/modules/agentCapacityPolicies.js | 156 ++++++++- .../agentCapacityPolicies/actions.spec.js | 191 ++++++++++- .../specs/agentCapacityPolicies/fixtures.js | 126 ++++++- .../agentCapacityPolicies/mutations.spec.js | 324 +++++++++++++++++- .../dashboard/store/mutation-types.js | 6 + 21 files changed, 1965 insertions(+), 33 deletions(-) create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/ExclusionRules.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/InboxCapacityLimits.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/story/ExclusionRules.story.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/story/InboxCapacityLimits.story.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityCreatePage.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityEditPage.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue index c664d8929..7c98db791 100644 --- a/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue @@ -4,6 +4,7 @@ import { useToggle } from '@vueuse/core'; import { vOnClickOutside } from '@vueuse/components'; import { picoSearch } from '@scmmishra/pico-search'; +import Avatar from 'next/avatar/Avatar.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import Input from 'dashboard/components-next/input/Input.vue'; @@ -36,8 +37,8 @@ const filteredItems = computed(() => { return picoSearch(props.items, query, ['name']); }); -const handleAdd = inbox => { - emit('add', inbox); +const handleAdd = item => { + emit('add', item); togglePopover(false); }; @@ -82,21 +83,35 @@ const handleClickOutside = () => { + + - {{ item.name }} + {{ item.name || item.title }} { emit('validationChange', { isValid: isValid.value, + section: 'baseInfo', }); }, { immediate: true } @@ -108,7 +109,7 @@ watch( - + +import Avatar from 'next/avatar/Avatar.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import Spinner from 'dashboard/components-next/spinner/Spinner.vue'; @@ -32,12 +33,14 @@ const handleDelete = itemId => { > - - {{ emptyStateMessage }} - + + {{ emptyStateMessage }} + + { :icon="item.icon" class="size-4 text-n-slate-12 flex-shrink-0" /> - + {{ item.name }} diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/ExclusionRules.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/ExclusionRules.vue new file mode 100644 index 000000000..5412675bd --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/ExclusionRules.vue @@ -0,0 +1,149 @@ + + + + + + + {{ + t( + 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.LABEL' + ) + }} + + + {{ + t( + 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.DESCRIPTION' + ) + }} + + + + + + {{ + t( + 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.TAGS.LABEL' + ) + }} + + + + + + + + + + {{ + t( + 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.DURATION.LABEL' + ) + }} + + + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/InboxCapacityLimits.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/InboxCapacityLimits.vue new file mode 100644 index 000000000..6a799005e --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/InboxCapacityLimits.vue @@ -0,0 +1,163 @@ + + + + + + + {{ t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.LABEL`) }} + + + + + + + + + + + + {{ t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.EMPTY_STATE`) }} + + + + + + + {{ getInboxName(limit.inboxId) }} + + + + + {{ + t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.FIELD.MAX_CONVERSATIONS`) + }} + + + + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/AddDataDropdown.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/AddDataDropdown.story.vue index 2ac4d8854..e69aa798f 100644 --- a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/AddDataDropdown.story.vue +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/AddDataDropdown.story.vue @@ -34,6 +34,29 @@ const mockInboxes = [ }, ]; +const mockTags = [ + { + id: 1, + name: 'urgent', + color: '#ff4757', + }, + { + id: 2, + name: 'bug', + color: '#ff6b6b', + }, + { + id: 3, + name: 'feature-request', + color: '#4834d4', + }, + { + id: 4, + name: 'documentation', + color: '#26de81', + }, +]; + const handleAdd = item => { console.log('Add item:', item); }; @@ -42,9 +65,9 @@ const handleAdd = item => { - + { /> + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/DataTable.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/DataTable.story.vue index 912b1fdfc..a81a29976 100644 --- a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/DataTable.story.vue +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/DataTable.story.vue @@ -22,6 +22,21 @@ const mockItems = [ }, ]; +const mockAgentList = [ + { + id: 1, + name: 'John Doe', + email: 'john.doe@example.com', + avatarUrl: 'https://i.pravatar.cc/150?img=1', + }, + { + id: 2, + name: 'Jane Smith', + email: 'jane.smith@example.com', + avatarUrl: 'https://i.pravatar.cc/150?img=2', + }, +]; + const handleDelete = itemId => { console.log('Delete item:', itemId); }; @@ -30,7 +45,7 @@ const handleDelete = itemId => { @@ -42,6 +57,16 @@ const handleDelete = itemId => { + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/ExclusionRules.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/ExclusionRules.story.vue new file mode 100644 index 000000000..7e0dbd595 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/ExclusionRules.story.vue @@ -0,0 +1,67 @@ + + + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/InboxCapacityLimits.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/InboxCapacityLimits.story.vue new file mode 100644 index 000000000..9d90112a1 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/InboxCapacityLimits.story.vue @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Current Limits: + {{ + JSON.stringify(inboxCapacityLimitsEmpty, null, 2) + }} + + + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue b/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue index af04de9a7..bc1370a0a 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue @@ -86,8 +86,8 @@ const handleLabelAction = async ({ value }) => { } }; -const handleRemoveLabel = labelId => { - return handleLabelAction({ value: labelId }); +const handleRemoveLabel = label => { + return handleLabelAction({ value: label.id }); }; watch( diff --git a/app/javascript/dashboard/components-next/Label/LabelItem.vue b/app/javascript/dashboard/components-next/Label/LabelItem.vue index 65c2600b1..e0917f954 100644 --- a/app/javascript/dashboard/components-next/Label/LabelItem.vue +++ b/app/javascript/dashboard/components-next/Label/LabelItem.vue @@ -15,7 +15,7 @@ const props = defineProps({ const emit = defineEmits(['remove', 'hover']); const handleRemoveLabel = () => { - emit('remove', props.label?.id); + emit('remove', props.label); }; const handleMouseEnter = () => { @@ -45,6 +45,7 @@ const handleMouseEnter = () => { +import { computed, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useStore, useMapGetter } from 'dashboard/composables/store'; +import { useRouter } from 'vue-router'; +import { useAlert } from 'dashboard/composables'; + +import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue'; +import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue'; +import AgentCapacityPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue'; + +const router = useRouter(); +const store = useStore(); +const { t } = useI18n(); + +const formRef = ref(null); +const uiFlags = useMapGetter('agentCapacityPolicies/getUIFlags'); +const labelsList = useMapGetter('labels/getLabels'); + +const allLabels = computed(() => + labelsList.value?.map(({ title, color, id }) => ({ + id, + name: title, + color, + })) +); + +const breadcrumbItems = computed(() => [ + { + label: t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.INDEX.HEADER.TITLE'), + routeName: 'agent_capacity_policy_index', + }, + { + label: t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.CREATE.HEADER.TITLE'), + }, +]); + +const handleBreadcrumbClick = item => { + router.push({ + name: item.routeName, + }); +}; + +const handleSubmit = async formState => { + try { + const policy = await store.dispatch( + 'agentCapacityPolicies/create', + formState + ); + useAlert( + t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.CREATE.API.SUCCESS_MESSAGE') + ); + formRef.value?.resetForm(); + + router.push({ + name: 'agent_capacity_policy_edit', + params: { + id: policy.id, + }, + }); + } catch (error) { + useAlert( + t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.CREATE.API.ERROR_MESSAGE') + ); + } +}; + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityEditPage.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityEditPage.vue new file mode 100644 index 000000000..428e126db --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityEditPage.vue @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue new file mode 100644 index 000000000..3619111c8 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + {{ t(`${BASE_KEY}.FORM.USERS.LABEL`) }} + + + {{ t(`${BASE_KEY}.FORM.USERS.DESCRIPTION`) }} + + + + + + + + + diff --git a/app/javascript/dashboard/store/modules/agentCapacityPolicies.js b/app/javascript/dashboard/store/modules/agentCapacityPolicies.js index e1e766f2c..ea554448f 100644 --- a/app/javascript/dashboard/store/modules/agentCapacityPolicies.js +++ b/app/javascript/dashboard/store/modules/agentCapacityPolicies.js @@ -40,7 +40,10 @@ export const actions = { 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)); + commit( + types.SET_AGENT_CAPACITY_POLICIES, + camelcaseKeys(response.data, { deep: true }) + ); } catch (error) { throwErrorMessage(error); } finally { @@ -52,7 +55,7 @@ export const actions = { commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetchingItem: true }); try { const response = await AgentCapacityPoliciesAPI.show(policyId); - const policy = camelcaseKeys(response.data); + const policy = camelcaseKeys(response.data, { deep: true }); commit(types.SET_AGENT_CAPACITY_POLICY, policy); } catch (error) { throwErrorMessage(error); @@ -69,7 +72,10 @@ export const actions = { const response = await AgentCapacityPoliciesAPI.create( snakecaseKeys(policyObj) ); - commit(types.ADD_AGENT_CAPACITY_POLICY, camelcaseKeys(response.data)); + commit( + types.ADD_AGENT_CAPACITY_POLICY, + camelcaseKeys(response.data, { deep: true }) + ); return response.data; } catch (error) { throwErrorMessage(error); @@ -86,7 +92,10 @@ export const actions = { id, snakecaseKeys(policyParams) ); - commit(types.EDIT_AGENT_CAPACITY_POLICY, camelcaseKeys(response.data)); + commit( + types.EDIT_AGENT_CAPACITY_POLICY, + camelcaseKeys(response.data, { deep: true }) + ); return response.data; } catch (error) { throwErrorMessage(error); @@ -129,6 +138,97 @@ export const actions = { }); } }, + + addUser: async function addUser({ commit }, { policyId, userData }) { + try { + const response = await AgentCapacityPoliciesAPI.addUser( + policyId, + userData + ); + commit(types.ADD_AGENT_CAPACITY_POLICIES_USERS, { + policyId, + user: camelcaseKeys(response.data), + }); + return response.data; + } catch (error) { + throwErrorMessage(error); + throw error; + } + }, + + removeUser: async function removeUser({ commit }, { policyId, userId }) { + commit(types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { + isDeleting: true, + }); + try { + await AgentCapacityPoliciesAPI.removeUser(policyId, userId); + commit(types.DELETE_AGENT_CAPACITY_POLICIES_USERS, { policyId, userId }); + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit(types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { + isDeleting: false, + }); + } + }, + + createInboxLimit: async function createInboxLimit( + { commit }, + { policyId, limitData } + ) { + try { + const response = await AgentCapacityPoliciesAPI.createInboxLimit( + policyId, + limitData + ); + commit( + types.SET_AGENT_CAPACITY_POLICIES_INBOXES, + camelcaseKeys(response.data) + ); + return response.data; + } catch (error) { + throwErrorMessage(error); + throw error; + } + }, + + updateInboxLimit: async function updateInboxLimit( + { commit }, + { policyId, limitId, limitData } + ) { + try { + const response = await AgentCapacityPoliciesAPI.updateInboxLimit( + policyId, + limitId, + limitData + ); + commit( + types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES, + camelcaseKeys(response.data) + ); + return response.data; + } catch (error) { + throwErrorMessage(error); + throw error; + } + }, + + deleteInboxLimit: async function deleteInboxLimit( + { commit }, + { policyId, limitId } + ) { + try { + await AgentCapacityPoliciesAPI.deleteInboxLimit(policyId, limitId); + commit(types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES, { + policyId, + limitId, + }); + } catch (error) { + throwErrorMessage(error); + throw error; + } + }, }; export const mutations = { @@ -157,6 +257,54 @@ export const mutations = { policy.users = users; } }, + [types.ADD_AGENT_CAPACITY_POLICIES_USERS](_state, { policyId, user }) { + const policy = _state.records.find(p => p.id === policyId); + if (policy) { + policy.users = policy.users || []; + policy.users.push(user); + policy.assignedAgentCount = policy.users.length; + } + }, + [types.DELETE_AGENT_CAPACITY_POLICIES_USERS](_state, { policyId, userId }) { + const policy = _state.records.find(p => p.id === policyId); + if (policy) { + policy.users = (policy.users || []).filter(user => user.id !== userId); + policy.assignedAgentCount = policy.users.length; + } + }, + + [types.SET_AGENT_CAPACITY_POLICIES_INBOXES](_state, data) { + const policy = _state.records.find( + p => p.id === data.agentCapacityPolicyId + ); + policy?.inboxCapacityLimits.push({ + id: data.id, + inboxId: data.inboxId, + conversationLimit: data.conversationLimit, + }); + }, + [types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](_state, data) { + const policy = _state.records.find( + p => p.id === data.agentCapacityPolicyId + ); + const limit = policy?.inboxCapacityLimits.find(l => l.id === data.id); + if (limit) { + Object.assign(limit, { + conversationLimit: data.conversationLimit, + }); + } + }, + [types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES]( + _state, + { policyId, limitId } + ) { + const policy = _state.records.find(p => p.id === policyId); + if (policy) { + policy.inboxCapacityLimits = policy.inboxCapacityLimits.filter( + limit => limit.id !== limitId + ); + } + }, }; export default { diff --git a/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/actions.spec.js b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/actions.spec.js index 0b5ece591..3414a2086 100644 --- a/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/actions.spec.js @@ -1,7 +1,12 @@ import axios from 'axios'; import { actions } from '../../agentCapacityPolicies'; import types from '../../../mutation-types'; -import agentCapacityPoliciesList, { camelCaseFixtures } from './fixtures'; +import agentCapacityPoliciesList, { + camelCaseFixtures, + mockUsers, + mockInboxLimits, + camelCaseMockInboxLimits, +} from './fixtures'; import camelcaseKeys from 'camelcase-keys'; import snakecaseKeys from 'snakecase-keys'; @@ -25,7 +30,9 @@ describe('#actions', () => { await actions.get({ commit }); - expect(camelcaseKeys).toHaveBeenCalledWith(agentCapacityPoliciesList); + expect(camelcaseKeys).toHaveBeenCalledWith(agentCapacityPoliciesList, { + deep: true, + }); expect(commit.mock.calls).toEqual([ [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetching: true }], [types.SET_AGENT_CAPACITY_POLICIES, camelCaseFixtures], @@ -55,7 +62,9 @@ describe('#actions', () => { await actions.show({ commit }, 1); - expect(camelcaseKeys).toHaveBeenCalledWith(policyData); + expect(camelcaseKeys).toHaveBeenCalledWith(policyData, { + deep: true, + }); expect(commit.mock.calls).toEqual([ [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetchingItem: true }], [types.SET_AGENT_CAPACITY_POLICY, camelCasedPolicy], @@ -88,7 +97,9 @@ describe('#actions', () => { const result = await actions.create({ commit }, newPolicy); expect(snakecaseKeys).toHaveBeenCalledWith(newPolicy); - expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy); + expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy, { + deep: true, + }); expect(commit.mock.calls).toEqual([ [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isCreating: true }], [types.ADD_AGENT_CAPACITY_POLICY, camelCasedData], @@ -129,7 +140,9 @@ describe('#actions', () => { const result = await actions.update({ commit }, updateParams); expect(snakecaseKeys).toHaveBeenCalledWith({ name: 'Updated Policy' }); - expect(camelcaseKeys).toHaveBeenCalledWith(responseData); + expect(camelcaseKeys).toHaveBeenCalledWith(responseData, { + deep: true, + }); expect(commit.mock.calls).toEqual([ [types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isUpdating: true }], [types.EDIT_AGENT_CAPACITY_POLICY, camelCasedData], @@ -224,4 +237,172 @@ describe('#actions', () => { ]); }); }); + + describe('#addUser', () => { + it('sends correct actions if API is success', async () => { + const policyId = 1; + const userData = { user_id: 3, capacity: 12 }; + const responseData = mockUsers[2]; + const camelCasedUser = mockUsers[2]; + + axios.post.mockResolvedValue({ data: responseData }); + camelcaseKeys.mockReturnValue(camelCasedUser); + + const result = await actions.addUser({ commit }, { policyId, userData }); + + expect(camelcaseKeys).toHaveBeenCalledWith(responseData); + expect(commit.mock.calls).toEqual([ + [ + types.ADD_AGENT_CAPACITY_POLICIES_USERS, + { policyId, user: camelCasedUser }, + ], + ]); + expect(result).toEqual(responseData); + }); + + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue(new Error('Validation error')); + + await expect( + actions.addUser({ commit }, { policyId: 1, userData: {} }) + ).rejects.toThrow(Error); + + expect(commit).not.toHaveBeenCalled(); + }); + }); + + describe('#removeUser', () => { + it('sends correct actions if API is success', async () => { + const policyId = 1; + const userId = 2; + axios.delete.mockResolvedValue({}); + + await actions.removeUser({ commit }, { policyId, userId }); + + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { isDeleting: true }], + [types.DELETE_AGENT_CAPACITY_POLICIES_USERS, { policyId, userId }], + [ + types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, + { isDeleting: false }, + ], + ]); + }); + + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue(new Error('Not found')); + + await expect( + actions.removeUser({ commit }, { policyId: 1, userId: 2 }) + ).rejects.toThrow(Error); + + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { isDeleting: true }], + [ + types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, + { isDeleting: false }, + ], + ]); + }); + }); + + describe('#createInboxLimit', () => { + it('sends correct actions if API is success', async () => { + const policyId = 1; + const limitData = { inbox_id: 3, conversation_limit: 20 }; + const responseData = mockInboxLimits[2]; + const camelCasedData = camelCaseMockInboxLimits[2]; + + axios.post.mockResolvedValue({ data: responseData }); + camelcaseKeys.mockReturnValue(camelCasedData); + + const result = await actions.createInboxLimit( + { commit }, + { policyId, limitData } + ); + + expect(camelcaseKeys).toHaveBeenCalledWith(responseData); + expect(commit.mock.calls).toEqual([ + [types.SET_AGENT_CAPACITY_POLICIES_INBOXES, camelCasedData], + ]); + expect(result).toEqual(responseData); + }); + + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue(new Error('Validation error')); + + await expect( + actions.createInboxLimit({ commit }, { policyId: 1, limitData: {} }) + ).rejects.toThrow(Error); + + expect(commit).not.toHaveBeenCalled(); + }); + }); + + describe('#updateInboxLimit', () => { + it('sends correct actions if API is success', async () => { + const policyId = 1; + const limitId = 1; + const limitData = { conversation_limit: 25 }; + const responseData = { + ...mockInboxLimits[0], + conversation_limit: 25, + }; + const camelCasedData = { + ...camelCaseMockInboxLimits[0], + conversationLimit: 25, + }; + + axios.put.mockResolvedValue({ data: responseData }); + camelcaseKeys.mockReturnValue(camelCasedData); + + const result = await actions.updateInboxLimit( + { commit }, + { policyId, limitId, limitData } + ); + + expect(camelcaseKeys).toHaveBeenCalledWith(responseData); + expect(commit.mock.calls).toEqual([ + [types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES, camelCasedData], + ]); + expect(result).toEqual(responseData); + }); + + it('sends correct actions if API is error', async () => { + axios.put.mockRejectedValue(new Error('Validation error')); + + await expect( + actions.updateInboxLimit( + { commit }, + { policyId: 1, limitId: 1, limitData: {} } + ) + ).rejects.toThrow(Error); + + expect(commit).not.toHaveBeenCalled(); + }); + }); + + describe('#deleteInboxLimit', () => { + it('sends correct actions if API is success', async () => { + const policyId = 1; + const limitId = 1; + axios.delete.mockResolvedValue({}); + + await actions.deleteInboxLimit({ commit }, { policyId, limitId }); + + expect(commit.mock.calls).toEqual([ + [types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES, { policyId, limitId }], + ]); + }); + + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue(new Error('Not found')); + + await expect( + actions.deleteInboxLimit({ commit }, { policyId: 1, limitId: 1 }) + ).rejects.toThrow(Error); + + expect(commit).not.toHaveBeenCalled(); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/fixtures.js b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/fixtures.js index 594d5848c..c79597919 100644 --- a/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/fixtures.js +++ b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/fixtures.js @@ -10,6 +10,20 @@ export default [ created_at: '2024-01-01T10:00:00.000Z', updated_at: '2024-01-01T10:00:00.000Z', users: [], + inbox_capacity_limits: [ + { + id: 1, + inbox_id: 1, + conversation_limit: 15, + agent_capacity_policy_id: 1, + }, + { + id: 2, + inbox_id: 2, + conversation_limit: 8, + agent_capacity_policy_id: 1, + }, + ], }, { id: 2, @@ -21,7 +35,21 @@ export default [ assigned_agent_count: 5, created_at: '2024-01-01T11:00:00.000Z', updated_at: '2024-01-01T11:00:00.000Z', - users: [], + users: [ + { + id: 1, + name: 'Agent Smith', + email: 'agent.smith@example.com', + capacity: 25, + }, + { + id: 2, + name: 'Agent Johnson', + email: 'agent.johnson@example.com', + capacity: 18, + }, + ], + inbox_capacity_limits: [], }, { id: 3, @@ -34,6 +62,7 @@ export default [ created_at: '2024-01-01T12:00:00.000Z', updated_at: '2024-01-01T12:00:00.000Z', users: [], + inbox_capacity_limits: [], }, ]; @@ -49,6 +78,20 @@ export const camelCaseFixtures = [ createdAt: '2024-01-01T10:00:00.000Z', updatedAt: '2024-01-01T10:00:00.000Z', users: [], + inboxCapacityLimits: [ + { + id: 1, + inboxId: 1, + conversationLimit: 15, + agentCapacityPolicyId: 1, + }, + { + id: 2, + inboxId: 2, + conversationLimit: 8, + agentCapacityPolicyId: 1, + }, + ], }, { id: 2, @@ -60,7 +103,21 @@ export const camelCaseFixtures = [ assignedAgentCount: 5, createdAt: '2024-01-01T11:00:00.000Z', updatedAt: '2024-01-01T11:00:00.000Z', - users: [], + users: [ + { + id: 1, + name: 'Agent Smith', + email: 'agent.smith@example.com', + capacity: 25, + }, + { + id: 2, + name: 'Agent Johnson', + email: 'agent.johnson@example.com', + capacity: 18, + }, + ], + inboxCapacityLimits: [], }, { id: 3, @@ -73,5 +130,70 @@ export const camelCaseFixtures = [ createdAt: '2024-01-01T12:00:00.000Z', updatedAt: '2024-01-01T12:00:00.000Z', users: [], + inboxCapacityLimits: [], + }, +]; + +// Additional test data for user and inbox limit operations +export const mockUsers = [ + { + id: 1, + name: 'Agent Smith', + email: 'agent.smith@example.com', + capacity: 25, + }, + { + id: 2, + name: 'Agent Johnson', + email: 'agent.johnson@example.com', + capacity: 18, + }, + { + id: 3, + name: 'Agent Brown', + email: 'agent.brown@example.com', + capacity: 12, + }, +]; + +export const mockInboxLimits = [ + { + id: 1, + inbox_id: 1, + conversation_limit: 15, + agent_capacity_policy_id: 1, + }, + { + id: 2, + inbox_id: 2, + conversation_limit: 8, + agent_capacity_policy_id: 1, + }, + { + id: 3, + inbox_id: 3, + conversation_limit: 20, + agent_capacity_policy_id: 2, + }, +]; + +export const camelCaseMockInboxLimits = [ + { + id: 1, + inboxId: 1, + conversationLimit: 15, + agentCapacityPolicyId: 1, + }, + { + id: 2, + inboxId: 2, + conversationLimit: 8, + agentCapacityPolicyId: 1, + }, + { + id: 3, + inboxId: 3, + conversationLimit: 20, + agentCapacityPolicyId: 2, }, ]; diff --git a/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/mutations.spec.js index 0ab033953..f6cfba80d 100644 --- a/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/agentCapacityPolicies/mutations.spec.js @@ -1,6 +1,6 @@ import { mutations } from '../../agentCapacityPolicies'; import types from '../../../mutation-types'; -import agentCapacityPoliciesList from './fixtures'; +import agentCapacityPoliciesList, { mockUsers } from './fixtures'; describe('#mutations', () => { describe('#SET_AGENT_CAPACITY_POLICIES_UI_FLAG', () => { @@ -248,7 +248,7 @@ describe('#mutations', () => { describe('#SET_AGENT_CAPACITY_POLICIES_USERS', () => { it('sets users for existing policy', () => { - const mockUsers = [ + const testUsers = [ { id: 1, name: 'Agent 1', email: 'agent1@example.com', capacity: 15 }, { id: 2, name: 'Agent 2', email: 'agent2@example.com', capacity: 20 }, ]; @@ -262,10 +262,10 @@ describe('#mutations', () => { mutations[types.SET_AGENT_CAPACITY_POLICIES_USERS](state, { policyId: 1, - users: mockUsers, + users: testUsers, }); - expect(state.records[0].users).toEqual(mockUsers); + expect(state.records[0].users).toEqual(testUsers); expect(state.records[1].users).toEqual([]); }); @@ -300,4 +300,320 @@ describe('#mutations', () => { expect(state).toEqual(originalState); }); }); + + describe('#ADD_AGENT_CAPACITY_POLICIES_USERS', () => { + it('adds user to existing policy', () => { + const state = { + records: [ + { id: 1, name: 'Policy 1', users: [] }, + { id: 2, name: 'Policy 2', users: [] }, + ], + }; + + mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, { + policyId: 1, + user: mockUsers[0], + }); + + expect(state.records[0].users).toEqual([mockUsers[0]]); + expect(state.records[1].users).toEqual([]); + }); + + it('adds user to policy with existing users', () => { + const state = { + records: [{ id: 1, name: 'Policy 1', users: [mockUsers[0]] }], + }; + + mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, { + policyId: 1, + user: mockUsers[1], + }); + + expect(state.records[0].users).toEqual([mockUsers[0], mockUsers[1]]); + }); + + it('initializes users array if undefined', () => { + const state = { + records: [{ id: 1, name: 'Policy 1' }], + }; + + mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, { + policyId: 1, + user: mockUsers[0], + }); + + expect(state.records[0].users).toEqual([mockUsers[0]]); + }); + + it('updates assigned agent count', () => { + const state = { + records: [{ id: 1, name: 'Policy 1', users: [] }], + }; + + mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, { + policyId: 1, + user: mockUsers[0], + }); + + expect(state.records[0].assignedAgentCount).toEqual(1); + }); + }); + + describe('#DELETE_AGENT_CAPACITY_POLICIES_USERS', () => { + it('removes user from policy', () => { + const state = { + records: [ + { + id: 1, + name: 'Policy 1', + users: [mockUsers[0], mockUsers[1], mockUsers[2]], + }, + ], + }; + + mutations[types.DELETE_AGENT_CAPACITY_POLICIES_USERS](state, { + policyId: 1, + userId: 2, + }); + + expect(state.records[0].users).toEqual([mockUsers[0], mockUsers[2]]); + }); + + it('handles removing non-existent user', () => { + const state = { + records: [ + { + id: 1, + name: 'Policy 1', + users: [mockUsers[0]], + }, + ], + }; + + mutations[types.DELETE_AGENT_CAPACITY_POLICIES_USERS](state, { + policyId: 1, + userId: 999, + }); + + expect(state.records[0].users).toEqual([mockUsers[0]]); + }); + + it('updates assigned agent count', () => { + const state = { + records: [{ id: 1, name: 'Policy 1', users: [mockUsers[0]] }], + }; + + mutations[types.DELETE_AGENT_CAPACITY_POLICIES_USERS](state, { + policyId: 1, + userId: 1, + }); + + expect(state.records[0].assignedAgentCount).toEqual(0); + }); + }); + + describe('#SET_AGENT_CAPACITY_POLICIES_INBOXES', () => { + it('adds inbox limit to policy', () => { + const state = { + records: [ + { + id: 1, + name: 'Policy 1', + inboxCapacityLimits: [], + }, + ], + }; + + const inboxLimitData = { + id: 1, + inboxId: 1, + conversationLimit: 15, + agentCapacityPolicyId: 1, + }; + + mutations[types.SET_AGENT_CAPACITY_POLICIES_INBOXES]( + state, + inboxLimitData + ); + + expect(state.records[0].inboxCapacityLimits).toEqual([ + { + id: 1, + inboxId: 1, + conversationLimit: 15, + }, + ]); + }); + + it('does nothing if policy not found', () => { + const state = { + records: [{ id: 1, name: 'Policy 1', inboxCapacityLimits: [] }], + }; + + const originalState = JSON.parse(JSON.stringify(state)); + + mutations[types.SET_AGENT_CAPACITY_POLICIES_INBOXES](state, { + id: 1, + inboxId: 1, + conversationLimit: 15, + agentCapacityPolicyId: 999, + }); + + expect(state).toEqual(originalState); + }); + }); + + describe('#EDIT_AGENT_CAPACITY_POLICIES_INBOXES', () => { + it('updates existing inbox limit', () => { + const state = { + records: [ + { + id: 1, + name: 'Policy 1', + inboxCapacityLimits: [ + { + id: 1, + inboxId: 1, + conversationLimit: 15, + }, + { + id: 2, + inboxId: 2, + conversationLimit: 8, + }, + ], + }, + ], + }; + + mutations[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](state, { + id: 1, + inboxId: 1, + conversationLimit: 25, + agentCapacityPolicyId: 1, + }); + + expect(state.records[0].inboxCapacityLimits[0]).toEqual({ + id: 1, + inboxId: 1, + conversationLimit: 25, + }); + expect(state.records[0].inboxCapacityLimits[1]).toEqual({ + id: 2, + inboxId: 2, + conversationLimit: 8, + }); + }); + + it('does nothing if limit not found', () => { + const state = { + records: [ + { + id: 1, + name: 'Policy 1', + inboxCapacityLimits: [ + { + id: 1, + inboxId: 1, + conversationLimit: 15, + }, + ], + }, + ], + }; + + const originalLimits = [...state.records[0].inboxCapacityLimits]; + + mutations[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](state, { + id: 999, + inboxId: 1, + conversationLimit: 25, + agentCapacityPolicyId: 1, + }); + + expect(state.records[0].inboxCapacityLimits).toEqual(originalLimits); + }); + + it('does nothing if policy not found', () => { + const state = { + records: [{ id: 1, name: 'Policy 1', inboxCapacityLimits: [] }], + }; + + const originalState = JSON.parse(JSON.stringify(state)); + + mutations[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](state, { + id: 1, + inboxId: 1, + conversationLimit: 25, + agentCapacityPolicyId: 999, + }); + + expect(state).toEqual(originalState); + }); + }); + + describe('#DELETE_AGENT_CAPACITY_POLICIES_INBOXES', () => { + it('removes inbox limit from policy', () => { + const state = { + records: [ + { + id: 1, + name: 'Policy 1', + inboxCapacityLimits: [ + { + id: 1, + inboxId: 1, + conversationLimit: 15, + }, + { + id: 2, + inboxId: 2, + conversationLimit: 8, + }, + ], + }, + ], + }; + + mutations[types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES](state, { + policyId: 1, + limitId: 1, + }); + + expect(state.records[0].inboxCapacityLimits).toEqual([ + { + id: 2, + inboxId: 2, + conversationLimit: 8, + }, + ]); + }); + + it('handles removing non-existent limit', () => { + const state = { + records: [ + { + id: 1, + name: 'Policy 1', + inboxCapacityLimits: [ + { + id: 1, + inboxId: 1, + conversationLimit: 15, + }, + ], + }, + ], + }; + + const originalLimits = [...state.records[0].inboxCapacityLimits]; + + mutations[types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES](state, { + policyId: 1, + limitId: 999, + }); + + expect(state.records[0].inboxCapacityLimits).toEqual(originalLimits); + }); + }); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 2a5948d14..4f361e140 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -372,4 +372,10 @@ export default { SET_AGENT_CAPACITY_POLICIES_USERS: 'SET_AGENT_CAPACITY_POLICIES_USERS', SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG: 'SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG', + ADD_AGENT_CAPACITY_POLICIES_USERS: 'ADD_AGENT_CAPACITY_POLICIES_USERS', + DELETE_AGENT_CAPACITY_POLICIES_USERS: 'DELETE_AGENT_CAPACITY_POLICIES_USERS', + SET_AGENT_CAPACITY_POLICIES_INBOXES: 'SET_AGENT_CAPACITY_POLICIES_INBOXES', + EDIT_AGENT_CAPACITY_POLICIES_INBOXES: 'EDIT_AGENT_CAPACITY_POLICIES_INBOXES', + DELETE_AGENT_CAPACITY_POLICIES_INBOXES: + 'DELETE_AGENT_CAPACITY_POLICIES_INBOXES', };
+ {{ + t( + 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.DESCRIPTION' + ) + }} +
{{ + JSON.stringify(inboxCapacityLimitsEmpty, null, 2) + }}
+ {{ t(`${BASE_KEY}.FORM.USERS.DESCRIPTION`) }} +