feat: Agent assignment policy Create/Edit pages (#12400)
This commit is contained in:
@@ -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">
|
||||
{{ t('DURATION_INPUT.HOURS') }}
|
||||
</option>
|
||||
<option :value="DURATION_UNITS.DAYS">{{ t('DURATION_INPUT.DAYS') }}</option>
|
||||
<option :value="DURATION_UNITS.DAYS">
|
||||
{{ t('DURATION_INPUT.DAYS') }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
@@ -15,6 +15,7 @@ const props = defineProps({
|
||||
validator: value => ['info', 'error', 'success'].includes(value),
|
||||
},
|
||||
min: { type: String, default: '' },
|
||||
max: { type: String, default: '' },
|
||||
autofocus: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
@@ -108,6 +109,11 @@ onMounted(() => {
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:min="['date', 'datetime-local', 'time'].includes(type) ? min : undefined"
|
||||
:max="
|
||||
['date', 'datetime-local', 'time', 'number'].includes(type)
|
||||
? max
|
||||
: undefined
|
||||
"
|
||||
class="block w-full reset-base text-sm h-10 !px-3 !py-2.5 !mb-0 outline outline-1 border-none border-0 outline-offset-[-1px] rounded-lg bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
|
||||
@@ -468,6 +468,100 @@
|
||||
},
|
||||
"NO_RECORDS_FOUND": "No assignment policies found"
|
||||
},
|
||||
"CREATE": {
|
||||
"HEADER": {
|
||||
"TITLE": "Create assignment policy"
|
||||
},
|
||||
"CREATE_BUTTON": "Create policy",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Assignment policy created successfully",
|
||||
"ERROR_MESSAGE": "Failed to create assignment policy"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"HEADER": {
|
||||
"TITLE": "Edit assignment policy"
|
||||
},
|
||||
"EDIT_BUTTON": "Update policy",
|
||||
"CONFIRM_ADD_INBOX_DIALOG": {
|
||||
"TITLE": "Add inbox",
|
||||
"DESCRIPTION": "{inboxName} inbox is already linked to another policy. Are you sure you want to link it to this policy? It will be unlinked from the other policy.",
|
||||
"CONFIRM_BUTTON_LABEL": "Continue",
|
||||
"CANCEL_BUTTON_LABEL": "Cancel"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Assignment policy updated successfully",
|
||||
"ERROR_MESSAGE": "Failed to update assignment policy"
|
||||
},
|
||||
"INBOX_API": {
|
||||
"ADD": {
|
||||
"SUCCESS_MESSAGE": "Inbox added to policy successfully",
|
||||
"ERROR_MESSAGE": "Failed to add inbox to policy"
|
||||
},
|
||||
"REMOVE": {
|
||||
"SUCCESS_MESSAGE": "Inbox removed from policy successfully",
|
||||
"ERROR_MESSAGE": "Failed to remove inbox from policy"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Policy name:",
|
||||
"PLACEHOLDER": "Enter policy name"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description:",
|
||||
"PLACEHOLDER": "Enter description"
|
||||
},
|
||||
"STATUS": {
|
||||
"LABEL": "Status:",
|
||||
"PLACEHOLDER": "Select status",
|
||||
"ACTIVE": "Policy is active",
|
||||
"INACTIVE": "Policy is inactive"
|
||||
},
|
||||
"ASSIGNMENT_ORDER": {
|
||||
"LABEL": "Assignment order",
|
||||
"ROUND_ROBIN": {
|
||||
"LABEL": "Round robin",
|
||||
"DESCRIPTION": "Assign conversations evenly among agents."
|
||||
},
|
||||
"BALANCED": {
|
||||
"LABEL": "Balanced",
|
||||
"DESCRIPTION": "Assign conversations based on available capacity."
|
||||
}
|
||||
},
|
||||
"ASSIGNMENT_PRIORITY": {
|
||||
"LABEL": "Assignment priority",
|
||||
"EARLIEST_CREATED": {
|
||||
"LABEL": "Earliest created",
|
||||
"DESCRIPTION": "The conversation that was created first gets assigned first."
|
||||
},
|
||||
"LONGEST_WAITING": {
|
||||
"LABEL": "Longest waiting",
|
||||
"DESCRIPTION": "The conversation waiting the longest gets assigned first."
|
||||
}
|
||||
},
|
||||
"FAIR_DISTRIBUTION": {
|
||||
"LABEL": "Fair distribution policy",
|
||||
"DESCRIPTION": "Set the maximum number of conversations that can be assigned per agent within a time window to avoid overloading any one agent. This required field defaults to 100 conversations per hour.",
|
||||
"INPUT_MAX": "Assign max",
|
||||
"DURATION": "Conversations per agent in every"
|
||||
},
|
||||
"INBOXES": {
|
||||
"LABEL": "Added inboxes",
|
||||
"DESCRIPTION": "Add inboxes for which this policy will be applicable.",
|
||||
"ADD_BUTTON": "Add inbox",
|
||||
"DROPDOWN": {
|
||||
"SEARCH_PLACEHOLDER": "Search and select inboxes to add",
|
||||
"ADD_BUTTON": "Add"
|
||||
},
|
||||
"EMPTY_STATE": "No inboxes added to this policy, add an inbox to get started",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Inbox successfully added to policy",
|
||||
"ERROR_MESSAGE": "Failed to add inbox to policy"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_POLICY": {
|
||||
"TITLE": "Delete policy",
|
||||
"DESCRIPTION": "Are you sure you want to delete this policy? This action cannot be undone.",
|
||||
|
||||
@@ -3,6 +3,8 @@ import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
import AssignmentPolicyIndex from './Index.vue';
|
||||
import AgentAssignmentIndex from './pages/AgentAssignmentIndexPage.vue';
|
||||
import AgentAssignmentCreate from './pages/AgentAssignmentCreatePage.vue';
|
||||
import AgentAssignmentEdit from './pages/AgentAssignmentEditPage.vue';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -34,6 +36,24 @@ export default {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'assignment/create',
|
||||
name: 'agent_assignment_policy_create',
|
||||
component: AgentAssignmentCreate,
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2,
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'assignment/edit/:id',
|
||||
name: 'agent_assignment_policy_edit',
|
||||
component: AgentAssignmentEdit,
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2,
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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 = () => {
|
||||
router.push({
|
||||
name: 'assignment_policy_create',
|
||||
name: 'agent_assignment_policy_create',
|
||||
});
|
||||
};
|
||||
|
||||
const onClickEditPolicy = id => {
|
||||
router.push({
|
||||
name: 'agent_assignment_policy_edit',
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -106,6 +115,7 @@ onMounted(() => {
|
||||
v-bind="policy"
|
||||
:is-fetching-inboxes="inboxUiFlags.isFetching"
|
||||
@fetch-inboxes="handleFetchInboxes"
|
||||
@edit="onClickEditPolicy"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</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 { throwErrorMessage } from '../utils/api';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import snakecaseKeys from 'snakecase-keys';
|
||||
|
||||
export const state = {
|
||||
records: [],
|
||||
@@ -15,6 +16,7 @@ export const state = {
|
||||
},
|
||||
inboxUiFlags: {
|
||||
isFetching: false,
|
||||
isDeleting: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,7 +53,7 @@ export const actions = {
|
||||
try {
|
||||
const response = await AssignmentPoliciesAPI.show(policyId);
|
||||
const policy = camelcaseKeys(response.data);
|
||||
commit(types.EDIT_ASSIGNMENT_POLICY, policy);
|
||||
commit(types.SET_ASSIGNMENT_POLICY, policy);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
@@ -62,7 +64,9 @@ export const actions = {
|
||||
create: async function create({ commit }, policyObj) {
|
||||
commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const response = await AssignmentPoliciesAPI.create(policyObj);
|
||||
const response = await AssignmentPoliciesAPI.create(
|
||||
snakecaseKeys(policyObj)
|
||||
);
|
||||
commit(types.ADD_ASSIGNMENT_POLICY, camelcaseKeys(response.data));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -76,7 +80,10 @@ export const actions = {
|
||||
update: async function update({ commit }, { id, ...policyParams }) {
|
||||
commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await AssignmentPoliciesAPI.update(id, policyParams);
|
||||
const response = await AssignmentPoliciesAPI.update(
|
||||
id,
|
||||
snakecaseKeys(policyParams)
|
||||
);
|
||||
commit(types.EDIT_ASSIGNMENT_POLICY, camelcaseKeys(response.data));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -117,6 +124,68 @@ export const actions = {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setInboxPolicy: async function setInboxPolicy(
|
||||
{ commit },
|
||||
{ inboxId, policyId }
|
||||
) {
|
||||
try {
|
||||
const response = await AssignmentPoliciesAPI.setInboxPolicy(
|
||||
inboxId,
|
||||
policyId
|
||||
);
|
||||
commit(
|
||||
types.ADD_ASSIGNMENT_POLICIES_INBOXES,
|
||||
camelcaseKeys(response.data)
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getInboxPolicy: async function getInboxPolicy(_, { inboxId }) {
|
||||
try {
|
||||
const response = await AssignmentPoliciesAPI.getInboxPolicy(inboxId);
|
||||
return camelcaseKeys(response.data);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateInboxPolicy: async function updateInboxPolicy({ commit }, { policy }) {
|
||||
try {
|
||||
commit(types.EDIT_ASSIGNMENT_POLICY, policy);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
removeInboxPolicy: async function removeInboxPolicy(
|
||||
{ commit },
|
||||
{ policyId, inboxId }
|
||||
) {
|
||||
commit(types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, {
|
||||
isDeleting: true,
|
||||
});
|
||||
try {
|
||||
await AssignmentPoliciesAPI.removeInboxPolicy(inboxId);
|
||||
commit(types.DELETE_ASSIGNMENT_POLICIES_INBOXES, {
|
||||
policyId,
|
||||
inboxId,
|
||||
});
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
} finally {
|
||||
commit(types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, {
|
||||
isDeleting: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
@@ -128,8 +197,9 @@ export const mutations = {
|
||||
},
|
||||
|
||||
[types.SET_ASSIGNMENT_POLICIES]: MutationHelpers.set,
|
||||
[types.SET_ASSIGNMENT_POLICY]: MutationHelpers.setSingleRecord,
|
||||
[types.ADD_ASSIGNMENT_POLICY]: MutationHelpers.create,
|
||||
[types.EDIT_ASSIGNMENT_POLICY]: MutationHelpers.update,
|
||||
[types.EDIT_ASSIGNMENT_POLICY]: MutationHelpers.updateAttributes,
|
||||
[types.DELETE_ASSIGNMENT_POLICY]: MutationHelpers.destroy,
|
||||
|
||||
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG](_state, data) {
|
||||
@@ -138,13 +208,19 @@ export const mutations = {
|
||||
...data,
|
||||
};
|
||||
},
|
||||
|
||||
[types.SET_ASSIGNMENT_POLICIES_INBOXES](_state, { policyId, inboxes }) {
|
||||
const policy = _state.records.find(p => p.id === policyId);
|
||||
if (policy) {
|
||||
policy.inboxes = inboxes;
|
||||
}
|
||||
},
|
||||
[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](_state, { policyId, inboxId }) {
|
||||
const policy = _state.records.find(p => p.id === policyId);
|
||||
if (policy) {
|
||||
policy.inboxes = policy?.inboxes?.filter(inbox => inbox.id !== inboxId);
|
||||
}
|
||||
},
|
||||
[types.ADD_ASSIGNMENT_POLICIES_INBOXES]: MutationHelpers.updateAttributes,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -29,6 +29,9 @@ export const getters = {
|
||||
getInboxes($state) {
|
||||
return $state.records;
|
||||
},
|
||||
getAllInboxes($state) {
|
||||
return camelcaseKeys($state.records, { deep: true });
|
||||
},
|
||||
getWhatsAppTemplates: $state => inboxId => {
|
||||
const [inbox] = $state.records.filter(
|
||||
record => record.id === Number(inboxId)
|
||||
|
||||
@@ -3,12 +3,14 @@ import { actions } from '../../assignmentPolicies';
|
||||
import types from '../../../mutation-types';
|
||||
import assignmentPoliciesList, { camelCaseFixtures } from './fixtures';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import snakecaseKeys from 'snakecase-keys';
|
||||
|
||||
const commit = vi.fn();
|
||||
|
||||
global.axios = axios;
|
||||
vi.mock('axios');
|
||||
vi.mock('camelcase-keys');
|
||||
vi.mock('snakecase-keys');
|
||||
vi.mock('../../../utils/api');
|
||||
|
||||
describe('#actions', () => {
|
||||
@@ -56,7 +58,7 @@ describe('#actions', () => {
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(policyData);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: true }],
|
||||
[types.EDIT_ASSIGNMENT_POLICY, camelCasedPolicy],
|
||||
[types.SET_ASSIGNMENT_POLICY, camelCasedPolicy],
|
||||
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: false }],
|
||||
]);
|
||||
});
|
||||
@@ -77,12 +79,15 @@ describe('#actions', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const newPolicy = assignmentPoliciesList[0];
|
||||
const camelCasedData = camelCaseFixtures[0];
|
||||
const snakeCasedPolicy = { assignment_order: 'round_robin' };
|
||||
|
||||
axios.post.mockResolvedValue({ data: newPolicy });
|
||||
camelcaseKeys.mockReturnValue(camelCasedData);
|
||||
snakecaseKeys.mockReturnValue(snakeCasedPolicy);
|
||||
|
||||
const result = await actions.create({ commit }, newPolicy);
|
||||
|
||||
expect(snakecaseKeys).toHaveBeenCalledWith(newPolicy);
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true }],
|
||||
@@ -115,12 +120,15 @@ describe('#actions', () => {
|
||||
...camelCaseFixtures[0],
|
||||
name: 'Updated Policy',
|
||||
};
|
||||
const snakeCasedParams = { name: 'Updated Policy' };
|
||||
|
||||
axios.patch.mockResolvedValue({ data: responseData });
|
||||
camelcaseKeys.mockReturnValue(camelCasedData);
|
||||
snakecaseKeys.mockReturnValue(snakeCasedParams);
|
||||
|
||||
const result = await actions.update({ commit }, updateParams);
|
||||
|
||||
expect(snakecaseKeys).toHaveBeenCalledWith({ name: 'Updated Policy' });
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true }],
|
||||
@@ -211,4 +219,108 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setInboxPolicy', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const responseData = { success: true, policy_id: 2 };
|
||||
const camelCasedData = { success: true, policyId: 2 };
|
||||
|
||||
axios.post.mockResolvedValue({ data: responseData });
|
||||
camelcaseKeys.mockReturnValue(camelCasedData);
|
||||
|
||||
const result = await actions.setInboxPolicy(
|
||||
{ commit },
|
||||
{ inboxId: 1, policyId: 2 }
|
||||
);
|
||||
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.ADD_ASSIGNMENT_POLICIES_INBOXES, camelCasedData],
|
||||
]);
|
||||
expect(result).toEqual(responseData);
|
||||
});
|
||||
|
||||
it('throws error if API fails', async () => {
|
||||
axios.post.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
await expect(
|
||||
actions.setInboxPolicy({ commit }, { inboxId: 1, policyId: 2 })
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getInboxPolicy', () => {
|
||||
it('returns camelCased response data if API is success', async () => {
|
||||
const responseData = { policy_id: 1, name: 'Round Robin' };
|
||||
const camelCasedData = { policyId: 1, name: 'Round Robin' };
|
||||
|
||||
axios.get.mockResolvedValue({ data: responseData });
|
||||
camelcaseKeys.mockReturnValue(camelCasedData);
|
||||
|
||||
const result = await actions.getInboxPolicy({}, { inboxId: 1 });
|
||||
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
||||
expect(result).toEqual(camelCasedData);
|
||||
});
|
||||
|
||||
it('throws error if API fails', async () => {
|
||||
axios.get.mockRejectedValue(new Error('Not found'));
|
||||
|
||||
await expect(
|
||||
actions.getInboxPolicy({}, { inboxId: 999 })
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateInboxPolicy', () => {
|
||||
it('commits EDIT_ASSIGNMENT_POLICY mutation', async () => {
|
||||
const policy = { id: 1, name: 'Updated Policy' };
|
||||
|
||||
await actions.updateInboxPolicy({ commit }, { policy });
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.EDIT_ASSIGNMENT_POLICY, policy],
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws error if commit fails', async () => {
|
||||
commit.mockImplementation(() => {
|
||||
throw new Error('Commit failed');
|
||||
});
|
||||
|
||||
await expect(
|
||||
actions.updateInboxPolicy({ commit }, { policy: {} })
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removeInboxPolicy', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const policyId = 1;
|
||||
const inboxId = 2;
|
||||
|
||||
axios.delete.mockResolvedValue({});
|
||||
|
||||
await actions.removeInboxPolicy({ commit }, { policyId, inboxId });
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: true }],
|
||||
[types.DELETE_ASSIGNMENT_POLICIES_INBOXES, { policyId, inboxId }],
|
||||
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends correct actions if API fails', async () => {
|
||||
axios.delete.mockRejectedValue(new Error('Not found'));
|
||||
|
||||
await expect(
|
||||
actions.removeInboxPolicy({ commit }, { policyId: 1, inboxId: 999 })
|
||||
).rejects.toThrow(Error);
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: true }],
|
||||
[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,10 +32,12 @@ describe('#getters', () => {
|
||||
const state = {
|
||||
inboxUiFlags: {
|
||||
isFetching: false,
|
||||
isDeleting: false,
|
||||
},
|
||||
};
|
||||
expect(getters.getInboxUiFlags(state)).toEqual({
|
||||
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', () => {
|
||||
it('adds new policy to empty records', () => {
|
||||
const state = { records: [] };
|
||||
@@ -264,4 +282,104 @@ describe('#mutations', () => {
|
||||
expect(state).toEqual(originalState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_ASSIGNMENT_POLICIES_INBOXES', () => {
|
||||
it('removes inbox from policy', () => {
|
||||
const mockInboxes = [
|
||||
{ id: 1, name: 'Support Inbox' },
|
||||
{ id: 2, name: 'Sales Inbox' },
|
||||
{ id: 3, name: 'Marketing Inbox' },
|
||||
];
|
||||
|
||||
const state = {
|
||||
records: [
|
||||
{ id: 1, name: 'Policy 1', inboxes: mockInboxes },
|
||||
{ id: 2, name: 'Policy 2', inboxes: [] },
|
||||
],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, {
|
||||
policyId: 1,
|
||||
inboxId: 2,
|
||||
});
|
||||
|
||||
expect(state.records[0].inboxes).toEqual([
|
||||
{ id: 1, name: 'Support Inbox' },
|
||||
{ id: 3, name: 'Marketing Inbox' },
|
||||
]);
|
||||
expect(state.records[1].inboxes).toEqual([]);
|
||||
});
|
||||
|
||||
it('does nothing if policy not found', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{ id: 1, name: 'Policy 1', inboxes: [{ id: 1, name: 'Test' }] },
|
||||
],
|
||||
};
|
||||
|
||||
const originalState = JSON.parse(JSON.stringify(state));
|
||||
|
||||
mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, {
|
||||
policyId: 999,
|
||||
inboxId: 1,
|
||||
});
|
||||
|
||||
expect(state).toEqual(originalState);
|
||||
});
|
||||
|
||||
it('does nothing if inbox not found in policy', () => {
|
||||
const mockInboxes = [{ id: 1, name: 'Support Inbox' }];
|
||||
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'Policy 1', inboxes: mockInboxes }],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, {
|
||||
policyId: 1,
|
||||
inboxId: 999,
|
||||
});
|
||||
|
||||
expect(state.records[0].inboxes).toEqual(mockInboxes);
|
||||
});
|
||||
|
||||
it('handles policy with no inboxes', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'Policy 1' }],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_ASSIGNMENT_POLICIES_INBOXES](state, {
|
||||
policyId: 1,
|
||||
inboxId: 1,
|
||||
});
|
||||
|
||||
expect(state.records[0]).toEqual({ id: 1, name: 'Policy 1' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ADD_ASSIGNMENT_POLICIES_INBOXES', () => {
|
||||
it('updates policy attributes using MutationHelpers.updateAttributes', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{ id: 1, name: 'Policy 1', assignedInboxCount: 2 },
|
||||
{ id: 2, name: 'Policy 2', assignedInboxCount: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
const updatedPolicy = {
|
||||
id: 1,
|
||||
name: 'Policy 1',
|
||||
assignedInboxCount: 3,
|
||||
inboxes: [{ id: 1, name: 'New Inbox' }],
|
||||
};
|
||||
|
||||
mutations[types.ADD_ASSIGNMENT_POLICIES_INBOXES](state, updatedPolicy);
|
||||
|
||||
expect(state.records[0]).toEqual(updatedPolicy);
|
||||
expect(state.records[1]).toEqual({
|
||||
id: 2,
|
||||
name: 'Policy 2',
|
||||
assignedInboxCount: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -352,10 +352,13 @@ export default {
|
||||
// Assignment Policies
|
||||
SET_ASSIGNMENT_POLICIES_UI_FLAG: 'SET_ASSIGNMENT_POLICIES_UI_FLAG',
|
||||
SET_ASSIGNMENT_POLICIES: 'SET_ASSIGNMENT_POLICIES',
|
||||
SET_ASSIGNMENT_POLICY: 'SET_ASSIGNMENT_POLICY',
|
||||
ADD_ASSIGNMENT_POLICY: 'ADD_ASSIGNMENT_POLICY',
|
||||
EDIT_ASSIGNMENT_POLICY: 'EDIT_ASSIGNMENT_POLICY',
|
||||
DELETE_ASSIGNMENT_POLICY: 'DELETE_ASSIGNMENT_POLICY',
|
||||
SET_ASSIGNMENT_POLICIES_INBOXES: 'SET_ASSIGNMENT_POLICIES_INBOXES',
|
||||
SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG:
|
||||
'SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG',
|
||||
DELETE_ASSIGNMENT_POLICIES_INBOXES: 'DELETE_ASSIGNMENT_POLICIES_INBOXES',
|
||||
ADD_ASSIGNMENT_POLICIES_INBOXES: 'ADD_ASSIGNMENT_POLICIES_INBOXES',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user