feat: Agent assignment policy Create/Edit pages (#12400)
This commit is contained in:
parent
aba4e8bc53
commit
257df30589
@ -0,0 +1,119 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
|
import { picoSearch } from '@scmmishra/pico-search';
|
||||||
|
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
searchPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['add']);
|
||||||
|
|
||||||
|
const [showPopover, togglePopover] = useToggle();
|
||||||
|
|
||||||
|
const searchValue = ref('');
|
||||||
|
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
if (!searchValue.value) return props.items;
|
||||||
|
const query = searchValue.value.toLowerCase();
|
||||||
|
|
||||||
|
return picoSearch(props.items, query, ['name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdd = inbox => {
|
||||||
|
emit('add', inbox);
|
||||||
|
togglePopover(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickOutside = () => {
|
||||||
|
if (showPopover.value) {
|
||||||
|
togglePopover(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-on-click-outside="handleClickOutside"
|
||||||
|
class="relative flex items-center group"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
slate
|
||||||
|
type="button"
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
sm
|
||||||
|
:label="label"
|
||||||
|
@click="togglePopover(!showPopover)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showPopover"
|
||||||
|
class="top-full mt-2 ltr:right-0 rtl:left-0 xl:ltr:left-0 xl: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 rounded-xl max-w-96 min-w-80 max-h-[20rem] overflow-y-auto py-2"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col divide-y divide-n-slate-4 w-full">
|
||||||
|
<Input
|
||||||
|
v-model="searchValue"
|
||||||
|
:placeholder="searchPlaceholder"
|
||||||
|
custom-input-class="bg-transparent !outline-none w-full ltr:!pl-10 rtl:!pr-10 h-10"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<Icon
|
||||||
|
icon="i-lucide-search"
|
||||||
|
class="absolute -translate-y-1/2 text-n-slate-11 size-4 top-1/2 ltr:left-3 rtl:right-3"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Input>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="item in filteredItems"
|
||||||
|
:key="item.id"
|
||||||
|
class="flex items-start gap-3 min-w-0 w-full py-4 px-3 hover:bg-n-alpha-2 cursor-pointer"
|
||||||
|
@click="handleAdd(item)"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-if="item.icon"
|
||||||
|
:icon="item.icon"
|
||||||
|
class="size-4 text-n-slate-12 flex-shrink-0 mt-0.5"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col items-start gap-2 min-w-0">
|
||||||
|
<div class="flex items-center gap-1 min-w-0">
|
||||||
|
<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-xs text-n-slate-11 flex-shrink-0"
|
||||||
|
>
|
||||||
|
{{ `#${item.id}` }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="item.email || item.phoneNumber"
|
||||||
|
class="text-sm text-n-slate-11 truncate min-w-0"
|
||||||
|
>
|
||||||
|
{{ item.email || item.phoneNumber }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, watch } from 'vue';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { required, minLength } from '@vuelidate/validators';
|
||||||
|
|
||||||
|
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import Switch from 'dashboard/components-next/switch/Switch.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
nameLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
namePlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
descriptionLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
descriptionPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
statusLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
statusPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['validationChange']);
|
||||||
|
|
||||||
|
const policyName = defineModel('policyName', {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const description = defineModel('description', {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const enabled = defineModel('enabled', {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const validationRules = {
|
||||||
|
policyName: { required, minLength: minLength(1) },
|
||||||
|
description: { required, minLength: minLength(1) },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(validationRules, { policyName, description });
|
||||||
|
|
||||||
|
const isValid = computed(() => !v$.value.$invalid);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
isValid,
|
||||||
|
() => {
|
||||||
|
emit('validationChange', {
|
||||||
|
isValid: isValid.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4 pb-4">
|
||||||
|
<!-- Policy Name Field -->
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<WithLabel
|
||||||
|
:label="nameLabel"
|
||||||
|
name="policyName"
|
||||||
|
class="flex items-center w-full [&>label]:min-w-[120px]"
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<Input
|
||||||
|
v-model="policyName"
|
||||||
|
type="text"
|
||||||
|
:placeholder="namePlaceholder"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</WithLabel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description Field -->
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<WithLabel
|
||||||
|
:label="descriptionLabel"
|
||||||
|
name="description"
|
||||||
|
class="flex items-center w-full [&>label]:min-w-[120px]"
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<Input
|
||||||
|
v-model="description"
|
||||||
|
type="text"
|
||||||
|
:placeholder="descriptionPlaceholder"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</WithLabel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Field -->
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<WithLabel
|
||||||
|
:label="statusLabel"
|
||||||
|
name="enabled"
|
||||||
|
class="flex items-center w-full [&>label]:min-w-[120px]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Switch v-model="enabled" />
|
||||||
|
<span class="text-sm text-n-slate-11">
|
||||||
|
{{ statusPlaceholder }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</WithLabel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
<script setup>
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
isFetching: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
emptyStateMessage: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['delete']);
|
||||||
|
|
||||||
|
const handleDelete = itemId => {
|
||||||
|
emit('delete', itemId);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isFetching"
|
||||||
|
class="flex items-center justify-center py-3 w-full text-n-slate-11"
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-else-if="items.length === 0 && emptyStateMessage"
|
||||||
|
class="flex items-center justify-center pt-4 pb-8 w-full text-sm text-n-slate-11"
|
||||||
|
>
|
||||||
|
{{ emptyStateMessage }}
|
||||||
|
</span>
|
||||||
|
<div v-else class="flex flex-col divide-y divide-n-weak">
|
||||||
|
<div
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
class="grid grid-cols-4 items-center gap-3 min-w-0 w-full justify-between h-[3.25rem] ltr:pr-2 rtl:pl-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 col-span-2">
|
||||||
|
<Icon
|
||||||
|
v-if="item.icon"
|
||||||
|
:icon="item.icon"
|
||||||
|
class="size-4 text-n-slate-12 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="text-sm text-n-slate-12 truncate min-w-0">
|
||||||
|
{{ item.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start gap-2 col-span-1">
|
||||||
|
<span
|
||||||
|
:title="item.email || item.phoneNumber"
|
||||||
|
class="text-sm text-n-slate-12 truncate min-w-0"
|
||||||
|
>
|
||||||
|
{{ item.email || item.phoneNumber }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-1 justify-end flex items-center">
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
slate
|
||||||
|
ghost
|
||||||
|
sm
|
||||||
|
type="button"
|
||||||
|
@click="handleDelete(item.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
|
||||||
|
import { DURATION_UNITS } from 'dashboard/components-next/input/constants';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const fairDistributionLimit = defineModel('fairDistributionLimit', {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
set(value) {
|
||||||
|
return Number(value) || 0;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fairDistributionWindow = defineModel('fairDistributionWindow', {
|
||||||
|
type: Number,
|
||||||
|
default: 3600,
|
||||||
|
set(value) {
|
||||||
|
return Number(value) || 0;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const windowUnit = ref(DURATION_UNITS.MINUTES);
|
||||||
|
|
||||||
|
const detectUnit = minutes => {
|
||||||
|
const m = Number(minutes) || 0;
|
||||||
|
if (m === 0) return DURATION_UNITS.MINUTES;
|
||||||
|
if (m % (24 * 60) === 0) return DURATION_UNITS.DAYS;
|
||||||
|
if (m % 60 === 0) return DURATION_UNITS.HOURS;
|
||||||
|
return DURATION_UNITS.MINUTES;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
windowUnit.value = detectUnit(fairDistributionWindow.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-start xl:items-center flex-col md:flex-row gap-4 lg:gap-3 bg-n-solid-1 p-4 outline outline-1 outline-n-weak rounded-xl"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<label class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.FORM.FAIR_DISTRIBUTION.INPUT_MAX'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
<div class="flex-1">
|
||||||
|
<Input
|
||||||
|
v-model="fairDistributionLimit"
|
||||||
|
type="number"
|
||||||
|
placeholder="100"
|
||||||
|
:max="100000"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex sm:flex-row flex-col items-start sm:items-center gap-4">
|
||||||
|
<label class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.FORM.FAIR_DISTRIBUTION.DURATION'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 flex-1 [&>select]:!bg-n-alpha-2 [&>select]:!outline-none [&>select]:hover:brightness-110"
|
||||||
|
>
|
||||||
|
<!-- allow 10 mins to 999 days -->
|
||||||
|
<DurationInput
|
||||||
|
v-model:model-value="fairDistributionWindow"
|
||||||
|
v-model:unit="windowUnit"
|
||||||
|
min="10"
|
||||||
|
max="1438560"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['select']);
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
if (!props.isActive) {
|
||||||
|
emit('select', props.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative cursor-pointer rounded-xl outline outline-1 p-4 transition-all duration-200 bg-n-solid-1 py-4 ltr:pl-4 rtl:pr-4 ltr:pr-6 rtl:pl-6"
|
||||||
|
:class="[
|
||||||
|
isActive ? 'outline-n-blue-9' : 'outline-n-weak hover:outline-n-strong',
|
||||||
|
]"
|
||||||
|
@click="handleChange"
|
||||||
|
>
|
||||||
|
<div class="absolute top-4 right-4">
|
||||||
|
<input
|
||||||
|
:id="`${id}`"
|
||||||
|
:checked="isActive"
|
||||||
|
:value="id"
|
||||||
|
:name="id"
|
||||||
|
type="radio"
|
||||||
|
class="h-4 w-4 border-n-slate-6 text-n-brand focus:ring-n-brand focus:ring-offset-0"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex flex-col gap-3 items-start">
|
||||||
|
<h3 class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ label }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-n-slate-11">
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
<script setup>
|
||||||
|
import AddDataDropdown from '../AddDataDropdown.vue';
|
||||||
|
|
||||||
|
const mockInboxes = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Website Support',
|
||||||
|
email: 'support@company.com',
|
||||||
|
icon: 'i-lucide-globe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Email Support',
|
||||||
|
email: 'help@company.com',
|
||||||
|
icon: 'i-lucide-mail',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'WhatsApp Business',
|
||||||
|
phoneNumber: '+1 555-0123',
|
||||||
|
icon: 'i-lucide-message-circle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Facebook Messenger',
|
||||||
|
email: 'messenger@company.com',
|
||||||
|
icon: 'i-lucide-facebook',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'Twitter DM',
|
||||||
|
email: 'twitter@company.com',
|
||||||
|
icon: 'i-lucide-twitter',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleAdd = item => {
|
||||||
|
console.log('Add item:', item);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/AgentManagementPolicy/AddDataDropdown"
|
||||||
|
:layout="{ type: 'grid', width: '400px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Basic Usage">
|
||||||
|
<div class="p-8 bg-n-background flex gap-4 h-[400px] items-start">
|
||||||
|
<AddDataDropdown
|
||||||
|
label="Add Inbox"
|
||||||
|
search-placeholder="Search inboxes..."
|
||||||
|
:items="mockInboxes"
|
||||||
|
@add="handleAdd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import BaseInfo from '../BaseInfo.vue';
|
||||||
|
|
||||||
|
const policyName = ref('Round Robin Policy');
|
||||||
|
const description = ref(
|
||||||
|
'Distributes conversations evenly among available agents'
|
||||||
|
);
|
||||||
|
const enabled = ref(true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/AgentManagementPolicy/BaseInfo"
|
||||||
|
:layout="{ type: 'grid', width: '600px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Basic Usage">
|
||||||
|
<div class="p-8 bg-n-background">
|
||||||
|
<BaseInfo
|
||||||
|
v-model:policy-name="policyName"
|
||||||
|
v-model:description="description"
|
||||||
|
v-model:enabled="enabled"
|
||||||
|
name-label="Policy Name"
|
||||||
|
name-placeholder="Enter policy name"
|
||||||
|
description-label="Description"
|
||||||
|
description-placeholder="Enter policy description"
|
||||||
|
status-label="Status"
|
||||||
|
status-placeholder="Active"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
<script setup>
|
||||||
|
import DataTable from '../DataTable.vue';
|
||||||
|
|
||||||
|
const mockItems = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Website Support',
|
||||||
|
email: 'support@company.com',
|
||||||
|
icon: 'i-lucide-globe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Email Support',
|
||||||
|
email: 'help@company.com',
|
||||||
|
icon: 'i-lucide-mail',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'WhatsApp Business',
|
||||||
|
phoneNumber: '+1 555-0123',
|
||||||
|
icon: 'i-lucide-message-circle',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleDelete = itemId => {
|
||||||
|
console.log('Delete item:', itemId);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/AgentManagementPolicy/DataTable"
|
||||||
|
:layout="{ type: 'grid', width: '600px' }"
|
||||||
|
>
|
||||||
|
<Variant title="With Data">
|
||||||
|
<div class="p-8 bg-n-background">
|
||||||
|
<DataTable
|
||||||
|
:items="mockItems"
|
||||||
|
:is-fetching="false"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Loading State">
|
||||||
|
<div class="p-8 bg-n-background">
|
||||||
|
<DataTable :items="[]" is-fetching @delete="handleDelete" />
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Empty State">
|
||||||
|
<div class="p-8 bg-n-background">
|
||||||
|
<DataTable
|
||||||
|
:items="[]"
|
||||||
|
:is-fetching="false"
|
||||||
|
empty-state-message="No items found"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import FairDistribution from '../FairDistribution.vue';
|
||||||
|
|
||||||
|
const fairDistributionLimit = ref(100);
|
||||||
|
const fairDistributionWindow = ref(3600);
|
||||||
|
const windowUnit = ref('minutes');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/AgentManagementPolicy/FairDistribution"
|
||||||
|
:layout="{ type: 'grid', width: '800px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Basic Usage">
|
||||||
|
<div class="p-8 bg-n-background">
|
||||||
|
<FairDistribution
|
||||||
|
v-model:fair-distribution-limit="fairDistributionLimit"
|
||||||
|
v-model:fair-distribution-window="fairDistributionWindow"
|
||||||
|
v-model:window-unit="windowUnit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import RadioCard from '../RadioCard.vue';
|
||||||
|
|
||||||
|
const selectedOption = ref('round_robin');
|
||||||
|
|
||||||
|
const handleSelect = value => {
|
||||||
|
selectedOption.value = value;
|
||||||
|
console.log('Selected:', value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/AgentManagementPolicy/RadioCard"
|
||||||
|
:layout="{ type: 'grid', width: '600px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Basic Usage">
|
||||||
|
<div class="p-8 bg-n-background space-y-4">
|
||||||
|
<RadioCard
|
||||||
|
id="round_robin"
|
||||||
|
label="Round Robin"
|
||||||
|
description="Distributes conversations evenly among all available agents in a rotating manner"
|
||||||
|
:is-active="selectedOption === 'round_robin'"
|
||||||
|
@select="handleSelect"
|
||||||
|
/>
|
||||||
|
<RadioCard
|
||||||
|
id="balanced"
|
||||||
|
label="Balanced Assignment"
|
||||||
|
description="Assigns conversations based on agent workload to maintain balance"
|
||||||
|
:is-active="selectedOption === 'balanced'"
|
||||||
|
@select="handleSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Active State">
|
||||||
|
<div class="p-8 bg-n-background">
|
||||||
|
<RadioCard
|
||||||
|
id="active_option"
|
||||||
|
label="Active Option"
|
||||||
|
description="This option is currently selected and active"
|
||||||
|
is-active
|
||||||
|
@select="handleSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Inactive State">
|
||||||
|
<div class="p-8 bg-n-background">
|
||||||
|
<RadioCard
|
||||||
|
id="inactive_option"
|
||||||
|
label="Inactive Option"
|
||||||
|
description="This option is not selected and can be clicked to activate"
|
||||||
|
is-active
|
||||||
|
@select="handleSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@ -78,6 +78,8 @@ watch(unit, () => {
|
|||||||
<option :value="DURATION_UNITS.HOURS">
|
<option :value="DURATION_UNITS.HOURS">
|
||||||
{{ t('DURATION_INPUT.HOURS') }}
|
{{ t('DURATION_INPUT.HOURS') }}
|
||||||
</option>
|
</option>
|
||||||
<option :value="DURATION_UNITS.DAYS">{{ t('DURATION_INPUT.DAYS') }}</option>
|
<option :value="DURATION_UNITS.DAYS">
|
||||||
|
{{ t('DURATION_INPUT.DAYS') }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ const props = defineProps({
|
|||||||
validator: value => ['info', 'error', 'success'].includes(value),
|
validator: value => ['info', 'error', 'success'].includes(value),
|
||||||
},
|
},
|
||||||
min: { type: String, default: '' },
|
min: { type: String, default: '' },
|
||||||
|
max: { type: String, default: '' },
|
||||||
autofocus: { type: Boolean, default: false },
|
autofocus: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -108,6 +109,11 @@ onMounted(() => {
|
|||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:min="['date', 'datetime-local', 'time'].includes(type) ? min : undefined"
|
:min="['date', 'datetime-local', 'time'].includes(type) ? min : undefined"
|
||||||
|
:max="
|
||||||
|
['date', 'datetime-local', 'time', 'number'].includes(type)
|
||||||
|
? max
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
class="block w-full reset-base text-sm h-10 !px-3 !py-2.5 !mb-0 outline outline-1 border-none border-0 outline-offset-[-1px] rounded-lg bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out"
|
class="block w-full reset-base text-sm h-10 !px-3 !py-2.5 !mb-0 outline outline-1 border-none border-0 outline-offset-[-1px] rounded-lg bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
|
|||||||
@ -468,6 +468,100 @@
|
|||||||
},
|
},
|
||||||
"NO_RECORDS_FOUND": "No assignment policies found"
|
"NO_RECORDS_FOUND": "No assignment policies found"
|
||||||
},
|
},
|
||||||
|
"CREATE": {
|
||||||
|
"HEADER": {
|
||||||
|
"TITLE": "Create assignment policy"
|
||||||
|
},
|
||||||
|
"CREATE_BUTTON": "Create policy",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Assignment policy created successfully",
|
||||||
|
"ERROR_MESSAGE": "Failed to create assignment policy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EDIT": {
|
||||||
|
"HEADER": {
|
||||||
|
"TITLE": "Edit assignment policy"
|
||||||
|
},
|
||||||
|
"EDIT_BUTTON": "Update policy",
|
||||||
|
"CONFIRM_ADD_INBOX_DIALOG": {
|
||||||
|
"TITLE": "Add inbox",
|
||||||
|
"DESCRIPTION": "{inboxName} inbox is already linked to another policy. Are you sure you want to link it to this policy? It will be unlinked from the other policy.",
|
||||||
|
"CONFIRM_BUTTON_LABEL": "Continue",
|
||||||
|
"CANCEL_BUTTON_LABEL": "Cancel"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Assignment policy updated successfully",
|
||||||
|
"ERROR_MESSAGE": "Failed to update assignment policy"
|
||||||
|
},
|
||||||
|
"INBOX_API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS_MESSAGE": "Inbox added to policy successfully",
|
||||||
|
"ERROR_MESSAGE": "Failed to add inbox to policy"
|
||||||
|
},
|
||||||
|
"REMOVE": {
|
||||||
|
"SUCCESS_MESSAGE": "Inbox removed from policy successfully",
|
||||||
|
"ERROR_MESSAGE": "Failed to remove inbox from policy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FORM": {
|
||||||
|
"NAME": {
|
||||||
|
"LABEL": "Policy name:",
|
||||||
|
"PLACEHOLDER": "Enter policy name"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"LABEL": "Description:",
|
||||||
|
"PLACEHOLDER": "Enter description"
|
||||||
|
},
|
||||||
|
"STATUS": {
|
||||||
|
"LABEL": "Status:",
|
||||||
|
"PLACEHOLDER": "Select status",
|
||||||
|
"ACTIVE": "Policy is active",
|
||||||
|
"INACTIVE": "Policy is inactive"
|
||||||
|
},
|
||||||
|
"ASSIGNMENT_ORDER": {
|
||||||
|
"LABEL": "Assignment order",
|
||||||
|
"ROUND_ROBIN": {
|
||||||
|
"LABEL": "Round robin",
|
||||||
|
"DESCRIPTION": "Assign conversations evenly among agents."
|
||||||
|
},
|
||||||
|
"BALANCED": {
|
||||||
|
"LABEL": "Balanced",
|
||||||
|
"DESCRIPTION": "Assign conversations based on available capacity."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ASSIGNMENT_PRIORITY": {
|
||||||
|
"LABEL": "Assignment priority",
|
||||||
|
"EARLIEST_CREATED": {
|
||||||
|
"LABEL": "Earliest created",
|
||||||
|
"DESCRIPTION": "The conversation that was created first gets assigned first."
|
||||||
|
},
|
||||||
|
"LONGEST_WAITING": {
|
||||||
|
"LABEL": "Longest waiting",
|
||||||
|
"DESCRIPTION": "The conversation waiting the longest gets assigned first."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FAIR_DISTRIBUTION": {
|
||||||
|
"LABEL": "Fair distribution policy",
|
||||||
|
"DESCRIPTION": "Set the maximum number of conversations that can be assigned per agent within a time window to avoid overloading any one agent. This required field defaults to 100 conversations per hour.",
|
||||||
|
"INPUT_MAX": "Assign max",
|
||||||
|
"DURATION": "Conversations per agent in every"
|
||||||
|
},
|
||||||
|
"INBOXES": {
|
||||||
|
"LABEL": "Added inboxes",
|
||||||
|
"DESCRIPTION": "Add inboxes for which this policy will be applicable.",
|
||||||
|
"ADD_BUTTON": "Add inbox",
|
||||||
|
"DROPDOWN": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search and select inboxes to add",
|
||||||
|
"ADD_BUTTON": "Add"
|
||||||
|
},
|
||||||
|
"EMPTY_STATE": "No inboxes added to this policy, add an inbox to get started",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Inbox successfully added to policy",
|
||||||
|
"ERROR_MESSAGE": "Failed to add inbox to policy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"DELETE_POLICY": {
|
"DELETE_POLICY": {
|
||||||
"TITLE": "Delete policy",
|
"TITLE": "Delete policy",
|
||||||
"DESCRIPTION": "Are you sure you want to delete this policy? This action cannot be undone.",
|
"DESCRIPTION": "Are you sure you want to delete this policy? This action cannot be undone.",
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { frontendURL } from '../../../../helper/URLHelper';
|
|||||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||||
import AssignmentPolicyIndex from './Index.vue';
|
import AssignmentPolicyIndex from './Index.vue';
|
||||||
import AgentAssignmentIndex from './pages/AgentAssignmentIndexPage.vue';
|
import AgentAssignmentIndex from './pages/AgentAssignmentIndexPage.vue';
|
||||||
|
import AgentAssignmentCreate from './pages/AgentAssignmentCreatePage.vue';
|
||||||
|
import AgentAssignmentEdit from './pages/AgentAssignmentEditPage.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
routes: [
|
routes: [
|
||||||
@ -34,6 +36,24 @@ export default {
|
|||||||
permissions: ['administrator'],
|
permissions: ['administrator'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'assignment/create',
|
||||||
|
name: 'agent_assignment_policy_create',
|
||||||
|
component: AgentAssignmentCreate,
|
||||||
|
meta: {
|
||||||
|
featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2,
|
||||||
|
permissions: ['administrator'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'assignment/edit/:id',
|
||||||
|
name: 'agent_assignment_policy_edit',
|
||||||
|
component: AgentAssignmentEdit,
|
||||||
|
meta: {
|
||||||
|
featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2,
|
||||||
|
permissions: ['administrator'],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
// Assignment order types
|
||||||
|
export const ROUND_ROBIN = 'round_robin';
|
||||||
|
export const BALANCED = 'balanced';
|
||||||
|
|
||||||
|
// Assignment priority types
|
||||||
|
export const EARLIEST_CREATED = 'earliest_created';
|
||||||
|
export const LONGEST_WAITING = 'longest_waiting';
|
||||||
|
|
||||||
|
// Default values for fair distribution
|
||||||
|
export const DEFAULT_FAIR_DISTRIBUTION_LIMIT = 100;
|
||||||
|
export const DEFAULT_FAIR_DISTRIBUTION_WINDOW = 3600;
|
||||||
|
|
||||||
|
// Options groupings
|
||||||
|
export const OPTIONS = {
|
||||||
|
ORDER: [ROUND_ROBIN, BALANCED],
|
||||||
|
PRIORITY: [EARLIEST_CREATED, LONGEST_WAITING],
|
||||||
|
};
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
|
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||||
|
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||||
|
import AssignmentPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
const uiFlags = useMapGetter('assignmentPolicies/getUIFlags');
|
||||||
|
|
||||||
|
const breadcrumbItems = computed(() => [
|
||||||
|
{
|
||||||
|
label: t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.HEADER.TITLE'),
|
||||||
|
routeName: 'agent_assignment_policy_index',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.CREATE.HEADER.TITLE'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleBreadcrumbClick = item => {
|
||||||
|
router.push({
|
||||||
|
name: item.routeName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async formState => {
|
||||||
|
try {
|
||||||
|
const policy = await store.dispatch('assignmentPolicies/create', formState);
|
||||||
|
useAlert(
|
||||||
|
t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.CREATE.API.SUCCESS_MESSAGE')
|
||||||
|
);
|
||||||
|
formRef.value?.resetForm();
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: 'agent_assignment_policy_edit',
|
||||||
|
params: {
|
||||||
|
id: policy.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(
|
||||||
|
t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.CREATE.API.ERROR_MESSAGE')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SettingsLayout class="xl:px-44">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2 w-full justify-between">
|
||||||
|
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<AssignmentPolicyForm
|
||||||
|
ref="formRef"
|
||||||
|
mode="CREATE"
|
||||||
|
:is-loading="uiFlags.isCreating"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</SettingsLayout>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,197 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { getInboxIconByType } from 'dashboard/helper/inbox';
|
||||||
|
import {
|
||||||
|
ROUND_ROBIN,
|
||||||
|
EARLIEST_CREATED,
|
||||||
|
} from 'dashboard/routes/dashboard/settings/assignmentPolicy/constants';
|
||||||
|
|
||||||
|
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||||
|
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||||
|
import AssignmentPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue';
|
||||||
|
import ConfirmInboxDialog from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmInboxDialog.vue';
|
||||||
|
|
||||||
|
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const uiFlags = useMapGetter('assignmentPolicies/getUIFlags');
|
||||||
|
const inboxes = useMapGetter('inboxes/getAllInboxes');
|
||||||
|
const inboxUiFlags = useMapGetter('assignmentPolicies/getInboxUiFlags');
|
||||||
|
const selectedPolicyById = useMapGetter(
|
||||||
|
'assignmentPolicies/getAssignmentPolicyById'
|
||||||
|
);
|
||||||
|
|
||||||
|
const routeId = computed(() => route.params.id);
|
||||||
|
const selectedPolicy = computed(() => selectedPolicyById.value(routeId.value));
|
||||||
|
|
||||||
|
const confirmInboxDialogRef = ref(null);
|
||||||
|
// Store the policy linked to the inbox when adding a new inbox
|
||||||
|
const inboxLinkedPolicy = ref(null);
|
||||||
|
|
||||||
|
const breadcrumbItems = computed(() => [
|
||||||
|
{
|
||||||
|
label: t(`${BASE_KEY}.INDEX.HEADER.TITLE`),
|
||||||
|
routeName: 'agent_assignment_policy_index',
|
||||||
|
},
|
||||||
|
{ label: t(`${BASE_KEY}.EDIT.HEADER.TITLE`) },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const buildInboxList = allInboxes =>
|
||||||
|
allInboxes?.map(({ name, id, email, phoneNumber, channelType, medium }) => ({
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
icon: getInboxIconByType(channelType, medium, 'line'),
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const policyInboxes = computed(() =>
|
||||||
|
buildInboxList(selectedPolicy.value?.inboxes)
|
||||||
|
);
|
||||||
|
|
||||||
|
const inboxList = computed(() =>
|
||||||
|
buildInboxList(
|
||||||
|
inboxes.value?.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const formData = computed(() => ({
|
||||||
|
name: selectedPolicy.value?.name || '',
|
||||||
|
description: selectedPolicy.value?.description || '',
|
||||||
|
enabled: selectedPolicy.value?.enabled || false,
|
||||||
|
assignmentOrder: selectedPolicy.value?.assignmentOrder || ROUND_ROBIN,
|
||||||
|
conversationPriority:
|
||||||
|
selectedPolicy.value?.conversationPriority || EARLIEST_CREATED,
|
||||||
|
fairDistributionLimit: selectedPolicy.value?.fairDistributionLimit || 10,
|
||||||
|
fairDistributionWindow: selectedPolicy.value?.fairDistributionWindow || 60,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleDeleteInbox = inboxId =>
|
||||||
|
store.dispatch('assignmentPolicies/removeInboxPolicy', {
|
||||||
|
policyId: selectedPolicy.value?.id,
|
||||||
|
inboxId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBreadcrumbClick = ({ routeName }) =>
|
||||||
|
router.push({ name: routeName });
|
||||||
|
|
||||||
|
const setInboxPolicy = async (inboxId, policyId) => {
|
||||||
|
try {
|
||||||
|
await store.dispatch('assignmentPolicies/setInboxPolicy', {
|
||||||
|
inboxId,
|
||||||
|
policyId,
|
||||||
|
});
|
||||||
|
useAlert(t(`${BASE_KEY}.FORM.INBOXES.API.SUCCESS_MESSAGE`));
|
||||||
|
await store.dispatch(
|
||||||
|
'assignmentPolicies/getInboxes',
|
||||||
|
Number(routeId.value)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t(`${BASE_KEY}.FORM.INBOXES.API.ERROR_MESSAGE`));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddInbox = async inbox => {
|
||||||
|
try {
|
||||||
|
const policy = await store.dispatch('assignmentPolicies/getInboxPolicy', {
|
||||||
|
inboxId: inbox?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (policy?.id !== selectedPolicy.value?.id) {
|
||||||
|
inboxLinkedPolicy.value = {
|
||||||
|
...policy,
|
||||||
|
assignedInboxCount: policy.assignedInboxCount - 1,
|
||||||
|
};
|
||||||
|
confirmInboxDialogRef.value.openDialog(inbox);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If getInboxPolicy fails, continue to setInboxPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
await setInboxPolicy(inbox?.id, selectedPolicy.value?.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmAddInbox = async inboxId => {
|
||||||
|
const success = await setInboxPolicy(inboxId, selectedPolicy.value?.id);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Update the policy to reflect the assigned inbox count change
|
||||||
|
await store.dispatch('assignmentPolicies/updateInboxPolicy', {
|
||||||
|
policy: inboxLinkedPolicy.value,
|
||||||
|
});
|
||||||
|
// Fetch the updated inboxes for the policy after update, to reflect real-time changes
|
||||||
|
store.dispatch(
|
||||||
|
'assignmentPolicies/getInboxes',
|
||||||
|
inboxLinkedPolicy.value?.id
|
||||||
|
);
|
||||||
|
inboxLinkedPolicy.value = null;
|
||||||
|
confirmInboxDialogRef.value.closeDialog();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async formState => {
|
||||||
|
try {
|
||||||
|
await store.dispatch('assignmentPolicies/update', {
|
||||||
|
id: selectedPolicy.value?.id,
|
||||||
|
...formState,
|
||||||
|
});
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.API.SUCCESS_MESSAGE`));
|
||||||
|
} catch {
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.API.ERROR_MESSAGE`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPolicyData = async () => {
|
||||||
|
if (!routeId.value) return;
|
||||||
|
|
||||||
|
// Fetch policy if not available
|
||||||
|
if (!selectedPolicy.value?.id)
|
||||||
|
await store.dispatch('assignmentPolicies/show', routeId.value);
|
||||||
|
|
||||||
|
await store.dispatch('assignmentPolicies/getInboxes', Number(routeId.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(routeId, fetchPolicyData, { immediate: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SettingsLayout :is-loading="uiFlags.isFetchingItem" class="xl:px-44">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2 w-full justify-between">
|
||||||
|
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<AssignmentPolicyForm
|
||||||
|
:key="routeId"
|
||||||
|
mode="EDIT"
|
||||||
|
:initial-data="formData"
|
||||||
|
:policy-inboxes="policyInboxes"
|
||||||
|
:inbox-list="inboxList"
|
||||||
|
show-inbox-section
|
||||||
|
:is-loading="uiFlags.isUpdating"
|
||||||
|
:is-inbox-loading="inboxUiFlags.isFetching"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@add-inbox="handleAddInbox"
|
||||||
|
@delete-inbox="handleDeleteInbox"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ConfirmInboxDialog
|
||||||
|
ref="confirmInboxDialogRef"
|
||||||
|
@add="handleConfirmAddInbox"
|
||||||
|
/>
|
||||||
|
</SettingsLayout>
|
||||||
|
</template>
|
||||||
@ -44,7 +44,16 @@ const handleBreadcrumbClick = item => {
|
|||||||
|
|
||||||
const onClickCreatePolicy = () => {
|
const onClickCreatePolicy = () => {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'assignment_policy_create',
|
name: 'agent_assignment_policy_create',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickEditPolicy = id => {
|
||||||
|
router.push({
|
||||||
|
name: 'agent_assignment_policy_edit',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,6 +115,7 @@ onMounted(() => {
|
|||||||
v-bind="policy"
|
v-bind="policy"
|
||||||
:is-fetching-inboxes="inboxUiFlags.isFetching"
|
:is-fetching-inboxes="inboxUiFlags.isFetching"
|
||||||
@fetch-inboxes="handleFetchInboxes"
|
@fetch-inboxes="handleFetchInboxes"
|
||||||
|
@edit="onClickEditPolicy"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,254 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useConfig } from 'dashboard/composables/useConfig';
|
||||||
|
import BaseInfo from 'dashboard/components-next/AssignmentPolicy/components/BaseInfo.vue';
|
||||||
|
import RadioCard from 'dashboard/components-next/AssignmentPolicy/components/RadioCard.vue';
|
||||||
|
import FairDistribution from 'dashboard/components-next/AssignmentPolicy/components/FairDistribution.vue';
|
||||||
|
import DataTable from 'dashboard/components-next/AssignmentPolicy/components/DataTable.vue';
|
||||||
|
import AddDataDropdown from 'dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue';
|
||||||
|
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import {
|
||||||
|
OPTIONS,
|
||||||
|
ROUND_ROBIN,
|
||||||
|
EARLIEST_CREATED,
|
||||||
|
DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||||
|
DEFAULT_FAIR_DISTRIBUTION_WINDOW,
|
||||||
|
} from 'dashboard/routes/dashboard/settings/assignmentPolicy/constants';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
initialData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
enabled: false,
|
||||||
|
assignmentOrder: ROUND_ROBIN,
|
||||||
|
conversationPriority: EARLIEST_CREATED,
|
||||||
|
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||||
|
fairDistributionWindow: DEFAULT_FAIR_DISTRIBUTION_WINDOW,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validator: value => ['CREATE', 'EDIT'].includes(value),
|
||||||
|
},
|
||||||
|
policyInboxes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
inboxList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
showInboxSection: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isInboxLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'submit',
|
||||||
|
'addInbox',
|
||||||
|
'deleteInbox',
|
||||||
|
'validationChange',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { isEnterprise } = useConfig();
|
||||||
|
|
||||||
|
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY';
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
enabled: false,
|
||||||
|
assignmentOrder: ROUND_ROBIN,
|
||||||
|
conversationPriority: EARLIEST_CREATED,
|
||||||
|
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||||
|
fairDistributionWindow: DEFAULT_FAIR_DISTRIBUTION_WINDOW,
|
||||||
|
});
|
||||||
|
|
||||||
|
const validationState = ref({
|
||||||
|
isValid: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createOption = (type, key, stateKey) => ({
|
||||||
|
key,
|
||||||
|
label: t(`${BASE_KEY}.FORM.${type}.${key.toUpperCase()}.LABEL`),
|
||||||
|
description: t(`${BASE_KEY}.FORM.${type}.${key.toUpperCase()}.DESCRIPTION`),
|
||||||
|
isActive: state[stateKey] === key,
|
||||||
|
});
|
||||||
|
|
||||||
|
const assignmentOrderOptions = computed(() => {
|
||||||
|
const options = OPTIONS.ORDER.filter(
|
||||||
|
key => isEnterprise || key !== 'balanced'
|
||||||
|
);
|
||||||
|
return options.map(key =>
|
||||||
|
createOption('ASSIGNMENT_ORDER', key, 'assignmentOrder')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const assignmentPriorityOptions = computed(() =>
|
||||||
|
OPTIONS.PRIORITY.map(key =>
|
||||||
|
createOption('ASSIGNMENT_PRIORITY', key, 'conversationPriority')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioSections = computed(() => [
|
||||||
|
{
|
||||||
|
key: 'assignmentOrder',
|
||||||
|
label: t(`${BASE_KEY}.FORM.ASSIGNMENT_ORDER.LABEL`),
|
||||||
|
options: assignmentOrderOptions.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'conversationPriority',
|
||||||
|
label: t(`${BASE_KEY}.FORM.ASSIGNMENT_PRIORITY.LABEL`),
|
||||||
|
options: assignmentPriorityOptions.value,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const buttonLabel = computed(() =>
|
||||||
|
t(`${BASE_KEY}.${props.mode.toUpperCase()}.${props.mode}_BUTTON`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleValidationChange = validation => {
|
||||||
|
validationState.value = validation;
|
||||||
|
emit('validationChange', validation);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(state, {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
enabled: false,
|
||||||
|
assignmentOrder: ROUND_ROBIN,
|
||||||
|
conversationPriority: EARLIEST_CREATED,
|
||||||
|
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||||
|
fairDistributionWindow: DEFAULT_FAIR_DISTRIBUTION_WINDOW,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
emit('submit', { ...state });
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialData,
|
||||||
|
newData => {
|
||||||
|
Object.assign(state, newData);
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
resetForm,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<div class="flex flex-col gap-4 divide-y divide-n-weak mb-4">
|
||||||
|
<BaseInfo
|
||||||
|
v-model:policy-name="state.name"
|
||||||
|
v-model:description="state.description"
|
||||||
|
v-model:enabled="state.enabled"
|
||||||
|
:name-label="t(`${BASE_KEY}.FORM.NAME.LABEL`)"
|
||||||
|
:name-placeholder="t(`${BASE_KEY}.FORM.NAME.PLACEHOLDER`)"
|
||||||
|
:description-label="t(`${BASE_KEY}.FORM.DESCRIPTION.LABEL`)"
|
||||||
|
:description-placeholder="t(`${BASE_KEY}.FORM.DESCRIPTION.PLACEHOLDER`)"
|
||||||
|
:status-label="t(`${BASE_KEY}.FORM.STATUS.LABEL`)"
|
||||||
|
:status-placeholder="
|
||||||
|
t(`${BASE_KEY}.FORM.STATUS.${state.enabled ? 'ACTIVE' : 'INACTIVE'}`)
|
||||||
|
"
|
||||||
|
@validation-change="handleValidationChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
v-for="section in radioSections"
|
||||||
|
:key="section.key"
|
||||||
|
class="py-4 flex flex-col items-start gap-3 w-full"
|
||||||
|
>
|
||||||
|
<WithLabel
|
||||||
|
:label="section.label"
|
||||||
|
name="assignmentPolicy"
|
||||||
|
class="w-full flex items-start flex-col gap-3"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 xs:grid-cols-2 gap-4 w-full">
|
||||||
|
<RadioCard
|
||||||
|
v-for="option in section.options"
|
||||||
|
:id="option.key"
|
||||||
|
:key="option.key"
|
||||||
|
:label="option.label"
|
||||||
|
:description="option.description"
|
||||||
|
:is-active="option.isActive"
|
||||||
|
@select="state[section.key] = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</WithLabel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4 pb-2 flex-col flex gap-4">
|
||||||
|
<div class="flex flex-col items-start gap-1 py-1">
|
||||||
|
<label class="text-sm font-medium text-n-slate-12 py-1">
|
||||||
|
{{ t(`${BASE_KEY}.FORM.FAIR_DISTRIBUTION.LABEL`) }}
|
||||||
|
</label>
|
||||||
|
<p class="mb-0 text-n-slate-11 text-sm">
|
||||||
|
{{ t(`${BASE_KEY}.FORM.FAIR_DISTRIBUTION.DESCRIPTION`) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FairDistribution
|
||||||
|
v-model:fair-distribution-limit="state.fairDistributionLimit"
|
||||||
|
v-model:fair-distribution-window="state.fairDistributionWindow"
|
||||||
|
v-model:window-unit="state.windowUnit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showInboxSection" class="py-4 flex-col flex gap-4">
|
||||||
|
<div class="flex items-end gap-4 w-full justify-between">
|
||||||
|
<div class="flex flex-col items-start gap-1 py-1">
|
||||||
|
<label class="text-sm font-medium text-n-slate-12 py-1">
|
||||||
|
{{ t(`${BASE_KEY}.FORM.INBOXES.LABEL`) }}
|
||||||
|
</label>
|
||||||
|
<p class="mb-0 text-n-slate-11 text-sm">
|
||||||
|
{{ t(`${BASE_KEY}.FORM.INBOXES.DESCRIPTION`) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<AddDataDropdown
|
||||||
|
:label="t(`${BASE_KEY}.FORM.INBOXES.ADD_BUTTON`)"
|
||||||
|
:search-placeholder="
|
||||||
|
t(`${BASE_KEY}.FORM.INBOXES.DROPDOWN.SEARCH_PLACEHOLDER`)
|
||||||
|
"
|
||||||
|
:items="inboxList"
|
||||||
|
@add="$emit('addInbox', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
:items="policyInboxes"
|
||||||
|
:is-fetching="isInboxLoading"
|
||||||
|
:empty-state-message="t(`${BASE_KEY}.FORM.INBOXES.EMPTY_STATE`)"
|
||||||
|
@delete="$emit('deleteInbox', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
:label="buttonLabel"
|
||||||
|
:disabled="!validationState.isValid || isLoading"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['add']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const dialogRef = ref(null);
|
||||||
|
const currentInbox = ref(null);
|
||||||
|
|
||||||
|
const openDialog = inbox => {
|
||||||
|
currentInbox.value = inbox;
|
||||||
|
dialogRef.value.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
dialogRef.value.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogConfirm = () => {
|
||||||
|
emit('add', currentInbox.value.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ openDialog, closeDialog });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
type="alert"
|
||||||
|
:title="
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.CONFIRM_ADD_INBOX_DIALOG.TITLE'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:description="
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.CONFIRM_ADD_INBOX_DIALOG.DESCRIPTION',
|
||||||
|
{
|
||||||
|
inboxName: currentInbox?.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:confirm-button-label="
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.CONFIRM_ADD_INBOX_DIALOG.CONFIRM_BUTTON_LABEL'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:cancel-button-label="
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.CONFIRM_ADD_INBOX_DIALOG.CANCEL_BUTTON_LABEL'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@confirm="handleDialogConfirm"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -3,6 +3,7 @@ import types from '../mutation-types';
|
|||||||
import AssignmentPoliciesAPI from '../../api/assignmentPolicies';
|
import AssignmentPoliciesAPI from '../../api/assignmentPolicies';
|
||||||
import { throwErrorMessage } from '../utils/api';
|
import { throwErrorMessage } from '../utils/api';
|
||||||
import camelcaseKeys from 'camelcase-keys';
|
import camelcaseKeys from 'camelcase-keys';
|
||||||
|
import snakecaseKeys from 'snakecase-keys';
|
||||||
|
|
||||||
export const state = {
|
export const state = {
|
||||||
records: [],
|
records: [],
|
||||||
@ -15,6 +16,7 @@ export const state = {
|
|||||||
},
|
},
|
||||||
inboxUiFlags: {
|
inboxUiFlags: {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
|
isDeleting: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -51,7 +53,7 @@ export const actions = {
|
|||||||
try {
|
try {
|
||||||
const response = await AssignmentPoliciesAPI.show(policyId);
|
const response = await AssignmentPoliciesAPI.show(policyId);
|
||||||
const policy = camelcaseKeys(response.data);
|
const policy = camelcaseKeys(response.data);
|
||||||
commit(types.EDIT_ASSIGNMENT_POLICY, policy);
|
commit(types.SET_ASSIGNMENT_POLICY, policy);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throwErrorMessage(error);
|
throwErrorMessage(error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -62,7 +64,9 @@ export const actions = {
|
|||||||
create: async function create({ commit }, policyObj) {
|
create: async function create({ commit }, policyObj) {
|
||||||
commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true });
|
commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true });
|
||||||
try {
|
try {
|
||||||
const response = await AssignmentPoliciesAPI.create(policyObj);
|
const response = await AssignmentPoliciesAPI.create(
|
||||||
|
snakecaseKeys(policyObj)
|
||||||
|
);
|
||||||
commit(types.ADD_ASSIGNMENT_POLICY, camelcaseKeys(response.data));
|
commit(types.ADD_ASSIGNMENT_POLICY, camelcaseKeys(response.data));
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -76,7 +80,10 @@ export const actions = {
|
|||||||
update: async function update({ commit }, { id, ...policyParams }) {
|
update: async function update({ commit }, { id, ...policyParams }) {
|
||||||
commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true });
|
commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true });
|
||||||
try {
|
try {
|
||||||
const response = await AssignmentPoliciesAPI.update(id, policyParams);
|
const response = await AssignmentPoliciesAPI.update(
|
||||||
|
id,
|
||||||
|
snakecaseKeys(policyParams)
|
||||||
|
);
|
||||||
commit(types.EDIT_ASSIGNMENT_POLICY, camelcaseKeys(response.data));
|
commit(types.EDIT_ASSIGNMENT_POLICY, camelcaseKeys(response.data));
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -117,6 +124,68 @@ export const actions = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setInboxPolicy: async function setInboxPolicy(
|
||||||
|
{ commit },
|
||||||
|
{ inboxId, policyId }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const response = await AssignmentPoliciesAPI.setInboxPolicy(
|
||||||
|
inboxId,
|
||||||
|
policyId
|
||||||
|
);
|
||||||
|
commit(
|
||||||
|
types.ADD_ASSIGNMENT_POLICIES_INBOXES,
|
||||||
|
camelcaseKeys(response.data)
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throwErrorMessage(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getInboxPolicy: async function getInboxPolicy(_, { inboxId }) {
|
||||||
|
try {
|
||||||
|
const response = await AssignmentPoliciesAPI.getInboxPolicy(inboxId);
|
||||||
|
return camelcaseKeys(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
throwErrorMessage(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateInboxPolicy: async function updateInboxPolicy({ commit }, { policy }) {
|
||||||
|
try {
|
||||||
|
commit(types.EDIT_ASSIGNMENT_POLICY, policy);
|
||||||
|
} catch (error) {
|
||||||
|
throwErrorMessage(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeInboxPolicy: async function removeInboxPolicy(
|
||||||
|
{ commit },
|
||||||
|
{ policyId, inboxId }
|
||||||
|
) {
|
||||||
|
commit(types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, {
|
||||||
|
isDeleting: true,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await AssignmentPoliciesAPI.removeInboxPolicy(inboxId);
|
||||||
|
commit(types.DELETE_ASSIGNMENT_POLICIES_INBOXES, {
|
||||||
|
policyId,
|
||||||
|
inboxId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throwErrorMessage(error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, {
|
||||||
|
isDeleting: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
@ -128,8 +197,9 @@ export const mutations = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_ASSIGNMENT_POLICIES]: MutationHelpers.set,
|
[types.SET_ASSIGNMENT_POLICIES]: MutationHelpers.set,
|
||||||
|
[types.SET_ASSIGNMENT_POLICY]: MutationHelpers.setSingleRecord,
|
||||||
[types.ADD_ASSIGNMENT_POLICY]: MutationHelpers.create,
|
[types.ADD_ASSIGNMENT_POLICY]: MutationHelpers.create,
|
||||||
[types.EDIT_ASSIGNMENT_POLICY]: MutationHelpers.update,
|
[types.EDIT_ASSIGNMENT_POLICY]: MutationHelpers.updateAttributes,
|
||||||
[types.DELETE_ASSIGNMENT_POLICY]: MutationHelpers.destroy,
|
[types.DELETE_ASSIGNMENT_POLICY]: MutationHelpers.destroy,
|
||||||
|
|
||||||
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG](_state, data) {
|
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG](_state, data) {
|
||||||
@ -138,13 +208,19 @@ export const mutations = {
|
|||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_ASSIGNMENT_POLICIES_INBOXES](_state, { policyId, inboxes }) {
|
[types.SET_ASSIGNMENT_POLICIES_INBOXES](_state, { policyId, inboxes }) {
|
||||||
const policy = _state.records.find(p => p.id === policyId);
|
const policy = _state.records.find(p => p.id === policyId);
|
||||||
if (policy) {
|
if (policy) {
|
||||||
policy.inboxes = inboxes;
|
policy.inboxes = inboxes;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](_state, { policyId, inboxId }) {
|
||||||
|
const policy = _state.records.find(p => p.id === policyId);
|
||||||
|
if (policy) {
|
||||||
|
policy.inboxes = policy?.inboxes?.filter(inbox => inbox.id !== inboxId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[types.ADD_ASSIGNMENT_POLICIES_INBOXES]: MutationHelpers.updateAttributes,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -29,6 +29,9 @@ export const getters = {
|
|||||||
getInboxes($state) {
|
getInboxes($state) {
|
||||||
return $state.records;
|
return $state.records;
|
||||||
},
|
},
|
||||||
|
getAllInboxes($state) {
|
||||||
|
return camelcaseKeys($state.records, { deep: true });
|
||||||
|
},
|
||||||
getWhatsAppTemplates: $state => inboxId => {
|
getWhatsAppTemplates: $state => inboxId => {
|
||||||
const [inbox] = $state.records.filter(
|
const [inbox] = $state.records.filter(
|
||||||
record => record.id === Number(inboxId)
|
record => record.id === Number(inboxId)
|
||||||
|
|||||||
@ -3,12 +3,14 @@ import { actions } from '../../assignmentPolicies';
|
|||||||
import types from '../../../mutation-types';
|
import types from '../../../mutation-types';
|
||||||
import assignmentPoliciesList, { camelCaseFixtures } from './fixtures';
|
import assignmentPoliciesList, { camelCaseFixtures } from './fixtures';
|
||||||
import camelcaseKeys from 'camelcase-keys';
|
import camelcaseKeys from 'camelcase-keys';
|
||||||
|
import snakecaseKeys from 'snakecase-keys';
|
||||||
|
|
||||||
const commit = vi.fn();
|
const commit = vi.fn();
|
||||||
|
|
||||||
global.axios = axios;
|
global.axios = axios;
|
||||||
vi.mock('axios');
|
vi.mock('axios');
|
||||||
vi.mock('camelcase-keys');
|
vi.mock('camelcase-keys');
|
||||||
|
vi.mock('snakecase-keys');
|
||||||
vi.mock('../../../utils/api');
|
vi.mock('../../../utils/api');
|
||||||
|
|
||||||
describe('#actions', () => {
|
describe('#actions', () => {
|
||||||
@ -56,7 +58,7 @@ describe('#actions', () => {
|
|||||||
expect(camelcaseKeys).toHaveBeenCalledWith(policyData);
|
expect(camelcaseKeys).toHaveBeenCalledWith(policyData);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: true }],
|
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: true }],
|
||||||
[types.EDIT_ASSIGNMENT_POLICY, camelCasedPolicy],
|
[types.SET_ASSIGNMENT_POLICY, camelCasedPolicy],
|
||||||
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: false }],
|
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: false }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -77,12 +79,15 @@ describe('#actions', () => {
|
|||||||
it('sends correct actions if API is success', async () => {
|
it('sends correct actions if API is success', async () => {
|
||||||
const newPolicy = assignmentPoliciesList[0];
|
const newPolicy = assignmentPoliciesList[0];
|
||||||
const camelCasedData = camelCaseFixtures[0];
|
const camelCasedData = camelCaseFixtures[0];
|
||||||
|
const snakeCasedPolicy = { assignment_order: 'round_robin' };
|
||||||
|
|
||||||
axios.post.mockResolvedValue({ data: newPolicy });
|
axios.post.mockResolvedValue({ data: newPolicy });
|
||||||
camelcaseKeys.mockReturnValue(camelCasedData);
|
camelcaseKeys.mockReturnValue(camelCasedData);
|
||||||
|
snakecaseKeys.mockReturnValue(snakeCasedPolicy);
|
||||||
|
|
||||||
const result = await actions.create({ commit }, newPolicy);
|
const result = await actions.create({ commit }, newPolicy);
|
||||||
|
|
||||||
|
expect(snakecaseKeys).toHaveBeenCalledWith(newPolicy);
|
||||||
expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy);
|
expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true }],
|
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true }],
|
||||||
@ -115,12 +120,15 @@ describe('#actions', () => {
|
|||||||
...camelCaseFixtures[0],
|
...camelCaseFixtures[0],
|
||||||
name: 'Updated Policy',
|
name: 'Updated Policy',
|
||||||
};
|
};
|
||||||
|
const snakeCasedParams = { name: 'Updated Policy' };
|
||||||
|
|
||||||
axios.patch.mockResolvedValue({ data: responseData });
|
axios.patch.mockResolvedValue({ data: responseData });
|
||||||
camelcaseKeys.mockReturnValue(camelCasedData);
|
camelcaseKeys.mockReturnValue(camelCasedData);
|
||||||
|
snakecaseKeys.mockReturnValue(snakeCasedParams);
|
||||||
|
|
||||||
const result = await actions.update({ commit }, updateParams);
|
const result = await actions.update({ commit }, updateParams);
|
||||||
|
|
||||||
|
expect(snakecaseKeys).toHaveBeenCalledWith({ name: 'Updated Policy' });
|
||||||
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true }],
|
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true }],
|
||||||
@ -211,4 +219,108 @@ describe('#actions', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#setInboxPolicy', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
const responseData = { success: true, policy_id: 2 };
|
||||||
|
const camelCasedData = { success: true, policyId: 2 };
|
||||||
|
|
||||||
|
axios.post.mockResolvedValue({ data: responseData });
|
||||||
|
camelcaseKeys.mockReturnValue(camelCasedData);
|
||||||
|
|
||||||
|
const result = await actions.setInboxPolicy(
|
||||||
|
{ commit },
|
||||||
|
{ inboxId: 1, policyId: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.ADD_ASSIGNMENT_POLICIES_INBOXES, camelCasedData],
|
||||||
|
]);
|
||||||
|
expect(result).toEqual(responseData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error if API fails', async () => {
|
||||||
|
axios.post.mockRejectedValue(new Error('API Error'));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
actions.setInboxPolicy({ commit }, { inboxId: 1, policyId: 2 })
|
||||||
|
).rejects.toThrow(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#getInboxPolicy', () => {
|
||||||
|
it('returns camelCased response data if API is success', async () => {
|
||||||
|
const responseData = { policy_id: 1, name: 'Round Robin' };
|
||||||
|
const camelCasedData = { policyId: 1, name: 'Round Robin' };
|
||||||
|
|
||||||
|
axios.get.mockResolvedValue({ data: responseData });
|
||||||
|
camelcaseKeys.mockReturnValue(camelCasedData);
|
||||||
|
|
||||||
|
const result = await actions.getInboxPolicy({}, { inboxId: 1 });
|
||||||
|
|
||||||
|
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
||||||
|
expect(result).toEqual(camelCasedData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error if API fails', async () => {
|
||||||
|
axios.get.mockRejectedValue(new Error('Not found'));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
actions.getInboxPolicy({}, { inboxId: 999 })
|
||||||
|
).rejects.toThrow(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#updateInboxPolicy', () => {
|
||||||
|
it('commits EDIT_ASSIGNMENT_POLICY mutation', async () => {
|
||||||
|
const policy = { id: 1, name: 'Updated Policy' };
|
||||||
|
|
||||||
|
await actions.updateInboxPolicy({ commit }, { policy });
|
||||||
|
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.EDIT_ASSIGNMENT_POLICY, policy],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error if commit fails', async () => {
|
||||||
|
commit.mockImplementation(() => {
|
||||||
|
throw new Error('Commit failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
actions.updateInboxPolicy({ commit }, { policy: {} })
|
||||||
|
).rejects.toThrow(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#removeInboxPolicy', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
const policyId = 1;
|
||||||
|
const inboxId = 2;
|
||||||
|
|
||||||
|
axios.delete.mockResolvedValue({});
|
||||||
|
|
||||||
|
await actions.removeInboxPolicy({ commit }, { policyId, inboxId });
|
||||||
|
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: true }],
|
||||||
|
[types.DELETE_ASSIGNMENT_POLICIES_INBOXES, { policyId, inboxId }],
|
||||||
|
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends correct actions if API fails', async () => {
|
||||||
|
axios.delete.mockRejectedValue(new Error('Not found'));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
actions.removeInboxPolicy({ commit }, { policyId: 1, inboxId: 999 })
|
||||||
|
).rejects.toThrow(Error);
|
||||||
|
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: true }],
|
||||||
|
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,10 +32,12 @@ describe('#getters', () => {
|
|||||||
const state = {
|
const state = {
|
||||||
inboxUiFlags: {
|
inboxUiFlags: {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
|
isDeleting: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(getters.getInboxUiFlags(state)).toEqual({
|
expect(getters.getInboxUiFlags(state)).toEqual({
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
|
isDeleting: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -62,6 +62,24 @@ describe('#mutations', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#SET_ASSIGNMENT_POLICY', () => {
|
||||||
|
it('sets single assignment policy record', () => {
|
||||||
|
const state = { records: [] };
|
||||||
|
|
||||||
|
mutations[types.SET_ASSIGNMENT_POLICY](state, assignmentPoliciesList[0]);
|
||||||
|
|
||||||
|
expect(state.records).toEqual([assignmentPoliciesList[0]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces existing record', () => {
|
||||||
|
const state = { records: [{ id: 1, name: 'Old Policy' }] };
|
||||||
|
|
||||||
|
mutations[types.SET_ASSIGNMENT_POLICY](state, assignmentPoliciesList[0]);
|
||||||
|
|
||||||
|
expect(state.records).toEqual([assignmentPoliciesList[0]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#ADD_ASSIGNMENT_POLICY', () => {
|
describe('#ADD_ASSIGNMENT_POLICY', () => {
|
||||||
it('adds new policy to empty records', () => {
|
it('adds new policy to empty records', () => {
|
||||||
const state = { records: [] };
|
const state = { records: [] };
|
||||||
@ -264,4 +282,104 @@ describe('#mutations', () => {
|
|||||||
expect(state).toEqual(originalState);
|
expect(state).toEqual(originalState);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#DELETE_ASSIGNMENT_POLICIES_INBOXES', () => {
|
||||||
|
it('removes inbox from policy', () => {
|
||||||
|
const mockInboxes = [
|
||||||
|
{ id: 1, name: 'Support Inbox' },
|
||||||
|
{ id: 2, name: 'Sales Inbox' },
|
||||||
|
{ id: 3, name: 'Marketing Inbox' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
records: [
|
||||||
|
{ id: 1, name: 'Policy 1', inboxes: mockInboxes },
|
||||||
|
{ id: 2, name: 'Policy 2', inboxes: [] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, {
|
||||||
|
policyId: 1,
|
||||||
|
inboxId: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(state.records[0].inboxes).toEqual([
|
||||||
|
{ id: 1, name: 'Support Inbox' },
|
||||||
|
{ id: 3, name: 'Marketing Inbox' },
|
||||||
|
]);
|
||||||
|
expect(state.records[1].inboxes).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if policy not found', () => {
|
||||||
|
const state = {
|
||||||
|
records: [
|
||||||
|
{ id: 1, name: 'Policy 1', inboxes: [{ id: 1, name: 'Test' }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalState = JSON.parse(JSON.stringify(state));
|
||||||
|
|
||||||
|
mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, {
|
||||||
|
policyId: 999,
|
||||||
|
inboxId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(state).toEqual(originalState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if inbox not found in policy', () => {
|
||||||
|
const mockInboxes = [{ id: 1, name: 'Support Inbox' }];
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
records: [{ id: 1, name: 'Policy 1', inboxes: mockInboxes }],
|
||||||
|
};
|
||||||
|
|
||||||
|
mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, {
|
||||||
|
policyId: 1,
|
||||||
|
inboxId: 999,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(state.records[0].inboxes).toEqual(mockInboxes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles policy with no inboxes', () => {
|
||||||
|
const state = {
|
||||||
|
records: [{ id: 1, name: 'Policy 1' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, {
|
||||||
|
policyId: 1,
|
||||||
|
inboxId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(state.records[0]).toEqual({ id: 1, name: 'Policy 1' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#ADD_ASSIGNMENT_POLICIES_INBOXES', () => {
|
||||||
|
it('updates policy attributes using MutationHelpers.updateAttributes', () => {
|
||||||
|
const state = {
|
||||||
|
records: [
|
||||||
|
{ id: 1, name: 'Policy 1', assignedInboxCount: 2 },
|
||||||
|
{ id: 2, name: 'Policy 2', assignedInboxCount: 1 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedPolicy = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Policy 1',
|
||||||
|
assignedInboxCount: 3,
|
||||||
|
inboxes: [{ id: 1, name: 'New Inbox' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
mutations[types.ADD_ASSIGNMENT_POLICIES_INBOXES](state, updatedPolicy);
|
||||||
|
|
||||||
|
expect(state.records[0]).toEqual(updatedPolicy);
|
||||||
|
expect(state.records[1]).toEqual({
|
||||||
|
id: 2,
|
||||||
|
name: 'Policy 2',
|
||||||
|
assignedInboxCount: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -352,10 +352,13 @@ export default {
|
|||||||
// Assignment Policies
|
// Assignment Policies
|
||||||
SET_ASSIGNMENT_POLICIES_UI_FLAG: 'SET_ASSIGNMENT_POLICIES_UI_FLAG',
|
SET_ASSIGNMENT_POLICIES_UI_FLAG: 'SET_ASSIGNMENT_POLICIES_UI_FLAG',
|
||||||
SET_ASSIGNMENT_POLICIES: 'SET_ASSIGNMENT_POLICIES',
|
SET_ASSIGNMENT_POLICIES: 'SET_ASSIGNMENT_POLICIES',
|
||||||
|
SET_ASSIGNMENT_POLICY: 'SET_ASSIGNMENT_POLICY',
|
||||||
ADD_ASSIGNMENT_POLICY: 'ADD_ASSIGNMENT_POLICY',
|
ADD_ASSIGNMENT_POLICY: 'ADD_ASSIGNMENT_POLICY',
|
||||||
EDIT_ASSIGNMENT_POLICY: 'EDIT_ASSIGNMENT_POLICY',
|
EDIT_ASSIGNMENT_POLICY: 'EDIT_ASSIGNMENT_POLICY',
|
||||||
DELETE_ASSIGNMENT_POLICY: 'DELETE_ASSIGNMENT_POLICY',
|
DELETE_ASSIGNMENT_POLICY: 'DELETE_ASSIGNMENT_POLICY',
|
||||||
SET_ASSIGNMENT_POLICIES_INBOXES: 'SET_ASSIGNMENT_POLICIES_INBOXES',
|
SET_ASSIGNMENT_POLICIES_INBOXES: 'SET_ASSIGNMENT_POLICIES_INBOXES',
|
||||||
SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG:
|
SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG:
|
||||||
'SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG',
|
'SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG',
|
||||||
|
DELETE_ASSIGNMENT_POLICIES_INBOXES: 'DELETE_ASSIGNMENT_POLICIES_INBOXES',
|
||||||
|
ADD_ASSIGNMENT_POLICIES_INBOXES: 'ADD_ASSIGNMENT_POLICIES_INBOXES',
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user