mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	feat: Agent assignment policy index page with CRUD actions (#12373)
# Pull Request Template ## Description This PR incudes new Agent assignment policy index page with CRUD actions. Fixes https://linear.app/chatwoot/issue/CW-5570/feat-assignment-policy-index-page-with-actions ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Loom https://www.loom.com/share/17ab5ceca4854f179628a3b53f347e5a?sid=cb64e881-57fd-4ae1-921b-7648653cca33 ### Screenshots **Light mode** <img width="1428" height="888" alt="image" src="https://github.com/user-attachments/assets/fdbb83e9-1f4f-4432-9e8a-4a8f1b810d31" /> **Dark mode** <img width="1428" height="888" alt="image" src="https://github.com/user-attachments/assets/f1fb38b9-1150-482c-ba62-3fe63ee1c7d4" /> <img width="726" height="495" alt="image" src="https://github.com/user-attachments/assets/90a6ad55-9ab6-4adb-93a7-2327f5f09c79" /> ## 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 - [ ] 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 <muhsinkeramam@gmail.com>
This commit is contained in:
		
							
								
								
									
										36
									
								
								app/javascript/dashboard/api/assignmentPolicies.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								app/javascript/dashboard/api/assignmentPolicies.js
									
									
									
									
									
										Normal file
									
								
							@@ -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();
 | 
			
		||||
@@ -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'
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,104 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import AssignmentPolicyCard from './AssignmentPolicyCard.vue';
 | 
			
		||||
 | 
			
		||||
const mockInboxes = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    name: 'Website Support',
 | 
			
		||||
    channel_type: 'Channel::WebWidget',
 | 
			
		||||
    inbox_type: 'Website',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 2,
 | 
			
		||||
    name: 'Email Support',
 | 
			
		||||
    channel_type: 'Channel::Email',
 | 
			
		||||
    inbox_type: 'Email',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 3,
 | 
			
		||||
    name: 'WhatsApp Business',
 | 
			
		||||
    channel_type: 'Channel::Whatsapp',
 | 
			
		||||
    inbox_type: 'WhatsApp',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 4,
 | 
			
		||||
    name: 'Facebook Messenger',
 | 
			
		||||
    channel_type: 'Channel::FacebookPage',
 | 
			
		||||
    inbox_type: 'Messenger',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const withCount = policy => ({
 | 
			
		||||
  ...policy,
 | 
			
		||||
  assignedInboxCount: policy.inboxes.length,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const policyA = withCount({
 | 
			
		||||
  id: 1,
 | 
			
		||||
  name: 'Website & Email',
 | 
			
		||||
  description: 'Distributes conversations evenly among available agents',
 | 
			
		||||
  assignmentOrder: 'round_robin',
 | 
			
		||||
  conversationPriority: 'high',
 | 
			
		||||
  enabled: true,
 | 
			
		||||
  inboxes: [mockInboxes[0], mockInboxes[1]],
 | 
			
		||||
  isFetchingInboxes: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const policyB = withCount({
 | 
			
		||||
  id: 2,
 | 
			
		||||
  name: 'WhatsApp & Messenger',
 | 
			
		||||
  description: 'Assigns based on capacity and workload',
 | 
			
		||||
  assignmentOrder: 'capacity_based',
 | 
			
		||||
  conversationPriority: 'medium',
 | 
			
		||||
  enabled: true,
 | 
			
		||||
  inboxes: [mockInboxes[2], mockInboxes[3]],
 | 
			
		||||
  isFetchingInboxes: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emptyPolicy = withCount({
 | 
			
		||||
  id: 3,
 | 
			
		||||
  name: 'No Inboxes Yet',
 | 
			
		||||
  description: 'Policy with no assigned inboxes',
 | 
			
		||||
  assignmentOrder: 'manual',
 | 
			
		||||
  conversationPriority: 'low',
 | 
			
		||||
  enabled: false,
 | 
			
		||||
  inboxes: [],
 | 
			
		||||
  isFetchingInboxes: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const onEdit = id => console.log('Edit policy:', id);
 | 
			
		||||
const onDelete = id => console.log('Delete policy:', id);
 | 
			
		||||
const onFetch = () => console.log('Fetch inboxes');
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Story
 | 
			
		||||
    title="Components/AgentManagementPolicy/AssignmentPolicyCard"
 | 
			
		||||
    :layout="{ type: 'grid', width: '1200px' }"
 | 
			
		||||
  >
 | 
			
		||||
    <Variant title="Three Cards (Two with inboxes, One empty)">
 | 
			
		||||
      <div class="p-4 bg-n-background">
 | 
			
		||||
        <div class="grid grid-cols-1 gap-4">
 | 
			
		||||
          <AssignmentPolicyCard
 | 
			
		||||
            v-bind="policyA"
 | 
			
		||||
            @edit="onEdit"
 | 
			
		||||
            @delete="onDelete"
 | 
			
		||||
            @fetch-inboxes="onFetch"
 | 
			
		||||
          />
 | 
			
		||||
          <AssignmentPolicyCard
 | 
			
		||||
            v-bind="policyB"
 | 
			
		||||
            @edit="onEdit"
 | 
			
		||||
            @delete="onDelete"
 | 
			
		||||
            @fetch-inboxes="onFetch"
 | 
			
		||||
          />
 | 
			
		||||
          <AssignmentPolicyCard
 | 
			
		||||
            v-bind="emptyPolicy"
 | 
			
		||||
            @edit="onEdit"
 | 
			
		||||
            @delete="onDelete"
 | 
			
		||||
            @fetch-inboxes="onFetch"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </Variant>
 | 
			
		||||
  </Story>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -0,0 +1,133 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useI18n } from 'vue-i18n';
 | 
			
		||||
import { getInboxIconByType } from 'dashboard/helper/inbox';
 | 
			
		||||
import { formatToTitleCase } from 'dashboard/helper/commons';
 | 
			
		||||
 | 
			
		||||
import Button from 'dashboard/components-next/button/Button.vue';
 | 
			
		||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
 | 
			
		||||
import CardPopover from '../components/CardPopover.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  id: { type: Number, required: true },
 | 
			
		||||
  name: { type: String, default: '' },
 | 
			
		||||
  description: { type: String, default: '' },
 | 
			
		||||
  assignmentOrder: { type: String, default: '' },
 | 
			
		||||
  conversationPriority: { type: String, default: '' },
 | 
			
		||||
  assignedInboxCount: { type: Number, default: 0 },
 | 
			
		||||
  enabled: { type: Boolean, default: false },
 | 
			
		||||
  inboxes: { type: Array, default: () => [] },
 | 
			
		||||
  isFetchingInboxes: { type: Boolean, default: false },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['edit', 'delete', 'fetchInboxes']);
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
const inboxes = computed(() => {
 | 
			
		||||
  return props.inboxes.map(inbox => {
 | 
			
		||||
    return {
 | 
			
		||||
      name: inbox.name,
 | 
			
		||||
      id: inbox.id,
 | 
			
		||||
      icon: getInboxIconByType(inbox.channelType, inbox.medium, 'line'),
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const order = computed(() => {
 | 
			
		||||
  return formatToTitleCase(props.assignmentOrder);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const priority = computed(() => {
 | 
			
		||||
  return formatToTitleCase(props.conversationPriority);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const handleEdit = () => {
 | 
			
		||||
  emit('edit', props.id);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleDelete = () => {
 | 
			
		||||
  emit('delete', props.id);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleFetchInboxes = () => {
 | 
			
		||||
  if (props.inboxes?.length > 0) return;
 | 
			
		||||
  emit('fetchInboxes', props.id);
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <CardLayout class="[&>div]:px-5">
 | 
			
		||||
    <div class="flex flex-col gap-2 relative justify-between w-full">
 | 
			
		||||
      <div class="flex items-center gap-3 justify-between w-full">
 | 
			
		||||
        <div class="flex items-center gap-3">
 | 
			
		||||
          <h3 class="text-base font-medium text-n-slate-12 line-clamp-1">
 | 
			
		||||
            {{ name }}
 | 
			
		||||
          </h3>
 | 
			
		||||
          <div class="flex items-center gap-2">
 | 
			
		||||
            <div class="flex items-center rounded-md bg-n-alpha-2 h-6 px-2">
 | 
			
		||||
              <span
 | 
			
		||||
                class="text-xs"
 | 
			
		||||
                :class="enabled ? 'text-n-teal-11' : 'text-n-slate-12'"
 | 
			
		||||
              >
 | 
			
		||||
                {{
 | 
			
		||||
                  enabled
 | 
			
		||||
                    ? t(
 | 
			
		||||
                        'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.ACTIVE'
 | 
			
		||||
                      )
 | 
			
		||||
                    : t(
 | 
			
		||||
                        'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.INACTIVE'
 | 
			
		||||
                      )
 | 
			
		||||
                }}
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <CardPopover
 | 
			
		||||
              :title="
 | 
			
		||||
                t(
 | 
			
		||||
                  'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.POPOVER'
 | 
			
		||||
                )
 | 
			
		||||
              "
 | 
			
		||||
              icon="i-lucide-inbox"
 | 
			
		||||
              :count="assignedInboxCount"
 | 
			
		||||
              :items="inboxes"
 | 
			
		||||
              :is-fetching="isFetchingInboxes"
 | 
			
		||||
              @fetch="handleFetchInboxes"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex items-center gap-2">
 | 
			
		||||
          <Button
 | 
			
		||||
            :label="
 | 
			
		||||
              t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.EDIT')
 | 
			
		||||
            "
 | 
			
		||||
            sm
 | 
			
		||||
            slate
 | 
			
		||||
            link
 | 
			
		||||
            class="px-2"
 | 
			
		||||
            @click="handleEdit"
 | 
			
		||||
          />
 | 
			
		||||
          <div v-if="order" class="w-px h-2.5 bg-n-slate-5" />
 | 
			
		||||
          <Button icon="i-lucide-trash" sm slate ghost @click="handleDelete" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <p class="text-n-slate-11 text-sm line-clamp-1 mb-0 py-1">
 | 
			
		||||
        {{ description }}
 | 
			
		||||
      </p>
 | 
			
		||||
      <div class="flex items-center gap-3 py-1.5">
 | 
			
		||||
        <span v-if="order" class="text-n-slate-11 text-sm">
 | 
			
		||||
          {{
 | 
			
		||||
            `${t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.ORDER')}:`
 | 
			
		||||
          }}
 | 
			
		||||
          <span class="text-n-slate-12">{{ order }}</span>
 | 
			
		||||
        </span>
 | 
			
		||||
        <div v-if="order" class="w-px h-3 bg-n-strong" />
 | 
			
		||||
        <span v-if="priority" class="text-n-slate-11 text-sm">
 | 
			
		||||
          {{
 | 
			
		||||
            `${t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.PRIORITY')}:`
 | 
			
		||||
          }}
 | 
			
		||||
          <span class="text-n-slate-12">{{ priority }}</span>
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </CardLayout>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -0,0 +1,103 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { useToggle } from '@vueuse/core';
 | 
			
		||||
import { vOnClickOutside } from '@vueuse/components';
 | 
			
		||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
 | 
			
		||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
 | 
			
		||||
 | 
			
		||||
defineProps({
 | 
			
		||||
  count: {
 | 
			
		||||
    type: Number,
 | 
			
		||||
    default: 0,
 | 
			
		||||
  },
 | 
			
		||||
  title: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: '',
 | 
			
		||||
  },
 | 
			
		||||
  icon: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: '',
 | 
			
		||||
  },
 | 
			
		||||
  items: {
 | 
			
		||||
    type: Array,
 | 
			
		||||
    default: () => [],
 | 
			
		||||
  },
 | 
			
		||||
  isFetching: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['fetch']);
 | 
			
		||||
 | 
			
		||||
const [showPopover, togglePopover] = useToggle();
 | 
			
		||||
 | 
			
		||||
const handleButtonClick = () => {
 | 
			
		||||
  emit('fetch');
 | 
			
		||||
  togglePopover(!showPopover.value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleClickOutside = () => {
 | 
			
		||||
  if (showPopover.value) {
 | 
			
		||||
    togglePopover(false);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    v-on-click-outside="handleClickOutside"
 | 
			
		||||
    class="relative flex items-center group"
 | 
			
		||||
  >
 | 
			
		||||
    <button
 | 
			
		||||
      v-if="count"
 | 
			
		||||
      class="h-6 px-2 rounded-md bg-n-alpha-2 gap-1.5 flex items-center"
 | 
			
		||||
      @click="handleButtonClick()"
 | 
			
		||||
    >
 | 
			
		||||
      <Icon icon="i-lucide-inbox" class="size-3.5 text-n-slate-12" />
 | 
			
		||||
      <span class="text-n-slate-12 text-sm">
 | 
			
		||||
        {{ count }}
 | 
			
		||||
      </span>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div
 | 
			
		||||
      v-if="showPopover"
 | 
			
		||||
      class="top-full mt-1 ltr:left-0 rtl:right-0 z-50 flex flex-col items-start absolute bg-n-alpha-3 backdrop-blur-[50px] border-0 gap-4 outline outline-1 outline-n-weak p-3 rounded-xl max-w-96 min-w-80 max-h-[20rem] overflow-y-auto"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="flex items-center gap-2.5 pb-2">
 | 
			
		||||
        <Icon :icon="icon" class="size-3.5" />
 | 
			
		||||
        <span class="text-sm text-n-slate-12 font-medium">{{ title }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        v-if="isFetching"
 | 
			
		||||
        class="flex items-center justify-center py-3 w-full text-n-slate-11"
 | 
			
		||||
      >
 | 
			
		||||
        <Spinner />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div v-else class="flex flex-col gap-4">
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="item in items"
 | 
			
		||||
          :key="item.id"
 | 
			
		||||
          class="flex items-center gap-2 min-w-0 w-full"
 | 
			
		||||
        >
 | 
			
		||||
          <Icon
 | 
			
		||||
            v-if="item.icon"
 | 
			
		||||
            :icon="item.icon"
 | 
			
		||||
            class="size-4 text-n-slate-12 flex-shrink-0"
 | 
			
		||||
          />
 | 
			
		||||
          <div class="flex items-center gap-1 min-w-0 flex-1">
 | 
			
		||||
            <span
 | 
			
		||||
              :title="item.name"
 | 
			
		||||
              class="text-sm text-n-slate-12 truncate min-w-0"
 | 
			
		||||
            >
 | 
			
		||||
              {{ item.name }}
 | 
			
		||||
            </span>
 | 
			
		||||
            <span v-if="item.id" class="text-sm text-n-slate-11 flex-shrink-0">
 | 
			
		||||
              {{ `#${item.id}` }}
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import CardPopover from '../CardPopover.vue';
 | 
			
		||||
 | 
			
		||||
const mockItems = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    name: 'Website Support',
 | 
			
		||||
    icon: 'i-lucide-globe',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 2,
 | 
			
		||||
    name: 'Email Support',
 | 
			
		||||
    icon: 'i-lucide-mail',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 3,
 | 
			
		||||
    name: 'WhatsApp Business',
 | 
			
		||||
    icon: 'i-lucide-message-circle',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 4,
 | 
			
		||||
    name: 'Facebook Messenger',
 | 
			
		||||
    icon: 'i-lucide-facebook',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Story
 | 
			
		||||
    title="Components/AgentManagementPolicy/CardPopover"
 | 
			
		||||
    :layout="{ type: 'grid', width: '800px' }"
 | 
			
		||||
  >
 | 
			
		||||
    <Variant title="Basic Usage">
 | 
			
		||||
      <div class="p-8 bg-n-background flex gap-4 h-96 items-start">
 | 
			
		||||
        <CardPopover
 | 
			
		||||
          :count="3"
 | 
			
		||||
          title="Added Inboxes"
 | 
			
		||||
          icon="i-lucide-inbox"
 | 
			
		||||
          :items="mockItems.slice(0, 3)"
 | 
			
		||||
          @fetch="() => console.log('Fetch triggered')"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </Variant>
 | 
			
		||||
  </Story>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -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() || ''
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ defineProps({
 | 
			
		||||
  <div class="flex flex-col w-full h-full gap-8 font-inter">
 | 
			
		||||
    <slot name="header" />
 | 
			
		||||
    <!-- Added to render any templates that should be rendered before body -->
 | 
			
		||||
    <div>
 | 
			
		||||
    <main>
 | 
			
		||||
      <slot name="preBody" />
 | 
			
		||||
      <slot v-if="isLoading" name="loading">
 | 
			
		||||
        <woot-loading-state :message="loadingMessage" />
 | 
			
		||||
@@ -37,6 +37,6 @@ defineProps({
 | 
			
		||||
      <slot v-else name="body" />
 | 
			
		||||
      <!-- Do not delete the slot below. It is required to render anything that is not defined in the above slots. -->
 | 
			
		||||
      <slot />
 | 
			
		||||
    </div>
 | 
			
		||||
    </main>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -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: [
 | 
			
		||||
 
 | 
			
		||||
@@ -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'],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,118 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { computed, onMounted, 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 Button from 'dashboard/components-next/button/Button.vue';
 | 
			
		||||
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
 | 
			
		||||
import AssignmentPolicyCard from 'dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.vue';
 | 
			
		||||
import ConfirmDeletePolicyDialog from './components/ConfirmDeletePolicyDialog.vue';
 | 
			
		||||
 | 
			
		||||
const store = useStore();
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
 | 
			
		||||
const agentAssignmentsPolicies = useMapGetter(
 | 
			
		||||
  'assignmentPolicies/getAssignmentPolicies'
 | 
			
		||||
);
 | 
			
		||||
const uiFlags = useMapGetter('assignmentPolicies/getUIFlags');
 | 
			
		||||
const inboxUiFlags = useMapGetter('assignmentPolicies/getInboxUiFlags');
 | 
			
		||||
 | 
			
		||||
const confirmDeletePolicyDialogRef = ref(null);
 | 
			
		||||
 | 
			
		||||
const breadcrumbItems = computed(() => {
 | 
			
		||||
  const items = [
 | 
			
		||||
    {
 | 
			
		||||
      label: t('ASSIGNMENT_POLICY.INDEX.HEADER.TITLE'),
 | 
			
		||||
      routeName: 'assignment_policy_index',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.HEADER.TITLE'),
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
  return items;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const handleBreadcrumbClick = item => {
 | 
			
		||||
  router.push({
 | 
			
		||||
    name: item.routeName,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onClickCreatePolicy = () => {
 | 
			
		||||
  router.push({
 | 
			
		||||
    name: 'assignment_policy_create',
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleFetchInboxes = id => {
 | 
			
		||||
  if (inboxUiFlags.value.isFetching) return;
 | 
			
		||||
  store.dispatch('assignmentPolicies/getInboxes', id);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleDelete = id => {
 | 
			
		||||
  confirmDeletePolicyDialogRef.value.openDialog(id);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleDeletePolicy = async policyId => {
 | 
			
		||||
  try {
 | 
			
		||||
    await store.dispatch('assignmentPolicies/delete', policyId);
 | 
			
		||||
    useAlert(
 | 
			
		||||
      t(
 | 
			
		||||
        'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.DELETE_POLICY.SUCCESS_MESSAGE'
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
    confirmDeletePolicyDialogRef.value.closeDialog();
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    useAlert(
 | 
			
		||||
      t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.DELETE_POLICY.ERROR_MESSAGE')
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  store.dispatch('assignmentPolicies/get');
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <SettingsLayout
 | 
			
		||||
    :is-loading="uiFlags.isFetching"
 | 
			
		||||
    :no-records-found="agentAssignmentsPolicies.length === 0"
 | 
			
		||||
    :no-records-message="
 | 
			
		||||
      $t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.NO_RECORDS_FOUND')
 | 
			
		||||
    "
 | 
			
		||||
  >
 | 
			
		||||
    <template #header>
 | 
			
		||||
      <div class="flex items-center gap-2 w-full justify-between">
 | 
			
		||||
        <Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
 | 
			
		||||
        <Button icon="i-lucide-plus" md @click="onClickCreatePolicy">
 | 
			
		||||
          {{
 | 
			
		||||
            $t(
 | 
			
		||||
              'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.HEADER.CREATE_POLICY'
 | 
			
		||||
            )
 | 
			
		||||
          }}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template #body>
 | 
			
		||||
      <div class="flex flex-col gap-4 pt-8">
 | 
			
		||||
        <AssignmentPolicyCard
 | 
			
		||||
          v-for="policy in agentAssignmentsPolicies"
 | 
			
		||||
          :key="policy.id"
 | 
			
		||||
          v-bind="policy"
 | 
			
		||||
          :is-fetching-inboxes="inboxUiFlags.isFetching"
 | 
			
		||||
          @fetch-inboxes="handleFetchInboxes"
 | 
			
		||||
          @delete="handleDelete"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <ConfirmDeletePolicyDialog
 | 
			
		||||
      ref="confirmDeletePolicyDialogRef"
 | 
			
		||||
      @delete="handleDeletePolicy"
 | 
			
		||||
    />
 | 
			
		||||
  </SettingsLayout>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -0,0 +1,50 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import { useI18n } from 'vue-i18n';
 | 
			
		||||
 | 
			
		||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['delete']);
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
const dialogRef = ref(null);
 | 
			
		||||
const currentPolicyId = ref(null);
 | 
			
		||||
 | 
			
		||||
const openDialog = policyId => {
 | 
			
		||||
  currentPolicyId.value = policyId;
 | 
			
		||||
  dialogRef.value.open();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeDialog = () => {
 | 
			
		||||
  dialogRef.value.close();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleDialogConfirm = () => {
 | 
			
		||||
  emit('delete', currentPolicyId.value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ openDialog, closeDialog });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Dialog
 | 
			
		||||
    ref="dialogRef"
 | 
			
		||||
    type="alert"
 | 
			
		||||
    :title="t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.DELETE_POLICY.TITLE')"
 | 
			
		||||
    :description="
 | 
			
		||||
      t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.DELETE_POLICY.DESCRIPTION')
 | 
			
		||||
    "
 | 
			
		||||
    :confirm-button-label="
 | 
			
		||||
      t(
 | 
			
		||||
        'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.DELETE_POLICY.CONFIRM_BUTTON_LABEL'
 | 
			
		||||
      )
 | 
			
		||||
    "
 | 
			
		||||
    :cancel-button-label="
 | 
			
		||||
      t(
 | 
			
		||||
        'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.DELETE_POLICY.CANCEL_BUTTON_LABEL'
 | 
			
		||||
      )
 | 
			
		||||
    "
 | 
			
		||||
    @confirm="handleDialogConfirm"
 | 
			
		||||
  />
 | 
			
		||||
</template>
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										156
									
								
								app/javascript/dashboard/store/modules/assignmentPolicies.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								app/javascript/dashboard/store/modules/assignmentPolicies.js
									
									
									
									
									
										Normal file
									
								
							@@ -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,
 | 
			
		||||
};
 | 
			
		||||
@@ -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 }],
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -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,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
@@ -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({});
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -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);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -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',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user