diff --git a/app/javascript/dashboard/api/assignmentPolicies.js b/app/javascript/dashboard/api/assignmentPolicies.js
new file mode 100644
index 000000000..e6baca97a
--- /dev/null
+++ b/app/javascript/dashboard/api/assignmentPolicies.js
@@ -0,0 +1,36 @@
+/* global axios */
+
+import ApiClient from './ApiClient';
+
+class AssignmentPolicies extends ApiClient {
+ constructor() {
+ super('assignment_policies', { accountScoped: true });
+ }
+
+ getInboxes(policyId) {
+ return axios.get(`${this.url}/${policyId}/inboxes`);
+ }
+
+ setInboxPolicy(inboxId, policyId) {
+ return axios.post(
+ `/api/v1/accounts/${this.accountIdFromRoute}/inboxes/${inboxId}/assignment_policy`,
+ {
+ assignment_policy_id: policyId,
+ }
+ );
+ }
+
+ getInboxPolicy(inboxId) {
+ return axios.get(
+ `/api/v1/accounts/${this.accountIdFromRoute}/inboxes/${inboxId}/assignment_policy`
+ );
+ }
+
+ removeInboxPolicy(inboxId) {
+ return axios.delete(
+ `/api/v1/accounts/${this.accountIdFromRoute}/inboxes/${inboxId}/assignment_policy`
+ );
+ }
+}
+
+export default new AssignmentPolicies();
diff --git a/app/javascript/dashboard/api/specs/assignmentPolicies.spec.js b/app/javascript/dashboard/api/specs/assignmentPolicies.spec.js
new file mode 100644
index 000000000..8d0aea7d0
--- /dev/null
+++ b/app/javascript/dashboard/api/specs/assignmentPolicies.spec.js
@@ -0,0 +1,70 @@
+import assignmentPolicies from '../assignmentPolicies';
+import ApiClient from '../ApiClient';
+
+describe('#AssignmentPoliciesAPI', () => {
+ it('creates correct instance', () => {
+ expect(assignmentPolicies).toBeInstanceOf(ApiClient);
+ expect(assignmentPolicies).toHaveProperty('get');
+ expect(assignmentPolicies).toHaveProperty('show');
+ expect(assignmentPolicies).toHaveProperty('create');
+ expect(assignmentPolicies).toHaveProperty('update');
+ expect(assignmentPolicies).toHaveProperty('delete');
+ expect(assignmentPolicies).toHaveProperty('getInboxes');
+ expect(assignmentPolicies).toHaveProperty('setInboxPolicy');
+ expect(assignmentPolicies).toHaveProperty('getInboxPolicy');
+ expect(assignmentPolicies).toHaveProperty('removeInboxPolicy');
+ });
+
+ describe('API calls', () => {
+ const originalAxios = window.axios;
+ const axiosMock = {
+ get: vi.fn(() => Promise.resolve()),
+ post: vi.fn(() => Promise.resolve()),
+ delete: vi.fn(() => Promise.resolve()),
+ };
+
+ beforeEach(() => {
+ window.axios = axiosMock;
+ // Mock accountIdFromRoute
+ Object.defineProperty(assignmentPolicies, 'accountIdFromRoute', {
+ get: () => '1',
+ configurable: true,
+ });
+ });
+
+ afterEach(() => {
+ window.axios = originalAxios;
+ });
+
+ it('#getInboxes', () => {
+ assignmentPolicies.getInboxes(123);
+ expect(axiosMock.get).toHaveBeenCalledWith(
+ '/api/v1/accounts/1/assignment_policies/123/inboxes'
+ );
+ });
+
+ it('#setInboxPolicy', () => {
+ assignmentPolicies.setInboxPolicy(456, 123);
+ expect(axiosMock.post).toHaveBeenCalledWith(
+ '/api/v1/accounts/1/inboxes/456/assignment_policy',
+ {
+ assignment_policy_id: 123,
+ }
+ );
+ });
+
+ it('#getInboxPolicy', () => {
+ assignmentPolicies.getInboxPolicy(456);
+ expect(axiosMock.get).toHaveBeenCalledWith(
+ '/api/v1/accounts/1/inboxes/456/assignment_policy'
+ );
+ });
+
+ it('#removeInboxPolicy', () => {
+ assignmentPolicies.removeInboxPolicy(456);
+ expect(axiosMock.delete).toHaveBeenCalledWith(
+ '/api/v1/accounts/1/inboxes/456/assignment_policy'
+ );
+ });
+ });
+});
diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.story.vue
new file mode 100644
index 000000000..cd6f1d49b
--- /dev/null
+++ b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.story.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.vue
new file mode 100644
index 000000000..fe9965777
--- /dev/null
+++ b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+ {{ name }}
+
+
+
+
+ {{
+ enabled
+ ? t(
+ 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.ACTIVE'
+ )
+ : t(
+ 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.INACTIVE'
+ )
+ }}
+
+
+
+
+
+
+
+
+ {{ description }}
+
+
+
+ {{
+ `${t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.ORDER')}:`
+ }}
+ {{ order }}
+
+
+
+ {{
+ `${t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.PRIORITY')}:`
+ }}
+ {{ priority }}
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/CardPopover.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/CardPopover.vue
new file mode 100644
index 000000000..013e6d5fe
--- /dev/null
+++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/CardPopover.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+ {{ `#${item.id}` }}
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/CardPopover.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/CardPopover.story.vue
new file mode 100644
index 000000000..d010c16f9
--- /dev/null
+++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/CardPopover.story.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ console.log('Fetch triggered')"
+ />
+
+
+
+
diff --git a/app/javascript/dashboard/helper/commons.js b/app/javascript/dashboard/helper/commons.js
index b12d1aa3d..3be7538bb 100644
--- a/app/javascript/dashboard/helper/commons.js
+++ b/app/javascript/dashboard/helper/commons.js
@@ -96,3 +96,18 @@ export const sanitizeVariableSearchKey = (searchKey = '') => {
.replace(/,/g, '') // remove commas
.trim();
};
+
+/**
+ * Convert underscore-separated string to title case.
+ * Eg. "round_robin" => "Round Robin"
+ * @param {string} str
+ * @returns {string}
+ */
+export const formatToTitleCase = str => {
+ return (
+ str
+ ?.replace(/_/g, ' ')
+ .replace(/\b\w/g, l => l.toUpperCase())
+ .trim() || ''
+ );
+};
diff --git a/app/javascript/dashboard/helper/specs/commons.spec.js b/app/javascript/dashboard/helper/specs/commons.spec.js
index 466cdcf45..d892d3a94 100644
--- a/app/javascript/dashboard/helper/specs/commons.spec.js
+++ b/app/javascript/dashboard/helper/specs/commons.spec.js
@@ -5,6 +5,7 @@ import {
convertToCategorySlug,
convertToPortalSlug,
sanitizeVariableSearchKey,
+ formatToTitleCase,
} from '../commons';
describe('#getTypingUsersText', () => {
@@ -142,3 +143,51 @@ describe('sanitizeVariableSearchKey', () => {
expect(sanitizeVariableSearchKey()).toBe('');
});
});
+
+describe('formatToTitleCase', () => {
+ it('converts underscore-separated string to title case', () => {
+ expect(formatToTitleCase('round_robin')).toBe('Round Robin');
+ });
+
+ it('converts single word to title case', () => {
+ expect(formatToTitleCase('priority')).toBe('Priority');
+ });
+
+ it('converts multiple underscores to title case', () => {
+ expect(formatToTitleCase('auto_assignment_policy')).toBe(
+ 'Auto Assignment Policy'
+ );
+ });
+
+ it('handles already capitalized words', () => {
+ expect(formatToTitleCase('HIGH_PRIORITY')).toBe('HIGH PRIORITY');
+ });
+
+ it('handles mixed case with underscores', () => {
+ expect(formatToTitleCase('first_Name_last')).toBe('First Name Last');
+ });
+
+ it('handles empty string', () => {
+ expect(formatToTitleCase('')).toBe('');
+ });
+
+ it('handles null input', () => {
+ expect(formatToTitleCase(null)).toBe('');
+ });
+
+ it('handles undefined input', () => {
+ expect(formatToTitleCase(undefined)).toBe('');
+ });
+
+ it('handles string without underscores', () => {
+ expect(formatToTitleCase('hello')).toBe('Hello');
+ });
+
+ it('handles string with numbers', () => {
+ expect(formatToTitleCase('priority_1_high')).toBe('Priority 1 High');
+ });
+
+ it('handles leading and trailing underscores', () => {
+ expect(formatToTitleCase('_leading_trailing_')).toBe('Leading Trailing');
+ });
+});
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index 2e24bace4..c0367ea9f 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -451,6 +451,31 @@
"Add agents to a policy - one policy per agent"
]
}
+ },
+ "AGENT_ASSIGNMENT_POLICY": {
+ "INDEX": {
+ "HEADER": {
+ "TITLE": "Assignment policy",
+ "CREATE_POLICY": "New policy"
+ },
+ "CARD": {
+ "ORDER": "Order",
+ "PRIORITY": "Priority",
+ "ACTIVE": "Active",
+ "INACTIVE": "Inactive",
+ "POPOVER": "Added inboxes",
+ "EDIT": "Edit"
+ },
+ "NO_RECORDS_FOUND": "No assignment policies found"
+ },
+ "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"
+ }
}
}
}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue b/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue
index 44bad28c1..9e34cb384 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue
@@ -23,7 +23,7 @@ defineProps({
-
+
@@ -37,6 +37,6 @@ defineProps({
-
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue
index 6b0f88033..b41d9990a 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue
@@ -11,7 +11,7 @@ const { t } = useI18n();
const agentAssignments = computed(() => [
{
- key: 'assignment_policy',
+ key: 'agent_assignment_policy_index',
title: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.TITLE'),
description: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_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 2d62674b0..6b4c35da3 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js
@@ -2,6 +2,7 @@ import { FEATURE_FLAGS } from '../../../../featureFlags';
import { frontendURL } from '../../../../helper/URLHelper';
import SettingsWrapper from '../SettingsWrapper.vue';
import AssignmentPolicyIndex from './Index.vue';
+import AgentAssignmentIndex from './pages/AgentAssignmentIndexPage.vue';
export default {
routes: [
@@ -24,6 +25,15 @@ export default {
permissions: ['administrator'],
},
},
+ {
+ path: 'assignment',
+ name: 'agent_assignment_policy_index',
+ component: AgentAssignmentIndex,
+ meta: {
+ featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2,
+ permissions: ['administrator'],
+ },
+ },
],
},
],
diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue
new file mode 100644
index 000000000..e931d6bbd
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 000000000..8cf13bcd5
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmDeletePolicyDialog.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js
index 5a020dda6..291031ffd 100755
--- a/app/javascript/dashboard/store/index.js
+++ b/app/javascript/dashboard/store/index.js
@@ -3,6 +3,7 @@ import { createStore } from 'vuex';
import accounts from './modules/accounts';
import agentBots from './modules/agentBots';
import agents from './modules/agents';
+import assignmentPolicies from './modules/assignmentPolicies';
import articles from './modules/helpCenterArticles';
import attributes from './modules/attributes';
import auditlogs from './modules/auditlogs';
@@ -63,6 +64,7 @@ export default createStore({
accounts,
agentBots,
agents,
+ assignmentPolicies,
articles,
attributes,
auditlogs,
diff --git a/app/javascript/dashboard/store/modules/assignmentPolicies.js b/app/javascript/dashboard/store/modules/assignmentPolicies.js
new file mode 100644
index 000000000..80c903c4e
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/assignmentPolicies.js
@@ -0,0 +1,156 @@
+import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
+import types from '../mutation-types';
+import AssignmentPoliciesAPI from '../../api/assignmentPolicies';
+import { throwErrorMessage } from '../utils/api';
+import camelcaseKeys from 'camelcase-keys';
+
+export const state = {
+ records: [],
+ uiFlags: {
+ isFetching: false,
+ isFetchingItem: false,
+ isCreating: false,
+ isUpdating: false,
+ isDeleting: false,
+ },
+ inboxUiFlags: {
+ isFetching: false,
+ },
+};
+
+export const getters = {
+ getAssignmentPolicies(_state) {
+ return _state.records;
+ },
+ getUIFlags(_state) {
+ return _state.uiFlags;
+ },
+ getInboxUiFlags(_state) {
+ return _state.inboxUiFlags;
+ },
+ getAssignmentPolicyById: _state => id => {
+ return _state.records.find(record => record.id === Number(id)) || {};
+ },
+};
+
+export const actions = {
+ get: async function get({ commit }) {
+ commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: true });
+ try {
+ const response = await AssignmentPoliciesAPI.get();
+ commit(types.SET_ASSIGNMENT_POLICIES, camelcaseKeys(response.data));
+ } catch (error) {
+ throwErrorMessage(error);
+ } finally {
+ commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: false });
+ }
+ },
+
+ show: async function show({ commit }, policyId) {
+ commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: true });
+ try {
+ const response = await AssignmentPoliciesAPI.show(policyId);
+ const policy = camelcaseKeys(response.data);
+ commit(types.EDIT_ASSIGNMENT_POLICY, policy);
+ } catch (error) {
+ throwErrorMessage(error);
+ } finally {
+ commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: false });
+ }
+ },
+
+ create: async function create({ commit }, policyObj) {
+ commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true });
+ try {
+ const response = await AssignmentPoliciesAPI.create(policyObj);
+ commit(types.ADD_ASSIGNMENT_POLICY, camelcaseKeys(response.data));
+ return response.data;
+ } catch (error) {
+ throwErrorMessage(error);
+ throw error;
+ } finally {
+ commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: false });
+ }
+ },
+
+ update: async function update({ commit }, { id, ...policyParams }) {
+ commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true });
+ try {
+ const response = await AssignmentPoliciesAPI.update(id, policyParams);
+ commit(types.EDIT_ASSIGNMENT_POLICY, camelcaseKeys(response.data));
+ return response.data;
+ } catch (error) {
+ throwErrorMessage(error);
+ throw error;
+ } finally {
+ commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: false });
+ }
+ },
+
+ delete: async function deletePolicy({ commit }, policyId) {
+ commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: true });
+ try {
+ await AssignmentPoliciesAPI.delete(policyId);
+ commit(types.DELETE_ASSIGNMENT_POLICY, policyId);
+ } catch (error) {
+ throwErrorMessage(error);
+ throw error;
+ } finally {
+ commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: false });
+ }
+ },
+
+ getInboxes: async function getInboxes({ commit }, policyId) {
+ commit(types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isFetching: true });
+ try {
+ const response = await AssignmentPoliciesAPI.getInboxes(policyId);
+ commit(types.SET_ASSIGNMENT_POLICIES_INBOXES, {
+ policyId,
+ inboxes: camelcaseKeys(response.data.inboxes),
+ });
+ } catch (error) {
+ throwErrorMessage(error);
+ throw error;
+ } finally {
+ commit(types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, {
+ isFetching: false,
+ });
+ }
+ },
+};
+
+export const mutations = {
+ [types.SET_ASSIGNMENT_POLICIES_UI_FLAG](_state, data) {
+ _state.uiFlags = {
+ ..._state.uiFlags,
+ ...data,
+ };
+ },
+
+ [types.SET_ASSIGNMENT_POLICIES]: MutationHelpers.set,
+ [types.ADD_ASSIGNMENT_POLICY]: MutationHelpers.create,
+ [types.EDIT_ASSIGNMENT_POLICY]: MutationHelpers.update,
+ [types.DELETE_ASSIGNMENT_POLICY]: MutationHelpers.destroy,
+
+ [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG](_state, data) {
+ _state.inboxUiFlags = {
+ ..._state.inboxUiFlags,
+ ...data,
+ };
+ },
+
+ [types.SET_ASSIGNMENT_POLICIES_INBOXES](_state, { policyId, inboxes }) {
+ const policy = _state.records.find(p => p.id === policyId);
+ if (policy) {
+ policy.inboxes = inboxes;
+ }
+ },
+};
+
+export default {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+};
diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/actions.spec.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/actions.spec.js
new file mode 100644
index 000000000..5358144e8
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/actions.spec.js
@@ -0,0 +1,214 @@
+import axios from 'axios';
+import { actions } from '../../assignmentPolicies';
+import types from '../../../mutation-types';
+import assignmentPoliciesList, { camelCaseFixtures } from './fixtures';
+import camelcaseKeys from 'camelcase-keys';
+
+const commit = vi.fn();
+
+global.axios = axios;
+vi.mock('axios');
+vi.mock('camelcase-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: assignmentPoliciesList });
+ camelcaseKeys.mockReturnValue(camelCaseFixtures);
+
+ await actions.get({ commit });
+
+ expect(camelcaseKeys).toHaveBeenCalledWith(assignmentPoliciesList);
+ expect(commit.mock.calls).toEqual([
+ [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: true }],
+ [types.SET_ASSIGNMENT_POLICIES, camelCaseFixtures],
+ [types.SET_ASSIGNMENT_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_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: true }],
+ [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+ });
+
+ describe('#show', () => {
+ it('sends correct actions if API is success', async () => {
+ const policyData = assignmentPoliciesList[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_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: true }],
+ [types.EDIT_ASSIGNMENT_POLICY, camelCasedPolicy],
+ [types.SET_ASSIGNMENT_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_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: true }],
+ [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: false }],
+ ]);
+ });
+ });
+
+ describe('#create', () => {
+ it('sends correct actions if API is success', async () => {
+ const newPolicy = assignmentPoliciesList[0];
+ const camelCasedData = camelCaseFixtures[0];
+
+ axios.post.mockResolvedValue({ data: newPolicy });
+ camelcaseKeys.mockReturnValue(camelCasedData);
+
+ const result = await actions.create({ commit }, newPolicy);
+
+ expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy);
+ expect(commit.mock.calls).toEqual([
+ [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true }],
+ [types.ADD_ASSIGNMENT_POLICY, camelCasedData],
+ [types.SET_ASSIGNMENT_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_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true }],
+ [types.SET_ASSIGNMENT_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 = {
+ ...assignmentPoliciesList[0],
+ name: 'Updated Policy',
+ };
+ const camelCasedData = {
+ ...camelCaseFixtures[0],
+ name: 'Updated Policy',
+ };
+
+ axios.patch.mockResolvedValue({ data: responseData });
+ camelcaseKeys.mockReturnValue(camelCasedData);
+
+ const result = await actions.update({ commit }, updateParams);
+
+ expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
+ expect(commit.mock.calls).toEqual([
+ [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true }],
+ [types.EDIT_ASSIGNMENT_POLICY, camelCasedData],
+ [types.SET_ASSIGNMENT_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_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true }],
+ [types.SET_ASSIGNMENT_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_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: true }],
+ [types.DELETE_ASSIGNMENT_POLICY, policyId],
+ [types.SET_ASSIGNMENT_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_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: true }],
+ [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: false }],
+ ]);
+ });
+ });
+
+ describe('#getInboxes', () => {
+ it('sends correct actions if API is success', async () => {
+ const policyId = 1;
+ const inboxData = {
+ inboxes: [
+ { id: 1, name: 'Support' },
+ { id: 2, name: 'Sales' },
+ ],
+ };
+ const camelCasedInboxes = [
+ { id: 1, name: 'Support' },
+ { id: 2, name: 'Sales' },
+ ];
+
+ axios.get.mockResolvedValue({ data: inboxData });
+ camelcaseKeys.mockReturnValue(camelCasedInboxes);
+
+ await actions.getInboxes({ commit }, policyId);
+
+ expect(camelcaseKeys).toHaveBeenCalledWith(inboxData.inboxes);
+ expect(commit.mock.calls).toEqual([
+ [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isFetching: true }],
+ [
+ types.SET_ASSIGNMENT_POLICIES_INBOXES,
+ { policyId, inboxes: camelCasedInboxes },
+ ],
+ [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+
+ it('sends correct actions if API fails', async () => {
+ axios.get.mockRejectedValue(new Error('API Error'));
+
+ await expect(actions.getInboxes({ commit }, 1)).rejects.toThrow(Error);
+
+ expect(commit.mock.calls).toEqual([
+ [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isFetching: true }],
+ [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+ });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/fixtures.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/fixtures.js
new file mode 100644
index 000000000..1b5ed25af
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/fixtures.js
@@ -0,0 +1,57 @@
+export default [
+ {
+ id: 1,
+ name: 'Round Robin Policy',
+ description: 'Distributes conversations evenly among agents',
+ assignment_order: 'round_robin',
+ conversation_priority: 'earliest_created',
+ fair_distribution_limit: 100,
+ fair_distribution_window: 3600,
+ enabled: true,
+ assigned_inbox_count: 3,
+ created_at: 1704110400,
+ updated_at: 1704110400,
+ },
+ {
+ id: 2,
+ name: 'Balanced Policy',
+ description: 'Assigns conversations based on agent capacity',
+ assignment_order: 'balanced',
+ conversation_priority: 'longest_waiting',
+ fair_distribution_limit: 50,
+ fair_distribution_window: 1800,
+ enabled: false,
+ assigned_inbox_count: 1,
+ created_at: 1704114000,
+ updated_at: 1704114000,
+ },
+];
+
+export const camelCaseFixtures = [
+ {
+ id: 1,
+ name: 'Round Robin Policy',
+ description: 'Distributes conversations evenly among agents',
+ assignmentOrder: 'round_robin',
+ conversationPriority: 'earliest_created',
+ fairDistributionLimit: 100,
+ fairDistributionWindow: 3600,
+ enabled: true,
+ assignedInboxCount: 3,
+ createdAt: 1704110400,
+ updatedAt: 1704110400,
+ },
+ {
+ id: 2,
+ name: 'Balanced Policy',
+ description: 'Assigns conversations based on agent capacity',
+ assignmentOrder: 'balanced',
+ conversationPriority: 'longest_waiting',
+ fairDistributionLimit: 50,
+ fairDistributionWindow: 1800,
+ enabled: false,
+ assignedInboxCount: 1,
+ createdAt: 1704114000,
+ updatedAt: 1704114000,
+ },
+];
diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/getters.spec.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/getters.spec.js
new file mode 100644
index 000000000..7e0e2041c
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/getters.spec.js
@@ -0,0 +1,49 @@
+import { getters } from '../../assignmentPolicies';
+import assignmentPoliciesList from './fixtures';
+
+describe('#getters', () => {
+ it('getAssignmentPolicies', () => {
+ const state = { records: assignmentPoliciesList };
+ expect(getters.getAssignmentPolicies(state)).toEqual(
+ assignmentPoliciesList
+ );
+ });
+
+ 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('getInboxUiFlags', () => {
+ const state = {
+ inboxUiFlags: {
+ isFetching: false,
+ },
+ };
+ expect(getters.getInboxUiFlags(state)).toEqual({
+ isFetching: false,
+ });
+ });
+
+ it('getAssignmentPolicyById', () => {
+ const state = { records: assignmentPoliciesList };
+ expect(getters.getAssignmentPolicyById(state)(1)).toEqual(
+ assignmentPoliciesList[0]
+ );
+ expect(getters.getAssignmentPolicyById(state)(3)).toEqual({});
+ });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/mutations.spec.js
new file mode 100644
index 000000000..58d5527ca
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/mutations.spec.js
@@ -0,0 +1,267 @@
+import { mutations } from '../../assignmentPolicies';
+import types from '../../../mutation-types';
+import assignmentPoliciesList from './fixtures';
+
+describe('#mutations', () => {
+ describe('#SET_ASSIGNMENT_POLICIES_UI_FLAG', () => {
+ it('sets single ui flag', () => {
+ const state = {
+ uiFlags: {
+ isFetching: false,
+ isCreating: false,
+ },
+ };
+
+ mutations[types.SET_ASSIGNMENT_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_ASSIGNMENT_POLICIES_UI_FLAG](state, {
+ isFetching: true,
+ isCreating: true,
+ });
+
+ expect(state.uiFlags).toEqual({
+ isFetching: true,
+ isCreating: true,
+ isUpdating: false,
+ });
+ });
+ });
+
+ describe('#SET_ASSIGNMENT_POLICIES', () => {
+ it('sets assignment policies records', () => {
+ const state = { records: [] };
+
+ mutations[types.SET_ASSIGNMENT_POLICIES](state, assignmentPoliciesList);
+
+ expect(state.records).toEqual(assignmentPoliciesList);
+ });
+
+ it('replaces existing records', () => {
+ const state = { records: [{ id: 999, name: 'Old Policy' }] };
+
+ mutations[types.SET_ASSIGNMENT_POLICIES](state, assignmentPoliciesList);
+
+ expect(state.records).toEqual(assignmentPoliciesList);
+ });
+ });
+
+ describe('#ADD_ASSIGNMENT_POLICY', () => {
+ it('adds new policy to empty records', () => {
+ const state = { records: [] };
+
+ mutations[types.ADD_ASSIGNMENT_POLICY](state, assignmentPoliciesList[0]);
+
+ expect(state.records).toEqual([assignmentPoliciesList[0]]);
+ });
+
+ it('adds new policy to existing records', () => {
+ const state = { records: [assignmentPoliciesList[0]] };
+
+ mutations[types.ADD_ASSIGNMENT_POLICY](state, assignmentPoliciesList[1]);
+
+ expect(state.records).toEqual([
+ assignmentPoliciesList[0],
+ assignmentPoliciesList[1],
+ ]);
+ });
+ });
+
+ describe('#EDIT_ASSIGNMENT_POLICY', () => {
+ it('updates existing policy by id', () => {
+ const state = {
+ records: [
+ { ...assignmentPoliciesList[0] },
+ { ...assignmentPoliciesList[1] },
+ ],
+ };
+
+ const updatedPolicy = {
+ ...assignmentPoliciesList[0],
+ name: 'Updated Policy Name',
+ description: 'Updated Description',
+ };
+
+ mutations[types.EDIT_ASSIGNMENT_POLICY](state, updatedPolicy);
+
+ expect(state.records[0]).toEqual(updatedPolicy);
+ expect(state.records[1]).toEqual(assignmentPoliciesList[1]);
+ });
+
+ it('updates policy with camelCase properties', () => {
+ const camelCasePolicy = {
+ id: 1,
+ name: 'Camel Case Policy',
+ assignmentOrder: 'round_robin',
+ conversationPriority: 'earliest_created',
+ };
+
+ const state = {
+ records: [camelCasePolicy],
+ };
+
+ const updatedPolicy = {
+ ...camelCasePolicy,
+ name: 'Updated Camel Case',
+ assignmentOrder: 'balanced',
+ };
+
+ mutations[types.EDIT_ASSIGNMENT_POLICY](state, updatedPolicy);
+
+ expect(state.records[0]).toEqual(updatedPolicy);
+ });
+
+ it('does nothing if policy id not found', () => {
+ const state = {
+ records: [assignmentPoliciesList[0]],
+ };
+
+ const nonExistentPolicy = {
+ id: 999,
+ name: 'Non-existent',
+ };
+
+ const originalRecords = [...state.records];
+ mutations[types.EDIT_ASSIGNMENT_POLICY](state, nonExistentPolicy);
+
+ expect(state.records).toEqual(originalRecords);
+ });
+ });
+
+ describe('#DELETE_ASSIGNMENT_POLICY', () => {
+ it('deletes policy by id', () => {
+ const state = {
+ records: [assignmentPoliciesList[0], assignmentPoliciesList[1]],
+ };
+
+ mutations[types.DELETE_ASSIGNMENT_POLICY](state, 1);
+
+ expect(state.records).toEqual([assignmentPoliciesList[1]]);
+ });
+
+ it('does nothing if id not found', () => {
+ const state = {
+ records: [assignmentPoliciesList[0]],
+ };
+
+ mutations[types.DELETE_ASSIGNMENT_POLICY](state, 999);
+
+ expect(state.records).toEqual([assignmentPoliciesList[0]]);
+ });
+
+ it('handles empty records', () => {
+ const state = { records: [] };
+
+ mutations[types.DELETE_ASSIGNMENT_POLICY](state, 1);
+
+ expect(state.records).toEqual([]);
+ });
+ });
+
+ describe('#SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG', () => {
+ it('sets inbox ui flags', () => {
+ const state = {
+ inboxUiFlags: {
+ isFetching: false,
+ },
+ };
+
+ mutations[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG](state, {
+ isFetching: true,
+ });
+
+ expect(state.inboxUiFlags).toEqual({
+ isFetching: true,
+ });
+ });
+
+ it('merges with existing flags', () => {
+ const state = {
+ inboxUiFlags: {
+ isFetching: false,
+ isLoading: true,
+ },
+ };
+
+ mutations[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG](state, {
+ isFetching: true,
+ });
+
+ expect(state.inboxUiFlags).toEqual({
+ isFetching: true,
+ isLoading: true,
+ });
+ });
+ });
+
+ describe('#SET_ASSIGNMENT_POLICIES_INBOXES', () => {
+ it('sets inboxes for existing policy', () => {
+ const mockInboxes = [
+ { id: 1, name: 'Support Inbox' },
+ { id: 2, name: 'Sales Inbox' },
+ ];
+
+ const state = {
+ records: [
+ { id: 1, name: 'Policy 1', inboxes: [] },
+ { id: 2, name: 'Policy 2', inboxes: [] },
+ ],
+ };
+
+ mutations[types.SET_ASSIGNMENT_POLICIES_INBOXES](state, {
+ policyId: 1,
+ inboxes: mockInboxes,
+ });
+
+ expect(state.records[0].inboxes).toEqual(mockInboxes);
+ expect(state.records[1].inboxes).toEqual([]);
+ });
+
+ it('replaces existing inboxes', () => {
+ const oldInboxes = [{ id: 99, name: 'Old Inbox' }];
+ const newInboxes = [{ id: 1, name: 'New Inbox' }];
+
+ const state = {
+ records: [{ id: 1, name: 'Policy 1', inboxes: oldInboxes }],
+ };
+
+ mutations[types.SET_ASSIGNMENT_POLICIES_INBOXES](state, {
+ policyId: 1,
+ inboxes: newInboxes,
+ });
+
+ expect(state.records[0].inboxes).toEqual(newInboxes);
+ });
+
+ it('does nothing if policy not found', () => {
+ const state = {
+ records: [{ id: 1, name: 'Policy 1', inboxes: [] }],
+ };
+
+ const originalState = JSON.parse(JSON.stringify(state));
+
+ mutations[types.SET_ASSIGNMENT_POLICIES_INBOXES](state, {
+ policyId: 999,
+ inboxes: [{ 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 a63fec2d1..a6fbefa17 100644
--- a/app/javascript/dashboard/store/mutation-types.js
+++ b/app/javascript/dashboard/store/mutation-types.js
@@ -348,4 +348,14 @@ export default {
SET_TEAM_CONVERSATION_METRIC: 'SET_TEAM_CONVERSATION_METRIC',
TOGGLE_TEAM_CONVERSATION_METRIC_LOADING:
'TOGGLE_TEAM_CONVERSATION_METRIC_LOADING',
+
+ // Assignment Policies
+ SET_ASSIGNMENT_POLICIES_UI_FLAG: 'SET_ASSIGNMENT_POLICIES_UI_FLAG',
+ SET_ASSIGNMENT_POLICIES: 'SET_ASSIGNMENT_POLICIES',
+ 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',
};
diff --git a/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder
index b48307a94..cf09a2949 100644
--- a/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder
+++ b/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder
@@ -6,5 +6,6 @@ json.conversation_priority assignment_policy.conversation_priority
json.fair_distribution_limit assignment_policy.fair_distribution_limit
json.fair_distribution_window assignment_policy.fair_distribution_window
json.enabled assignment_policy.enabled
+json.assigned_inbox_count assignment_policy.inboxes.count
json.created_at assignment_policy.created_at.to_i
json.updated_at assignment_policy.updated_at.to_i