From 257df305899fb185869bdcb8e709ca7ce41953f7 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Wed, 10 Sep 2025 20:02:11 +0530 Subject: [PATCH] feat: Agent assignment policy Create/Edit pages (#12400) --- .../components/AddDataDropdown.vue | 119 ++++++++ .../AssignmentPolicy/components/BaseInfo.vue | 126 +++++++++ .../AssignmentPolicy/components/DataTable.vue | 80 ++++++ .../components/FairDistribution.vue | 86 ++++++ .../AssignmentPolicy/components/RadioCard.vue | 60 +++++ .../story/AddDataDropdown.story.vue | 58 ++++ .../components/story/BaseInfo.story.vue | 33 +++ .../components/story/DataTable.story.vue | 62 +++++ .../story/FairDistribution.story.vue | 25 ++ .../components/story/RadioCard.story.vue | 61 +++++ .../components-next/input/DurationInput.vue | 4 +- .../dashboard/components-next/input/Input.vue | 6 + .../dashboard/i18n/locale/en/settings.json | 94 +++++++ .../assignmentPolicy.routes.js | 20 ++ .../settings/assignmentPolicy/constants.js | 17 ++ .../pages/AgentAssignmentCreatePage.vue | 74 +++++ .../pages/AgentAssignmentEditPage.vue | 197 ++++++++++++++ .../pages/AgentAssignmentIndexPage.vue | 12 +- .../components/AgentAssignmentPolicyForm.vue | 254 ++++++++++++++++++ .../pages/components/ConfirmInboxDialog.vue | 59 ++++ .../store/modules/assignmentPolicies.js | 86 +++++- .../dashboard/store/modules/inboxes.js | 3 + .../specs/assignmentPolicies/actions.spec.js | 114 +++++++- .../specs/assignmentPolicies/getters.spec.js | 2 + .../assignmentPolicies/mutations.spec.js | 118 ++++++++ .../dashboard/store/mutation-types.js | 3 + 26 files changed, 1765 insertions(+), 8 deletions(-) create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/BaseInfo.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/DataTable.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/FairDistribution.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/RadioCard.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/story/AddDataDropdown.story.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/story/BaseInfo.story.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/story/DataTable.story.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/story/FairDistribution.story.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/story/RadioCard.story.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/constants.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentCreatePage.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentEditPage.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmInboxDialog.vue diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue new file mode 100644 index 000000000..c664d8929 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + {{ item.name }} + + + {{ `#${item.id}` }} + + + + {{ item.email || item.phoneNumber }} + + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/BaseInfo.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/BaseInfo.vue new file mode 100644 index 000000000..6b3a25e48 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/BaseInfo.vue @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ statusPlaceholder }} + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/DataTable.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/DataTable.vue new file mode 100644 index 000000000..986c69a1f --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/DataTable.vue @@ -0,0 +1,80 @@ + + + + + + + + {{ emptyStateMessage }} + + + + + + + + {{ item.name }} + + + + + + {{ item.email || item.phoneNumber }} + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/FairDistribution.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/FairDistribution.vue new file mode 100644 index 000000000..b54ec9ee5 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/FairDistribution.vue @@ -0,0 +1,86 @@ + + + + + + + {{ + t( + 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.FORM.FAIR_DISTRIBUTION.INPUT_MAX' + ) + }} + + + + + + + + + {{ + t( + 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.FORM.FAIR_DISTRIBUTION.DURATION' + ) + }} + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/RadioCard.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/RadioCard.vue new file mode 100644 index 000000000..3d0c8a8b3 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/RadioCard.vue @@ -0,0 +1,60 @@ + + + + + + + + + + + + {{ label }} + + + {{ description }} + + + + 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 new file mode 100644 index 000000000..2ac4d8854 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/AddDataDropdown.story.vue @@ -0,0 +1,58 @@ + + + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/BaseInfo.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/BaseInfo.story.vue new file mode 100644 index 000000000..a3bfe9bee --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/BaseInfo.story.vue @@ -0,0 +1,33 @@ + + + + + + + + + + + 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 new file mode 100644 index 000000000..912b1fdfc --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/DataTable.story.vue @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/FairDistribution.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/FairDistribution.story.vue new file mode 100644 index 000000000..edec5fc92 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/FairDistribution.story.vue @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/RadioCard.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/RadioCard.story.vue new file mode 100644 index 000000000..df1f8655c --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/RadioCard.story.vue @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/input/DurationInput.vue b/app/javascript/dashboard/components-next/input/DurationInput.vue index 802e1d500..7a9fbc12d 100644 --- a/app/javascript/dashboard/components-next/input/DurationInput.vue +++ b/app/javascript/dashboard/components-next/input/DurationInput.vue @@ -78,6 +78,8 @@ watch(unit, () => { {{ t('DURATION_INPUT.HOURS') }} - {{ t('DURATION_INPUT.DAYS') }} + + {{ t('DURATION_INPUT.DAYS') }} + diff --git a/app/javascript/dashboard/components-next/input/Input.vue b/app/javascript/dashboard/components-next/input/Input.vue index 6288f7452..f20c43449 100644 --- a/app/javascript/dashboard/components-next/input/Input.vue +++ b/app/javascript/dashboard/components-next/input/Input.vue @@ -15,6 +15,7 @@ const props = defineProps({ validator: value => ['info', 'error', 'success'].includes(value), }, min: { type: String, default: '' }, + max: { type: String, default: '' }, autofocus: { type: Boolean, default: false }, }); @@ -108,6 +109,11 @@ onMounted(() => { :placeholder="placeholder" :disabled="disabled" :min="['date', 'datetime-local', 'time'].includes(type) ? min : undefined" + :max=" + ['date', 'datetime-local', 'time', 'number'].includes(type) + ? max + : undefined + " class="block w-full reset-base text-sm h-10 !px-3 !py-2.5 !mb-0 outline outline-1 border-none border-0 outline-offset-[-1px] rounded-lg bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out" @input="handleInput" @focus="handleFocus" diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index c0367ea9f..fd7247396 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -468,6 +468,100 @@ }, "NO_RECORDS_FOUND": "No assignment policies found" }, + "CREATE": { + "HEADER": { + "TITLE": "Create assignment policy" + }, + "CREATE_BUTTON": "Create policy", + "API": { + "SUCCESS_MESSAGE": "Assignment policy created successfully", + "ERROR_MESSAGE": "Failed to create assignment policy" + } + }, + "EDIT": { + "HEADER": { + "TITLE": "Edit assignment policy" + }, + "EDIT_BUTTON": "Update policy", + "CONFIRM_ADD_INBOX_DIALOG": { + "TITLE": "Add inbox", + "DESCRIPTION": "{inboxName} inbox is already linked to another policy. Are you sure you want to link it to this policy? It will be unlinked from the other policy.", + "CONFIRM_BUTTON_LABEL": "Continue", + "CANCEL_BUTTON_LABEL": "Cancel" + }, + "API": { + "SUCCESS_MESSAGE": "Assignment policy updated successfully", + "ERROR_MESSAGE": "Failed to update assignment policy" + }, + "INBOX_API": { + "ADD": { + "SUCCESS_MESSAGE": "Inbox added to policy successfully", + "ERROR_MESSAGE": "Failed to add inbox to policy" + }, + "REMOVE": { + "SUCCESS_MESSAGE": "Inbox removed from policy successfully", + "ERROR_MESSAGE": "Failed to remove inbox from policy" + } + } + }, + "FORM": { + "NAME": { + "LABEL": "Policy name:", + "PLACEHOLDER": "Enter policy name" + }, + "DESCRIPTION": { + "LABEL": "Description:", + "PLACEHOLDER": "Enter description" + }, + "STATUS": { + "LABEL": "Status:", + "PLACEHOLDER": "Select status", + "ACTIVE": "Policy is active", + "INACTIVE": "Policy is inactive" + }, + "ASSIGNMENT_ORDER": { + "LABEL": "Assignment order", + "ROUND_ROBIN": { + "LABEL": "Round robin", + "DESCRIPTION": "Assign conversations evenly among agents." + }, + "BALANCED": { + "LABEL": "Balanced", + "DESCRIPTION": "Assign conversations based on available capacity." + } + }, + "ASSIGNMENT_PRIORITY": { + "LABEL": "Assignment priority", + "EARLIEST_CREATED": { + "LABEL": "Earliest created", + "DESCRIPTION": "The conversation that was created first gets assigned first." + }, + "LONGEST_WAITING": { + "LABEL": "Longest waiting", + "DESCRIPTION": "The conversation waiting the longest gets assigned first." + } + }, + "FAIR_DISTRIBUTION": { + "LABEL": "Fair distribution policy", + "DESCRIPTION": "Set the maximum number of conversations that can be assigned per agent within a time window to avoid overloading any one agent. This required field defaults to 100 conversations per hour.", + "INPUT_MAX": "Assign max", + "DURATION": "Conversations per agent in every" + }, + "INBOXES": { + "LABEL": "Added inboxes", + "DESCRIPTION": "Add inboxes for which this policy will be applicable.", + "ADD_BUTTON": "Add inbox", + "DROPDOWN": { + "SEARCH_PLACEHOLDER": "Search and select inboxes to add", + "ADD_BUTTON": "Add" + }, + "EMPTY_STATE": "No inboxes added to this policy, add an inbox to get started", + "API": { + "SUCCESS_MESSAGE": "Inbox successfully added to policy", + "ERROR_MESSAGE": "Failed to add inbox to policy" + } + } + }, "DELETE_POLICY": { "TITLE": "Delete policy", "DESCRIPTION": "Are you sure you want to delete this policy? This action cannot be undone.", 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 6b4c35da3..37934f505 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js @@ -3,6 +3,8 @@ import { frontendURL } from '../../../../helper/URLHelper'; import SettingsWrapper from '../SettingsWrapper.vue'; import AssignmentPolicyIndex from './Index.vue'; import AgentAssignmentIndex from './pages/AgentAssignmentIndexPage.vue'; +import AgentAssignmentCreate from './pages/AgentAssignmentCreatePage.vue'; +import AgentAssignmentEdit from './pages/AgentAssignmentEditPage.vue'; export default { routes: [ @@ -34,6 +36,24 @@ export default { permissions: ['administrator'], }, }, + { + path: 'assignment/create', + name: 'agent_assignment_policy_create', + component: AgentAssignmentCreate, + meta: { + featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2, + permissions: ['administrator'], + }, + }, + { + path: 'assignment/edit/:id', + name: 'agent_assignment_policy_edit', + component: AgentAssignmentEdit, + meta: { + featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2, + permissions: ['administrator'], + }, + }, ], }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/constants.js b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/constants.js new file mode 100644 index 000000000..350faa60c --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/constants.js @@ -0,0 +1,17 @@ +// Assignment order types +export const ROUND_ROBIN = 'round_robin'; +export const BALANCED = 'balanced'; + +// Assignment priority types +export const EARLIEST_CREATED = 'earliest_created'; +export const LONGEST_WAITING = 'longest_waiting'; + +// Default values for fair distribution +export const DEFAULT_FAIR_DISTRIBUTION_LIMIT = 100; +export const DEFAULT_FAIR_DISTRIBUTION_WINDOW = 3600; + +// Options groupings +export const OPTIONS = { + ORDER: [ROUND_ROBIN, BALANCED], + PRIORITY: [EARLIEST_CREATED, LONGEST_WAITING], +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentCreatePage.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentCreatePage.vue new file mode 100644 index 000000000..3c68d9585 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentCreatePage.vue @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentEditPage.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentEditPage.vue new file mode 100644 index 000000000..c54f912d4 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentEditPage.vue @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue index e931d6bbd..be5297a16 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue @@ -44,7 +44,16 @@ const handleBreadcrumbClick = item => { const onClickCreatePolicy = () => { router.push({ - name: 'assignment_policy_create', + name: 'agent_assignment_policy_create', + }); +}; + +const onClickEditPolicy = id => { + router.push({ + name: 'agent_assignment_policy_edit', + params: { + id, + }, }); }; @@ -106,6 +115,7 @@ onMounted(() => { v-bind="policy" :is-fetching-inboxes="inboxUiFlags.isFetching" @fetch-inboxes="handleFetchInboxes" + @edit="onClickEditPolicy" @delete="handleDelete" /> diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue new file mode 100644 index 000000000..d00ead2a8 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + {{ t(`${BASE_KEY}.FORM.FAIR_DISTRIBUTION.LABEL`) }} + + + {{ t(`${BASE_KEY}.FORM.FAIR_DISTRIBUTION.DESCRIPTION`) }} + + + + + + + + + + {{ t(`${BASE_KEY}.FORM.INBOXES.LABEL`) }} + + + {{ t(`${BASE_KEY}.FORM.INBOXES.DESCRIPTION`) }} + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmInboxDialog.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmInboxDialog.vue new file mode 100644 index 000000000..af49a29af --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmInboxDialog.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/app/javascript/dashboard/store/modules/assignmentPolicies.js b/app/javascript/dashboard/store/modules/assignmentPolicies.js index 80c903c4e..9ea2f49c3 100644 --- a/app/javascript/dashboard/store/modules/assignmentPolicies.js +++ b/app/javascript/dashboard/store/modules/assignmentPolicies.js @@ -3,6 +3,7 @@ import types from '../mutation-types'; import AssignmentPoliciesAPI from '../../api/assignmentPolicies'; import { throwErrorMessage } from '../utils/api'; import camelcaseKeys from 'camelcase-keys'; +import snakecaseKeys from 'snakecase-keys'; export const state = { records: [], @@ -15,6 +16,7 @@ export const state = { }, inboxUiFlags: { isFetching: false, + isDeleting: false, }, }; @@ -51,7 +53,7 @@ export const actions = { try { const response = await AssignmentPoliciesAPI.show(policyId); const policy = camelcaseKeys(response.data); - commit(types.EDIT_ASSIGNMENT_POLICY, policy); + commit(types.SET_ASSIGNMENT_POLICY, policy); } catch (error) { throwErrorMessage(error); } finally { @@ -62,7 +64,9 @@ export const actions = { create: async function create({ commit }, policyObj) { commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true }); try { - const response = await AssignmentPoliciesAPI.create(policyObj); + const response = await AssignmentPoliciesAPI.create( + snakecaseKeys(policyObj) + ); commit(types.ADD_ASSIGNMENT_POLICY, camelcaseKeys(response.data)); return response.data; } catch (error) { @@ -76,7 +80,10 @@ export const actions = { update: async function update({ commit }, { id, ...policyParams }) { commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true }); try { - const response = await AssignmentPoliciesAPI.update(id, policyParams); + const response = await AssignmentPoliciesAPI.update( + id, + snakecaseKeys(policyParams) + ); commit(types.EDIT_ASSIGNMENT_POLICY, camelcaseKeys(response.data)); return response.data; } catch (error) { @@ -117,6 +124,68 @@ export const actions = { }); } }, + + setInboxPolicy: async function setInboxPolicy( + { commit }, + { inboxId, policyId } + ) { + try { + const response = await AssignmentPoliciesAPI.setInboxPolicy( + inboxId, + policyId + ); + commit( + types.ADD_ASSIGNMENT_POLICIES_INBOXES, + camelcaseKeys(response.data) + ); + return response.data; + } catch (error) { + throwErrorMessage(error); + throw error; + } + }, + + getInboxPolicy: async function getInboxPolicy(_, { inboxId }) { + try { + const response = await AssignmentPoliciesAPI.getInboxPolicy(inboxId); + return camelcaseKeys(response.data); + } catch (error) { + throwErrorMessage(error); + throw error; + } + }, + + updateInboxPolicy: async function updateInboxPolicy({ commit }, { policy }) { + try { + commit(types.EDIT_ASSIGNMENT_POLICY, policy); + } catch (error) { + throwErrorMessage(error); + throw error; + } + }, + + removeInboxPolicy: async function removeInboxPolicy( + { commit }, + { policyId, inboxId } + ) { + commit(types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { + isDeleting: true, + }); + try { + await AssignmentPoliciesAPI.removeInboxPolicy(inboxId); + commit(types.DELETE_ASSIGNMENT_POLICIES_INBOXES, { + policyId, + inboxId, + }); + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit(types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { + isDeleting: false, + }); + } + }, }; export const mutations = { @@ -128,8 +197,9 @@ export const mutations = { }, [types.SET_ASSIGNMENT_POLICIES]: MutationHelpers.set, + [types.SET_ASSIGNMENT_POLICY]: MutationHelpers.setSingleRecord, [types.ADD_ASSIGNMENT_POLICY]: MutationHelpers.create, - [types.EDIT_ASSIGNMENT_POLICY]: MutationHelpers.update, + [types.EDIT_ASSIGNMENT_POLICY]: MutationHelpers.updateAttributes, [types.DELETE_ASSIGNMENT_POLICY]: MutationHelpers.destroy, [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG](_state, data) { @@ -138,13 +208,19 @@ export const mutations = { ...data, }; }, - [types.SET_ASSIGNMENT_POLICIES_INBOXES](_state, { policyId, inboxes }) { const policy = _state.records.find(p => p.id === policyId); if (policy) { policy.inboxes = inboxes; } }, + [types.DELETE_ASSIGNMENT_POLICIES_INBOXES](_state, { policyId, inboxId }) { + const policy = _state.records.find(p => p.id === policyId); + if (policy) { + policy.inboxes = policy?.inboxes?.filter(inbox => inbox.id !== inboxId); + } + }, + [types.ADD_ASSIGNMENT_POLICIES_INBOXES]: MutationHelpers.updateAttributes, }; export default { diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index c4789a7a9..9886be679 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -29,6 +29,9 @@ export const getters = { getInboxes($state) { return $state.records; }, + getAllInboxes($state) { + return camelcaseKeys($state.records, { deep: true }); + }, getWhatsAppTemplates: $state => inboxId => { const [inbox] = $state.records.filter( record => record.id === Number(inboxId) diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/actions.spec.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/actions.spec.js index 5358144e8..1398f5959 100644 --- a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/actions.spec.js @@ -3,12 +3,14 @@ import { actions } from '../../assignmentPolicies'; import types from '../../../mutation-types'; import assignmentPoliciesList, { 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', () => { @@ -56,7 +58,7 @@ describe('#actions', () => { expect(camelcaseKeys).toHaveBeenCalledWith(policyData); expect(commit.mock.calls).toEqual([ [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: true }], - [types.EDIT_ASSIGNMENT_POLICY, camelCasedPolicy], + [types.SET_ASSIGNMENT_POLICY, camelCasedPolicy], [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: false }], ]); }); @@ -77,12 +79,15 @@ describe('#actions', () => { it('sends correct actions if API is success', async () => { const newPolicy = assignmentPoliciesList[0]; const camelCasedData = camelCaseFixtures[0]; + const snakeCasedPolicy = { assignment_order: 'round_robin' }; 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_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true }], @@ -115,12 +120,15 @@ describe('#actions', () => { ...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_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true }], @@ -211,4 +219,108 @@ describe('#actions', () => { ]); }); }); + + describe('#setInboxPolicy', () => { + it('sends correct actions if API is success', async () => { + const responseData = { success: true, policy_id: 2 }; + const camelCasedData = { success: true, policyId: 2 }; + + axios.post.mockResolvedValue({ data: responseData }); + camelcaseKeys.mockReturnValue(camelCasedData); + + const result = await actions.setInboxPolicy( + { commit }, + { inboxId: 1, policyId: 2 } + ); + + expect(camelcaseKeys).toHaveBeenCalledWith(responseData); + expect(commit.mock.calls).toEqual([ + [types.ADD_ASSIGNMENT_POLICIES_INBOXES, camelCasedData], + ]); + expect(result).toEqual(responseData); + }); + + it('throws error if API fails', async () => { + axios.post.mockRejectedValue(new Error('API Error')); + + await expect( + actions.setInboxPolicy({ commit }, { inboxId: 1, policyId: 2 }) + ).rejects.toThrow(Error); + }); + }); + + describe('#getInboxPolicy', () => { + it('returns camelCased response data if API is success', async () => { + const responseData = { policy_id: 1, name: 'Round Robin' }; + const camelCasedData = { policyId: 1, name: 'Round Robin' }; + + axios.get.mockResolvedValue({ data: responseData }); + camelcaseKeys.mockReturnValue(camelCasedData); + + const result = await actions.getInboxPolicy({}, { inboxId: 1 }); + + expect(camelcaseKeys).toHaveBeenCalledWith(responseData); + expect(result).toEqual(camelCasedData); + }); + + it('throws error if API fails', async () => { + axios.get.mockRejectedValue(new Error('Not found')); + + await expect( + actions.getInboxPolicy({}, { inboxId: 999 }) + ).rejects.toThrow(Error); + }); + }); + + describe('#updateInboxPolicy', () => { + it('commits EDIT_ASSIGNMENT_POLICY mutation', async () => { + const policy = { id: 1, name: 'Updated Policy' }; + + await actions.updateInboxPolicy({ commit }, { policy }); + + expect(commit.mock.calls).toEqual([ + [types.EDIT_ASSIGNMENT_POLICY, policy], + ]); + }); + + it('throws error if commit fails', async () => { + commit.mockImplementation(() => { + throw new Error('Commit failed'); + }); + + await expect( + actions.updateInboxPolicy({ commit }, { policy: {} }) + ).rejects.toThrow(Error); + }); + }); + + describe('#removeInboxPolicy', () => { + it('sends correct actions if API is success', async () => { + const policyId = 1; + const inboxId = 2; + + axios.delete.mockResolvedValue({}); + + await actions.removeInboxPolicy({ commit }, { policyId, inboxId }); + + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: true }], + [types.DELETE_ASSIGNMENT_POLICIES_INBOXES, { policyId, inboxId }], + [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: false }], + ]); + }); + + it('sends correct actions if API fails', async () => { + axios.delete.mockRejectedValue(new Error('Not found')); + + await expect( + actions.removeInboxPolicy({ commit }, { policyId: 1, inboxId: 999 }) + ).rejects.toThrow(Error); + + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: true }], + [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: false }], + ]); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/getters.spec.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/getters.spec.js index 7e0e2041c..4fd1e7ad7 100644 --- a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/getters.spec.js @@ -32,10 +32,12 @@ describe('#getters', () => { const state = { inboxUiFlags: { isFetching: false, + isDeleting: false, }, }; expect(getters.getInboxUiFlags(state)).toEqual({ isFetching: false, + isDeleting: false, }); }); diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/mutations.spec.js index 58d5527ca..b3c029c57 100644 --- a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/mutations.spec.js @@ -62,6 +62,24 @@ describe('#mutations', () => { }); }); + describe('#SET_ASSIGNMENT_POLICY', () => { + it('sets single assignment policy record', () => { + const state = { records: [] }; + + mutations[types.SET_ASSIGNMENT_POLICY](state, assignmentPoliciesList[0]); + + expect(state.records).toEqual([assignmentPoliciesList[0]]); + }); + + it('replaces existing record', () => { + const state = { records: [{ id: 1, name: 'Old Policy' }] }; + + mutations[types.SET_ASSIGNMENT_POLICY](state, assignmentPoliciesList[0]); + + expect(state.records).toEqual([assignmentPoliciesList[0]]); + }); + }); + describe('#ADD_ASSIGNMENT_POLICY', () => { it('adds new policy to empty records', () => { const state = { records: [] }; @@ -264,4 +282,104 @@ describe('#mutations', () => { expect(state).toEqual(originalState); }); }); + + describe('#DELETE_ASSIGNMENT_POLICIES_INBOXES', () => { + it('removes inbox from policy', () => { + const mockInboxes = [ + { id: 1, name: 'Support Inbox' }, + { id: 2, name: 'Sales Inbox' }, + { id: 3, name: 'Marketing Inbox' }, + ]; + + const state = { + records: [ + { id: 1, name: 'Policy 1', inboxes: mockInboxes }, + { id: 2, name: 'Policy 2', inboxes: [] }, + ], + }; + + mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, { + policyId: 1, + inboxId: 2, + }); + + expect(state.records[0].inboxes).toEqual([ + { id: 1, name: 'Support Inbox' }, + { id: 3, name: 'Marketing Inbox' }, + ]); + expect(state.records[1].inboxes).toEqual([]); + }); + + it('does nothing if policy not found', () => { + const state = { + records: [ + { id: 1, name: 'Policy 1', inboxes: [{ id: 1, name: 'Test' }] }, + ], + }; + + const originalState = JSON.parse(JSON.stringify(state)); + + mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, { + policyId: 999, + inboxId: 1, + }); + + expect(state).toEqual(originalState); + }); + + it('does nothing if inbox not found in policy', () => { + const mockInboxes = [{ id: 1, name: 'Support Inbox' }]; + + const state = { + records: [{ id: 1, name: 'Policy 1', inboxes: mockInboxes }], + }; + + mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, { + policyId: 1, + inboxId: 999, + }); + + expect(state.records[0].inboxes).toEqual(mockInboxes); + }); + + it('handles policy with no inboxes', () => { + const state = { + records: [{ id: 1, name: 'Policy 1' }], + }; + + mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, { + policyId: 1, + inboxId: 1, + }); + + expect(state.records[0]).toEqual({ id: 1, name: 'Policy 1' }); + }); + }); + + describe('#ADD_ASSIGNMENT_POLICIES_INBOXES', () => { + it('updates policy attributes using MutationHelpers.updateAttributes', () => { + const state = { + records: [ + { id: 1, name: 'Policy 1', assignedInboxCount: 2 }, + { id: 2, name: 'Policy 2', assignedInboxCount: 1 }, + ], + }; + + const updatedPolicy = { + id: 1, + name: 'Policy 1', + assignedInboxCount: 3, + inboxes: [{ id: 1, name: 'New Inbox' }], + }; + + mutations[types.ADD_ASSIGNMENT_POLICIES_INBOXES](state, updatedPolicy); + + expect(state.records[0]).toEqual(updatedPolicy); + expect(state.records[1]).toEqual({ + id: 2, + name: 'Policy 2', + assignedInboxCount: 1, + }); + }); + }); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index a6fbefa17..3b5ba16f2 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -352,10 +352,13 @@ export default { // Assignment Policies SET_ASSIGNMENT_POLICIES_UI_FLAG: 'SET_ASSIGNMENT_POLICIES_UI_FLAG', SET_ASSIGNMENT_POLICIES: 'SET_ASSIGNMENT_POLICIES', + SET_ASSIGNMENT_POLICY: 'SET_ASSIGNMENT_POLICY', ADD_ASSIGNMENT_POLICY: 'ADD_ASSIGNMENT_POLICY', EDIT_ASSIGNMENT_POLICY: 'EDIT_ASSIGNMENT_POLICY', DELETE_ASSIGNMENT_POLICY: 'DELETE_ASSIGNMENT_POLICY', SET_ASSIGNMENT_POLICIES_INBOXES: 'SET_ASSIGNMENT_POLICIES_INBOXES', SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG: 'SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG', + DELETE_ASSIGNMENT_POLICIES_INBOXES: 'DELETE_ASSIGNMENT_POLICIES_INBOXES', + ADD_ASSIGNMENT_POLICIES_INBOXES: 'ADD_ASSIGNMENT_POLICIES_INBOXES', };
+ {{ description }} +
+ {{ t(`${BASE_KEY}.FORM.FAIR_DISTRIBUTION.DESCRIPTION`) }} +
+ {{ t(`${BASE_KEY}.FORM.INBOXES.DESCRIPTION`) }} +