mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: Agent capacity policy Create/Edit pages (#12424)
# Pull Request Template ## Description Fixes https://linear.app/chatwoot/issue/CW-5573/feat-createedit-agent-capacity-policy-page ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Loom video https://www.loom.com/share/8de9e3c5d8824cd998d242636540dd18?sid=1314536f-c8d6-41fd-8139-cae9bf94f942 ### Screenshots **Light mode** <img width="1666" height="1225" alt="image" src="https://github.com/user-attachments/assets/7e6d83a4-ce02-47a7-91f6-87745f8f5549" /> <img width="1666" height="1225" alt="image" src="https://github.com/user-attachments/assets/7dd1f840-2e25-4365-aa1d-ed9dac13385a" /> **Dark mode** <img width="1666" height="1225" alt="image" src="https://github.com/user-attachments/assets/0c787095-7146-4fb3-a61a-e2232973bcba" /> <img width="1666" height="1225" alt="image" src="https://github.com/user-attachments/assets/481c21fd-03b5-4c1f-b59e-7f8c8017f9ce" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { useToggle } from '@vueuse/core';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
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';
|
||||
@@ -36,8 +37,8 @@ const filteredItems = computed(() => {
|
||||
return picoSearch(props.items, query, ['name']);
|
||||
});
|
||||
|
||||
const handleAdd = inbox => {
|
||||
emit('add', inbox);
|
||||
const handleAdd = item => {
|
||||
emit('add', item);
|
||||
togglePopover(false);
|
||||
};
|
||||
|
||||
@@ -82,21 +83,35 @@ const handleClickOutside = () => {
|
||||
<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"
|
||||
class="flex gap-3 min-w-0 w-full py-4 px-3 hover:bg-n-alpha-2 cursor-pointer"
|
||||
:class="{ 'items-center': item.color, 'items-start': !item.color }"
|
||||
@click="handleAdd(item)"
|
||||
>
|
||||
<Icon
|
||||
v-if="item.icon"
|
||||
:icon="item.icon"
|
||||
class="size-4 text-n-slate-12 flex-shrink-0 mt-0.5"
|
||||
class="size-2 text-n-slate-12 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<span
|
||||
v-else-if="item.color"
|
||||
:style="{ backgroundColor: item.color }"
|
||||
class="size-3 rounded-sm"
|
||||
/>
|
||||
<Avatar
|
||||
v-else
|
||||
:title="item.name"
|
||||
:src="item.avatarUrl"
|
||||
:name="item.name"
|
||||
:size="20"
|
||||
rounded-full
|
||||
/>
|
||||
<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"
|
||||
:title="item.name || item.title"
|
||||
class="text-sm text-n-slate-12 truncate min-w-0"
|
||||
>
|
||||
{{ item.name }}
|
||||
{{ item.name || item.title }}
|
||||
</span>
|
||||
<span
|
||||
v-if="item.id"
|
||||
|
@@ -65,6 +65,7 @@ watch(
|
||||
() => {
|
||||
emit('validationChange', {
|
||||
isValid: isValid.value,
|
||||
section: 'baseInfo',
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -108,7 +109,7 @@ watch(
|
||||
</div>
|
||||
|
||||
<!-- Status Field -->
|
||||
<div class="flex items-center gap-6">
|
||||
<div v-if="statusLabel" class="flex items-center gap-6">
|
||||
<WithLabel
|
||||
:label="statusLabel"
|
||||
name="enabled"
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
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';
|
||||
@@ -32,12 +33,14 @@ const handleDelete = itemId => {
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
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"
|
||||
class="custom-dashed-border flex items-center justify-center py-6 w-full"
|
||||
>
|
||||
{{ emptyStateMessage }}
|
||||
</span>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ emptyStateMessage }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col divide-y divide-n-weak">
|
||||
<div
|
||||
v-for="item in items"
|
||||
@@ -50,7 +53,14 @@ const handleDelete = itemId => {
|
||||
:icon="item.icon"
|
||||
class="size-4 text-n-slate-12 flex-shrink-0"
|
||||
/>
|
||||
|
||||
<Avatar
|
||||
v-else
|
||||
:title="item.name"
|
||||
:src="item.avatarUrl"
|
||||
:name="item.name"
|
||||
:size="20"
|
||||
rounded-full
|
||||
/>
|
||||
<span class="text-sm text-n-slate-12 truncate min-w-0">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
|
@@ -0,0 +1,149 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import AddDataDropdown from 'dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue';
|
||||
import LabelItem from 'dashboard/components-next/Label/LabelItem.vue';
|
||||
import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
|
||||
import { DURATION_UNITS } from 'dashboard/components-next/input/constants';
|
||||
|
||||
const props = defineProps({
|
||||
tagsList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const excludedLabels = defineModel('excludedLabels', {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
const excludeOlderThanMinutes = defineModel('excludeOlderThanMinutes', {
|
||||
type: Number,
|
||||
default: 10,
|
||||
});
|
||||
|
||||
// Duration limits: 10 minutes to 999 days (in minutes)
|
||||
const MIN_DURATION_MINUTES = 10;
|
||||
const MAX_DURATION_MINUTES = 1438560; // 999 days * 24 hours * 60 minutes
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const hoveredLabel = ref(null);
|
||||
const windowUnit = ref(DURATION_UNITS.MINUTES);
|
||||
|
||||
const addedTags = computed(() =>
|
||||
props.tagsList
|
||||
.filter(label => excludedLabels.value.includes(label.name))
|
||||
.map(label => ({ id: label.id, title: label.name, ...label }))
|
||||
);
|
||||
|
||||
const filteredTags = computed(() =>
|
||||
props.tagsList.filter(
|
||||
label => !addedTags.value.some(tag => tag.id === label.id)
|
||||
)
|
||||
);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const onClickAddTag = tag => {
|
||||
excludedLabels.value = [...excludedLabels.value, tag.name];
|
||||
};
|
||||
|
||||
const onClickRemoveTag = tag => {
|
||||
excludedLabels.value = excludedLabels.value.filter(
|
||||
name => name !== tag.title
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
windowUnit.value = detectUnit(excludeOlderThanMinutes.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="py-4 flex-col flex gap-6">
|
||||
<div class="flex flex-col items-start gap-1 py-1">
|
||||
<label class="text-sm font-medium text-n-slate-12 py-1">
|
||||
{{
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.LABEL'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
<p class="mb-0 text-n-slate-11 text-sm">
|
||||
{{
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.DESCRIPTION'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-start gap-4">
|
||||
<label class="text-sm font-medium text-n-slate-12 py-1">
|
||||
{{
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.TAGS.LABEL'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
<div
|
||||
class="flex items-start gap-2 flex-wrap"
|
||||
@mouseleave="hoveredLabel = null"
|
||||
>
|
||||
<LabelItem
|
||||
v-for="tag in addedTags"
|
||||
:key="tag.id"
|
||||
:label="tag"
|
||||
:is-hovered="hoveredLabel === tag.id"
|
||||
class="h-8"
|
||||
@remove="onClickRemoveTag"
|
||||
@hover="hoveredLabel = tag.id"
|
||||
/>
|
||||
<AddDataDropdown
|
||||
:label="
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.TAGS.ADD_TAG'
|
||||
)
|
||||
"
|
||||
:search-placeholder="
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.TAGS.DROPDOWN.SEARCH_PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:items="filteredTags"
|
||||
class="[&>button]:!text-n-blue-text"
|
||||
@add="onClickAddTag"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-start gap-4">
|
||||
<label class="text-sm font-medium text-n-slate-12 py-1">
|
||||
{{
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.FORM.EXCLUSION_RULES.DURATION.LABEL'
|
||||
)
|
||||
}}
|
||||
</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:unit="windowUnit"
|
||||
v-model:model-value="excludeOlderThanMinutes"
|
||||
:min="MIN_DURATION_MINUTES"
|
||||
:max="MAX_DURATION_MINUTES"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,163 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import AddDataDropdown from 'dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue';
|
||||
|
||||
const props = defineProps({
|
||||
inboxList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isFetching: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['delete', 'add', 'update']);
|
||||
|
||||
const inboxCapacityLimits = defineModel('inboxCapacityLimits', {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY';
|
||||
const DEFAULT_CONVERSATION_LIMIT = 10;
|
||||
const MIN_CONVERSATION_LIMIT = 1;
|
||||
const MAX_CONVERSATION_LIMIT = 100000;
|
||||
|
||||
const selectedInboxIds = computed(
|
||||
() => new Set(inboxCapacityLimits.value.map(limit => limit.inboxId))
|
||||
);
|
||||
|
||||
const availableInboxes = computed(() =>
|
||||
props.inboxList.filter(
|
||||
inbox => inbox && !selectedInboxIds.value.has(inbox.id)
|
||||
)
|
||||
);
|
||||
|
||||
const isLimitValid = limit => {
|
||||
return (
|
||||
limit.conversationLimit >= MIN_CONVERSATION_LIMIT &&
|
||||
limit.conversationLimit <= MAX_CONVERSATION_LIMIT
|
||||
);
|
||||
};
|
||||
|
||||
const inboxMap = computed(
|
||||
() => new Map(props.inboxList.map(inbox => [inbox.id, inbox]))
|
||||
);
|
||||
|
||||
const handleAddInbox = inbox => {
|
||||
emit('add', {
|
||||
inboxId: inbox.id,
|
||||
conversationLimit: DEFAULT_CONVERSATION_LIMIT,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveLimit = limitId => {
|
||||
emit('delete', limitId);
|
||||
};
|
||||
|
||||
const handleLimitChange = limit => {
|
||||
if (isLimitValid(limit)) {
|
||||
emit('update', limit);
|
||||
}
|
||||
};
|
||||
|
||||
const getInboxName = inboxId => {
|
||||
return inboxMap.value.get(inboxId)?.name || '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="py-4 flex-col flex gap-3">
|
||||
<div class="flex items-center w-full gap-8 justify-between pt-1 pb-3">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.LABEL`) }}
|
||||
</label>
|
||||
|
||||
<AddDataDropdown
|
||||
:label="t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.ADD_BUTTON`)"
|
||||
:search-placeholder="
|
||||
t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.FIELD.SELECT_INBOX`)
|
||||
"
|
||||
:items="availableInboxes"
|
||||
@add="handleAddInbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center py-3 w-full text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="!inboxCapacityLimits.length"
|
||||
class="custom-dashed-border flex items-center justify-center py-6 w-full"
|
||||
>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.EMPTY_STATE`) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex-col flex gap-3">
|
||||
<div
|
||||
v-for="(limit, index) in inboxCapacityLimits"
|
||||
:key="limit.id || `temp-${index}`"
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<div
|
||||
class="flex items-start rounded-lg outline-1 outline cursor-not-allowed text-n-slate-11 outline-n-weak py-2.5 px-3 text-sm w-full"
|
||||
>
|
||||
{{ getInboxName(limit.inboxId) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="py-2.5 px-3 rounded-lg gap-2 outline outline-1 flex-shrink-0 flex items-center"
|
||||
:class="[
|
||||
!isLimitValid(limit) ? 'outline-n-ruby-8' : 'outline-n-weak',
|
||||
]"
|
||||
>
|
||||
<label class="text-sm text-n-slate-12 ltr:pr-2 rtl:pl-2">
|
||||
{{
|
||||
t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.FIELD.MAX_CONVERSATIONS`)
|
||||
}}
|
||||
</label>
|
||||
|
||||
<div class="h-5 w-px bg-n-weak" />
|
||||
|
||||
<input
|
||||
v-model.number="limit.conversationLimit"
|
||||
type="number"
|
||||
:min="MIN_CONVERSATION_LIMIT"
|
||||
:max="MAX_CONVERSATION_LIMIT"
|
||||
class="reset-base bg-transparent focus:outline-none max-w-20 text-sm"
|
||||
:class="[
|
||||
!isLimitValid(limit)
|
||||
? 'placeholder:text-n-ruby-9 !text-n-ruby-9'
|
||||
: 'placeholder:text-n-slate-10 text-n-slate-12',
|
||||
]"
|
||||
:placeholder="
|
||||
t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.FIELD.SET_LIMIT`)
|
||||
"
|
||||
@blur="handleLimitChange(limit)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
slate
|
||||
icon="i-lucide-trash"
|
||||
class="flex-shrink-0"
|
||||
@click="handleRemoveLimit(limit.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -34,6 +34,29 @@ const mockInboxes = [
|
||||
},
|
||||
];
|
||||
|
||||
const mockTags = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'urgent',
|
||||
color: '#ff4757',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'bug',
|
||||
color: '#ff6b6b',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'feature-request',
|
||||
color: '#4834d4',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'documentation',
|
||||
color: '#26de81',
|
||||
},
|
||||
];
|
||||
|
||||
const handleAdd = item => {
|
||||
console.log('Add item:', item);
|
||||
};
|
||||
@@ -42,9 +65,9 @@ const handleAdd = item => {
|
||||
<template>
|
||||
<Story
|
||||
title="Components/AgentManagementPolicy/AddDataDropdown"
|
||||
:layout="{ type: 'grid', width: '400px' }"
|
||||
:layout="{ type: 'grid', width: '500px' }"
|
||||
>
|
||||
<Variant title="Basic Usage">
|
||||
<Variant title="Basic Usage - Inboxes">
|
||||
<div class="p-8 bg-n-background flex gap-4 h-[400px] items-start">
|
||||
<AddDataDropdown
|
||||
label="Add Inbox"
|
||||
@@ -54,5 +77,16 @@ const handleAdd = item => {
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Basic Usage - Tags">
|
||||
<div class="p-8 bg-n-background flex gap-4 h-[400px] items-start">
|
||||
<AddDataDropdown
|
||||
label="Add Tag"
|
||||
search-placeholder="Search tags..."
|
||||
:items="mockTags"
|
||||
@add="handleAdd"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
@@ -22,6 +22,21 @@ const mockItems = [
|
||||
},
|
||||
];
|
||||
|
||||
const mockAgentList = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
avatarUrl: 'https://i.pravatar.cc/150?img=1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jane Smith',
|
||||
email: 'jane.smith@example.com',
|
||||
avatarUrl: 'https://i.pravatar.cc/150?img=2',
|
||||
},
|
||||
];
|
||||
|
||||
const handleDelete = itemId => {
|
||||
console.log('Delete item:', itemId);
|
||||
};
|
||||
@@ -30,7 +45,7 @@ const handleDelete = itemId => {
|
||||
<template>
|
||||
<Story
|
||||
title="Components/AgentManagementPolicy/DataTable"
|
||||
:layout="{ type: 'grid', width: '600px' }"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="With Data">
|
||||
<div class="p-8 bg-n-background">
|
||||
@@ -42,6 +57,16 @@ const handleDelete = itemId => {
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Agents">
|
||||
<div class="p-8 bg-n-background">
|
||||
<DataTable
|
||||
:items="mockAgentList"
|
||||
:is-fetching="false"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Loading State">
|
||||
<div class="p-8 bg-n-background">
|
||||
<DataTable :items="[]" is-fetching @delete="handleDelete" />
|
||||
|
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import ExclusionRules from '../ExclusionRules.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const mockTagsList = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'urgent',
|
||||
color: '#ff4757',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'bug',
|
||||
color: '#ff6b6b',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'feature-request',
|
||||
color: '#4834d4',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'documentation',
|
||||
color: '#26de81',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'enhancement',
|
||||
color: '#2ed573',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'question',
|
||||
color: '#ffa502',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'duplicate',
|
||||
color: '#747d8c',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'wontfix',
|
||||
color: '#57606f',
|
||||
},
|
||||
];
|
||||
|
||||
const excludedLabelsBasic = ref([]);
|
||||
const excludeOlderThanHoursBasic = ref(10);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/AgentManagementPolicy/ExclusionRules"
|
||||
:layout="{ type: 'grid', width: '1200px' }"
|
||||
>
|
||||
<Variant title="Basic Usage">
|
||||
<div class="p-8 bg-n-background h-[600px]">
|
||||
<ExclusionRules
|
||||
v-model:excluded-labels="excludedLabelsBasic"
|
||||
v-model:exclude-older-than-minutes="excludeOlderThanHoursBasic"
|
||||
:tags-list="mockTagsList"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
@@ -0,0 +1,108 @@
|
||||
<script setup>
|
||||
import InboxCapacityLimits from '../InboxCapacityLimits.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const mockInboxList = [
|
||||
{
|
||||
value: 1,
|
||||
label: 'Website Support',
|
||||
icon: 'i-lucide-globe',
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: 'Email Support',
|
||||
icon: 'i-lucide-mail',
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: 'WhatsApp Business',
|
||||
icon: 'i-lucide-message-circle',
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
label: 'Facebook Messenger',
|
||||
icon: 'i-lucide-facebook',
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
label: 'Twitter DM',
|
||||
icon: 'i-lucide-twitter',
|
||||
},
|
||||
{
|
||||
value: 6,
|
||||
label: 'Telegram',
|
||||
icon: 'i-lucide-send',
|
||||
},
|
||||
];
|
||||
|
||||
const inboxCapacityLimitsEmpty = ref([]);
|
||||
const inboxCapacityLimitsNew = ref([
|
||||
{ id: 1, inboxId: 1, conversationLimit: 5 },
|
||||
{ inboxId: null, conversationLimit: null },
|
||||
]);
|
||||
|
||||
const handleDelete = id => {
|
||||
console.log('Delete capacity limit:', id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/AgentManagementPolicy/InboxCapacityLimits"
|
||||
:layout="{ type: 'grid', width: '900px' }"
|
||||
>
|
||||
<Variant title="Empty State">
|
||||
<div class="p-8 bg-n-background">
|
||||
<InboxCapacityLimits
|
||||
v-model:inbox-capacity-limits="inboxCapacityLimitsEmpty"
|
||||
:inbox-list="mockInboxList"
|
||||
:is-fetching="false"
|
||||
:is-updating="false"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Loading State">
|
||||
<div class="p-8 bg-n-background">
|
||||
<InboxCapacityLimits
|
||||
v-model:inbox-capacity-limits="inboxCapacityLimitsEmpty"
|
||||
:inbox-list="mockInboxList"
|
||||
is-fetching
|
||||
:is-updating="false"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="With New Row and existing data">
|
||||
<div class="p-8 bg-n-background">
|
||||
<InboxCapacityLimits
|
||||
v-model:inbox-capacity-limits="inboxCapacityLimitsNew"
|
||||
:inbox-list="mockInboxList"
|
||||
:is-fetching="false"
|
||||
:is-updating="false"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Interactive Demo">
|
||||
<div class="p-8 bg-n-background">
|
||||
<InboxCapacityLimits
|
||||
v-model:inbox-capacity-limits="inboxCapacityLimitsEmpty"
|
||||
:inbox-list="mockInboxList"
|
||||
:is-fetching="false"
|
||||
:is-updating="false"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<div class="mt-4 p-4 bg-n-alpha-2 rounded-lg">
|
||||
<h4 class="text-sm font-medium mb-2">Current Limits:</h4>
|
||||
<pre class="text-xs">{{
|
||||
JSON.stringify(inboxCapacityLimitsEmpty, null, 2)
|
||||
}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
@@ -86,8 +86,8 @@ const handleLabelAction = async ({ value }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveLabel = labelId => {
|
||||
return handleLabelAction({ value: labelId });
|
||||
const handleRemoveLabel = label => {
|
||||
return handleLabelAction({ value: label.id });
|
||||
};
|
||||
|
||||
watch(
|
||||
|
@@ -15,7 +15,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['remove', 'hover']);
|
||||
|
||||
const handleRemoveLabel = () => {
|
||||
emit('remove', props.label?.id);
|
||||
emit('remove', props.label);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
@@ -45,6 +45,7 @@ const handleMouseEnter = () => {
|
||||
<Button
|
||||
class="transition-opacity duration-200 !h-7 ltr:rounded-r-md rtl:rounded-l-md ltr:rounded-l-none rtl:rounded-r-none w-6 bg-transparent"
|
||||
:class="{ 'opacity-0': !isHovered, 'opacity-100': isHovered }"
|
||||
type="button"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
|
@@ -579,6 +579,92 @@
|
||||
},
|
||||
"NO_RECORDS_FOUND": "No agent capacity policies found"
|
||||
},
|
||||
"CREATE": {
|
||||
"HEADER": {
|
||||
"TITLE": "Create agent capacity policy"
|
||||
},
|
||||
"CREATE_BUTTON": "Create policy",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Agent capacity policy created successfully",
|
||||
"ERROR_MESSAGE": "Failed to create agent capacity policy"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"HEADER": {
|
||||
"TITLE": "Edit agent capacity policy"
|
||||
},
|
||||
"EDIT_BUTTON": "Update policy",
|
||||
"CONFIRM_ADD_AGENT_DIALOG": {
|
||||
"TITLE": "Add agent",
|
||||
"DESCRIPTION": "{agentName} 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": "Agent capacity policy updated successfully",
|
||||
"ERROR_MESSAGE": "Failed to update agent capacity policy"
|
||||
},
|
||||
"AGENT_API": {
|
||||
"ADD": {
|
||||
"SUCCESS_MESSAGE": "Agent added to policy successfully",
|
||||
"ERROR_MESSAGE": "Failed to add agent to policy"
|
||||
},
|
||||
"REMOVE": {
|
||||
"SUCCESS_MESSAGE": "Agent removed from policy successfully",
|
||||
"ERROR_MESSAGE": "Failed to remove agent from policy"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Policy name:",
|
||||
"PLACEHOLDER": "Enter policy name"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description:",
|
||||
"PLACEHOLDER": "Enter description"
|
||||
},
|
||||
"INBOX_CAPACITY_LIMIT": {
|
||||
"LABEL": "Inbox capacity limits",
|
||||
"ADD_BUTTON": "Add inbox",
|
||||
"FIELD": {
|
||||
"SELECT_INBOX": "Select inbox",
|
||||
"MAX_CONVERSATIONS": "Max conversations",
|
||||
"SET_LIMIT": "Set limit"
|
||||
},
|
||||
"EMPTY_STATE": "No inbox limit set"
|
||||
},
|
||||
"EXCLUSION_RULES": {
|
||||
"LABEL": "Exclusion rules",
|
||||
"DESCRIPTION": "Conversations that satisfy the following conditions would not count towards agent capacity",
|
||||
"TAGS": {
|
||||
"LABEL": "Exclude conversations tagged with specific labels",
|
||||
"ADD_TAG": "add tag",
|
||||
"DROPDOWN": {
|
||||
"SEARCH_PLACEHOLDER": "Search and select tags to add"
|
||||
},
|
||||
"EMPTY_STATE": "No tags added to this policy."
|
||||
},
|
||||
"DURATION": {
|
||||
"LABEL": "Exclude conversations older than a specified duration",
|
||||
"PLACEHOLDER": "Set time"
|
||||
}
|
||||
},
|
||||
"USERS": {
|
||||
"LABEL": "Assigned agents",
|
||||
"DESCRIPTION": "Add agents for which this policy will be applicable.",
|
||||
"ADD_BUTTON": "Add agent",
|
||||
"DROPDOWN": {
|
||||
"SEARCH_PLACEHOLDER": "Search and select agents to add",
|
||||
"ADD_BUTTON": "Add"
|
||||
},
|
||||
"EMPTY_STATE": "No agents added",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Agent successfully added to policy",
|
||||
"ERROR_MESSAGE": "Failed to add agent to policy"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_POLICY": {
|
||||
"SUCCESS_MESSAGE": "Agent capacity policy deleted successfully",
|
||||
"ERROR_MESSAGE": "Failed to delete agent capacity policy"
|
||||
|
@@ -6,6 +6,8 @@ import AgentAssignmentIndex from './pages/AgentAssignmentIndexPage.vue';
|
||||
import AgentAssignmentCreate from './pages/AgentAssignmentCreatePage.vue';
|
||||
import AgentAssignmentEdit from './pages/AgentAssignmentEditPage.vue';
|
||||
import AgentCapacityIndex from './pages/AgentCapacityIndexPage.vue';
|
||||
import AgentCapacityCreate from './pages/AgentCapacityCreatePage.vue';
|
||||
import AgentCapacityEdit from './pages/AgentCapacityEditPage.vue';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -64,6 +66,24 @@ export default {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'capacity/create',
|
||||
name: 'agent_capacity_policy_create',
|
||||
component: AgentCapacityCreate,
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2,
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'capacity/edit/:id',
|
||||
name: 'agent_capacity_policy_edit',
|
||||
component: AgentCapacityEdit,
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2,
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@@ -0,0 +1,87 @@
|
||||
<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 AgentCapacityPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const formRef = ref(null);
|
||||
const uiFlags = useMapGetter('agentCapacityPolicies/getUIFlags');
|
||||
const labelsList = useMapGetter('labels/getLabels');
|
||||
|
||||
const allLabels = computed(() =>
|
||||
labelsList.value?.map(({ title, color, id }) => ({
|
||||
id,
|
||||
name: title,
|
||||
color,
|
||||
}))
|
||||
);
|
||||
|
||||
const breadcrumbItems = computed(() => [
|
||||
{
|
||||
label: t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.INDEX.HEADER.TITLE'),
|
||||
routeName: 'agent_capacity_policy_index',
|
||||
},
|
||||
{
|
||||
label: t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.CREATE.HEADER.TITLE'),
|
||||
},
|
||||
]);
|
||||
|
||||
const handleBreadcrumbClick = item => {
|
||||
router.push({
|
||||
name: item.routeName,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async formState => {
|
||||
try {
|
||||
const policy = await store.dispatch(
|
||||
'agentCapacityPolicies/create',
|
||||
formState
|
||||
);
|
||||
useAlert(
|
||||
t('ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY.CREATE.API.SUCCESS_MESSAGE')
|
||||
);
|
||||
formRef.value?.resetForm();
|
||||
|
||||
router.push({
|
||||
name: 'agent_capacity_policy_edit',
|
||||
params: {
|
||||
id: policy.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
t('ASSIGNMENT_POLICY.AGENT_CAPACITY_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>
|
||||
<AgentCapacityPolicyForm
|
||||
ref="formRef"
|
||||
mode="CREATE"
|
||||
:is-loading="uiFlags.isCreating"
|
||||
:label-list="allLabels"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
@@ -0,0 +1,179 @@
|
||||
<script setup>
|
||||
import { computed, watch, onMounted } 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 camelcaseKeys from 'camelcase-keys';
|
||||
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||
import AgentCapacityPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue';
|
||||
|
||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const uiFlags = useMapGetter('agentCapacityPolicies/getUIFlags');
|
||||
const usersUiFlags = useMapGetter('agentCapacityPolicies/getUsersUIFlags');
|
||||
const selectedPolicyById = useMapGetter(
|
||||
'agentCapacityPolicies/getAgentCapacityPolicyById'
|
||||
);
|
||||
const agentsList = useMapGetter('agents/getAgents');
|
||||
const labelsList = useMapGetter('labels/getLabels');
|
||||
const inboxes = useMapGetter('inboxes/getAllInboxes');
|
||||
const inboxesUiFlags = useMapGetter('inboxes/getUIFlags');
|
||||
|
||||
const routeId = computed(() => route.params.id);
|
||||
const selectedPolicy = computed(() => selectedPolicyById.value(routeId.value));
|
||||
const selectedPolicyId = computed(() => selectedPolicy.value?.id);
|
||||
|
||||
const breadcrumbItems = computed(() => [
|
||||
{
|
||||
label: t(`${BASE_KEY}.INDEX.HEADER.TITLE`),
|
||||
routeName: 'agent_capacity_policy_index',
|
||||
},
|
||||
{ label: t(`${BASE_KEY}.EDIT.HEADER.TITLE`) },
|
||||
]);
|
||||
|
||||
const buildList = items =>
|
||||
items?.map(({ name, title, id, email, avatarUrl, thumbnail, color }) => ({
|
||||
name: name || title,
|
||||
id,
|
||||
email,
|
||||
avatarUrl: avatarUrl || thumbnail,
|
||||
color,
|
||||
})) || [];
|
||||
|
||||
const policyUsers = computed(() => buildList(selectedPolicy.value?.users));
|
||||
|
||||
const allAgents = computed(() =>
|
||||
buildList(camelcaseKeys(agentsList.value)).filter(
|
||||
agent => !policyUsers.value?.some(user => user.id === agent.id)
|
||||
)
|
||||
);
|
||||
|
||||
const allLabels = computed(() => buildList(labelsList.value));
|
||||
|
||||
const allInboxes = computed(() => buildList(inboxes.value));
|
||||
|
||||
const formData = computed(() => ({
|
||||
name: selectedPolicy.value?.name || '',
|
||||
description: selectedPolicy.value?.description || '',
|
||||
exclusionRules: {
|
||||
excludedLabels: [
|
||||
...(selectedPolicy.value?.exclusionRules?.excludedLabels || []),
|
||||
],
|
||||
excludeOlderThanHours:
|
||||
selectedPolicy.value?.exclusionRules?.excludeOlderThanHours || 10,
|
||||
},
|
||||
inboxCapacityLimits:
|
||||
selectedPolicy.value?.inboxCapacityLimits?.map(limit => ({
|
||||
...limit,
|
||||
})) || [],
|
||||
}));
|
||||
|
||||
const handleBreadcrumbClick = ({ routeName }) =>
|
||||
router.push({ name: routeName });
|
||||
|
||||
const handleDeleteUser = agentId => {
|
||||
store.dispatch('agentCapacityPolicies/removeUser', {
|
||||
policyId: selectedPolicyId.value,
|
||||
userId: agentId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddUser = agent => {
|
||||
store.dispatch('agentCapacityPolicies/addUser', {
|
||||
policyId: selectedPolicyId.value,
|
||||
userData: { id: agent.id, capacity: 20 },
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteInboxLimit = limitId => {
|
||||
store.dispatch('agentCapacityPolicies/deleteInboxLimit', {
|
||||
policyId: selectedPolicyId.value,
|
||||
limitId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddInboxLimit = limit => {
|
||||
store.dispatch('agentCapacityPolicies/createInboxLimit', {
|
||||
policyId: selectedPolicyId.value,
|
||||
limitData: {
|
||||
inboxId: limit.inboxId,
|
||||
conversationLimit: limit.conversationLimit,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLimitChange = limit => {
|
||||
store.dispatch('agentCapacityPolicies/updateInboxLimit', {
|
||||
policyId: selectedPolicyId.value,
|
||||
limitId: limit.id,
|
||||
limitData: { conversationLimit: limit.conversationLimit },
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async formState => {
|
||||
try {
|
||||
await store.dispatch('agentCapacityPolicies/update', {
|
||||
id: selectedPolicyId.value,
|
||||
...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 (!selectedPolicyId.value)
|
||||
await store.dispatch('agentCapacityPolicies/show', routeId.value);
|
||||
|
||||
await store.dispatch('agentCapacityPolicies/getUsers', Number(routeId.value));
|
||||
};
|
||||
|
||||
watch(routeId, fetchPolicyData, { immediate: true });
|
||||
onMounted(() => store.dispatch('agents/get'));
|
||||
</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>
|
||||
<AgentCapacityPolicyForm
|
||||
:key="routeId"
|
||||
mode="EDIT"
|
||||
:initial-data="formData"
|
||||
:policy-users="policyUsers"
|
||||
:agent-list="allAgents"
|
||||
:label-list="allLabels"
|
||||
:inbox-list="allInboxes"
|
||||
show-user-section
|
||||
show-inbox-limit-section
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
:is-users-loading="usersUiFlags.isFetching"
|
||||
:is-inboxes-loading="inboxesUiFlags.isFetching"
|
||||
@submit="handleSubmit"
|
||||
@add-user="handleAddUser"
|
||||
@delete-user="handleDeleteUser"
|
||||
@add-inbox-limit="handleAddInboxLimit"
|
||||
@update-inbox-limit="handleLimitChange"
|
||||
@delete-inbox-limit="handleDeleteInboxLimit"
|
||||
/>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
@@ -0,0 +1,214 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BaseInfo from 'dashboard/components-next/AssignmentPolicy/components/BaseInfo.vue';
|
||||
import DataTable from 'dashboard/components-next/AssignmentPolicy/components/DataTable.vue';
|
||||
import AddDataDropdown from 'dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ExclusionRules from 'dashboard/components-next/AssignmentPolicy/components/ExclusionRules.vue';
|
||||
import InboxCapacityLimits from 'dashboard/components-next/AssignmentPolicy/components/InboxCapacityLimits.vue';
|
||||
|
||||
const props = defineProps({
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
name: '',
|
||||
description: '',
|
||||
enabled: false,
|
||||
exclusionRules: {
|
||||
excludedLabels: [],
|
||||
excludeOlderThanHours: 10,
|
||||
},
|
||||
inboxCapacityLimits: [],
|
||||
}),
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: value => ['CREATE', 'EDIT'].includes(value),
|
||||
},
|
||||
policyUsers: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
agentList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
labelList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
inboxList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showUserSection: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showInboxLimitSection: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isUsersLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isInboxesLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'submit',
|
||||
'addUser',
|
||||
'deleteUser',
|
||||
'validationChange',
|
||||
'deleteInboxLimit',
|
||||
'addInboxLimit',
|
||||
'updateInboxLimit',
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_CAPACITY_POLICY';
|
||||
|
||||
const state = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
exclusionRules: {
|
||||
excludedLabels: [],
|
||||
excludeOlderThanHours: 10,
|
||||
},
|
||||
inboxCapacityLimits: [],
|
||||
});
|
||||
|
||||
const validationState = ref({
|
||||
isValid: false,
|
||||
});
|
||||
|
||||
const buttonLabel = computed(() =>
|
||||
t(`${BASE_KEY}.${props.mode.toUpperCase()}.${props.mode}_BUTTON`)
|
||||
);
|
||||
|
||||
const handleValidationChange = validation => {
|
||||
validationState.value = validation;
|
||||
emit('validationChange', validation);
|
||||
};
|
||||
|
||||
const handleDeleteInboxLimit = id => {
|
||||
emit('deleteInboxLimit', id);
|
||||
};
|
||||
|
||||
const handleAddInboxLimit = limit => {
|
||||
emit('addInboxLimit', limit);
|
||||
};
|
||||
|
||||
const handleLimitChange = limit => {
|
||||
emit('updateInboxLimit', limit);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(state, {
|
||||
name: '',
|
||||
description: '',
|
||||
exclusionRules: {
|
||||
excludedLabels: [],
|
||||
excludeOlderThanHours: 10,
|
||||
},
|
||||
inboxCapacityLimits: [],
|
||||
});
|
||||
};
|
||||
|
||||
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">
|
||||
<BaseInfo
|
||||
v-model:policy-name="state.name"
|
||||
v-model:description="state.description"
|
||||
: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`)"
|
||||
@validation-change="handleValidationChange"
|
||||
/>
|
||||
<ExclusionRules
|
||||
v-model:excluded-labels="state.exclusionRules.excludedLabels"
|
||||
v-model:exclude-older-than-minutes="
|
||||
state.exclusionRules.excludeOlderThanHours
|
||||
"
|
||||
:tags-list="labelList"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="buttonLabel"
|
||||
:disabled="!validationState.isValid || isLoading"
|
||||
:is-loading="isLoading"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="showInboxLimitSection || showUserSection"
|
||||
class="flex flex-col gap-4 divide-y divide-n-weak border-t border-n-weak mt-6"
|
||||
>
|
||||
<InboxCapacityLimits
|
||||
v-if="showInboxLimitSection"
|
||||
v-model:inbox-capacity-limits="state.inboxCapacityLimits"
|
||||
:inbox-list="inboxList"
|
||||
:is-fetching="isInboxesLoading"
|
||||
@delete="handleDeleteInboxLimit"
|
||||
@add="handleAddInboxLimit"
|
||||
@update="handleLimitChange"
|
||||
/>
|
||||
<div v-if="showUserSection" 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.USERS.LABEL`) }}
|
||||
</label>
|
||||
<p class="mb-0 text-n-slate-11 text-sm">
|
||||
{{ t(`${BASE_KEY}.FORM.USERS.DESCRIPTION`) }}
|
||||
</p>
|
||||
</div>
|
||||
<AddDataDropdown
|
||||
:label="t(`${BASE_KEY}.FORM.USERS.ADD_BUTTON`)"
|
||||
:search-placeholder="
|
||||
t(`${BASE_KEY}.FORM.USERS.DROPDOWN.SEARCH_PLACEHOLDER`)
|
||||
"
|
||||
:items="agentList"
|
||||
@add="$emit('addUser', $event)"
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
:items="policyUsers"
|
||||
:is-fetching="isUsersLoading"
|
||||
:empty-state-message="t(`${BASE_KEY}.FORM.USERS.EMPTY_STATE`)"
|
||||
@delete="$emit('deleteUser', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
@@ -40,7 +40,10 @@ export const actions = {
|
||||
commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const response = await AgentCapacityPoliciesAPI.get();
|
||||
commit(types.SET_AGENT_CAPACITY_POLICIES, camelcaseKeys(response.data));
|
||||
commit(
|
||||
types.SET_AGENT_CAPACITY_POLICIES,
|
||||
camelcaseKeys(response.data, { deep: true })
|
||||
);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
@@ -52,7 +55,7 @@ export const actions = {
|
||||
commit(types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetchingItem: true });
|
||||
try {
|
||||
const response = await AgentCapacityPoliciesAPI.show(policyId);
|
||||
const policy = camelcaseKeys(response.data);
|
||||
const policy = camelcaseKeys(response.data, { deep: true });
|
||||
commit(types.SET_AGENT_CAPACITY_POLICY, policy);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
@@ -69,7 +72,10 @@ export const actions = {
|
||||
const response = await AgentCapacityPoliciesAPI.create(
|
||||
snakecaseKeys(policyObj)
|
||||
);
|
||||
commit(types.ADD_AGENT_CAPACITY_POLICY, camelcaseKeys(response.data));
|
||||
commit(
|
||||
types.ADD_AGENT_CAPACITY_POLICY,
|
||||
camelcaseKeys(response.data, { deep: true })
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
@@ -86,7 +92,10 @@ export const actions = {
|
||||
id,
|
||||
snakecaseKeys(policyParams)
|
||||
);
|
||||
commit(types.EDIT_AGENT_CAPACITY_POLICY, camelcaseKeys(response.data));
|
||||
commit(
|
||||
types.EDIT_AGENT_CAPACITY_POLICY,
|
||||
camelcaseKeys(response.data, { deep: true })
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
@@ -129,6 +138,97 @@ export const actions = {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addUser: async function addUser({ commit }, { policyId, userData }) {
|
||||
try {
|
||||
const response = await AgentCapacityPoliciesAPI.addUser(
|
||||
policyId,
|
||||
userData
|
||||
);
|
||||
commit(types.ADD_AGENT_CAPACITY_POLICIES_USERS, {
|
||||
policyId,
|
||||
user: camelcaseKeys(response.data),
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
removeUser: async function removeUser({ commit }, { policyId, userId }) {
|
||||
commit(types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, {
|
||||
isDeleting: true,
|
||||
});
|
||||
try {
|
||||
await AgentCapacityPoliciesAPI.removeUser(policyId, userId);
|
||||
commit(types.DELETE_AGENT_CAPACITY_POLICIES_USERS, { policyId, userId });
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
} finally {
|
||||
commit(types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, {
|
||||
isDeleting: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
createInboxLimit: async function createInboxLimit(
|
||||
{ commit },
|
||||
{ policyId, limitData }
|
||||
) {
|
||||
try {
|
||||
const response = await AgentCapacityPoliciesAPI.createInboxLimit(
|
||||
policyId,
|
||||
limitData
|
||||
);
|
||||
commit(
|
||||
types.SET_AGENT_CAPACITY_POLICIES_INBOXES,
|
||||
camelcaseKeys(response.data)
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateInboxLimit: async function updateInboxLimit(
|
||||
{ commit },
|
||||
{ policyId, limitId, limitData }
|
||||
) {
|
||||
try {
|
||||
const response = await AgentCapacityPoliciesAPI.updateInboxLimit(
|
||||
policyId,
|
||||
limitId,
|
||||
limitData
|
||||
);
|
||||
commit(
|
||||
types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES,
|
||||
camelcaseKeys(response.data)
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteInboxLimit: async function deleteInboxLimit(
|
||||
{ commit },
|
||||
{ policyId, limitId }
|
||||
) {
|
||||
try {
|
||||
await AgentCapacityPoliciesAPI.deleteInboxLimit(policyId, limitId);
|
||||
commit(types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES, {
|
||||
policyId,
|
||||
limitId,
|
||||
});
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
@@ -157,6 +257,54 @@ export const mutations = {
|
||||
policy.users = users;
|
||||
}
|
||||
},
|
||||
[types.ADD_AGENT_CAPACITY_POLICIES_USERS](_state, { policyId, user }) {
|
||||
const policy = _state.records.find(p => p.id === policyId);
|
||||
if (policy) {
|
||||
policy.users = policy.users || [];
|
||||
policy.users.push(user);
|
||||
policy.assignedAgentCount = policy.users.length;
|
||||
}
|
||||
},
|
||||
[types.DELETE_AGENT_CAPACITY_POLICIES_USERS](_state, { policyId, userId }) {
|
||||
const policy = _state.records.find(p => p.id === policyId);
|
||||
if (policy) {
|
||||
policy.users = (policy.users || []).filter(user => user.id !== userId);
|
||||
policy.assignedAgentCount = policy.users.length;
|
||||
}
|
||||
},
|
||||
|
||||
[types.SET_AGENT_CAPACITY_POLICIES_INBOXES](_state, data) {
|
||||
const policy = _state.records.find(
|
||||
p => p.id === data.agentCapacityPolicyId
|
||||
);
|
||||
policy?.inboxCapacityLimits.push({
|
||||
id: data.id,
|
||||
inboxId: data.inboxId,
|
||||
conversationLimit: data.conversationLimit,
|
||||
});
|
||||
},
|
||||
[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](_state, data) {
|
||||
const policy = _state.records.find(
|
||||
p => p.id === data.agentCapacityPolicyId
|
||||
);
|
||||
const limit = policy?.inboxCapacityLimits.find(l => l.id === data.id);
|
||||
if (limit) {
|
||||
Object.assign(limit, {
|
||||
conversationLimit: data.conversationLimit,
|
||||
});
|
||||
}
|
||||
},
|
||||
[types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES](
|
||||
_state,
|
||||
{ policyId, limitId }
|
||||
) {
|
||||
const policy = _state.records.find(p => p.id === policyId);
|
||||
if (policy) {
|
||||
policy.inboxCapacityLimits = policy.inboxCapacityLimits.filter(
|
||||
limit => limit.id !== limitId
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@@ -1,7 +1,12 @@
|
||||
import axios from 'axios';
|
||||
import { actions } from '../../agentCapacityPolicies';
|
||||
import types from '../../../mutation-types';
|
||||
import agentCapacityPoliciesList, { camelCaseFixtures } from './fixtures';
|
||||
import agentCapacityPoliciesList, {
|
||||
camelCaseFixtures,
|
||||
mockUsers,
|
||||
mockInboxLimits,
|
||||
camelCaseMockInboxLimits,
|
||||
} from './fixtures';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import snakecaseKeys from 'snakecase-keys';
|
||||
|
||||
@@ -25,7 +30,9 @@ describe('#actions', () => {
|
||||
|
||||
await actions.get({ commit });
|
||||
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(agentCapacityPoliciesList);
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(agentCapacityPoliciesList, {
|
||||
deep: true,
|
||||
});
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetching: true }],
|
||||
[types.SET_AGENT_CAPACITY_POLICIES, camelCaseFixtures],
|
||||
@@ -55,7 +62,9 @@ describe('#actions', () => {
|
||||
|
||||
await actions.show({ commit }, 1);
|
||||
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(policyData);
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(policyData, {
|
||||
deep: true,
|
||||
});
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetchingItem: true }],
|
||||
[types.SET_AGENT_CAPACITY_POLICY, camelCasedPolicy],
|
||||
@@ -88,7 +97,9 @@ describe('#actions', () => {
|
||||
const result = await actions.create({ commit }, newPolicy);
|
||||
|
||||
expect(snakecaseKeys).toHaveBeenCalledWith(newPolicy);
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy);
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy, {
|
||||
deep: true,
|
||||
});
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isCreating: true }],
|
||||
[types.ADD_AGENT_CAPACITY_POLICY, camelCasedData],
|
||||
@@ -129,7 +140,9 @@ describe('#actions', () => {
|
||||
const result = await actions.update({ commit }, updateParams);
|
||||
|
||||
expect(snakecaseKeys).toHaveBeenCalledWith({ name: 'Updated Policy' });
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(responseData, {
|
||||
deep: true,
|
||||
});
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isUpdating: true }],
|
||||
[types.EDIT_AGENT_CAPACITY_POLICY, camelCasedData],
|
||||
@@ -224,4 +237,172 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addUser', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const policyId = 1;
|
||||
const userData = { user_id: 3, capacity: 12 };
|
||||
const responseData = mockUsers[2];
|
||||
const camelCasedUser = mockUsers[2];
|
||||
|
||||
axios.post.mockResolvedValue({ data: responseData });
|
||||
camelcaseKeys.mockReturnValue(camelCasedUser);
|
||||
|
||||
const result = await actions.addUser({ commit }, { policyId, userData });
|
||||
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
types.ADD_AGENT_CAPACITY_POLICIES_USERS,
|
||||
{ policyId, user: camelCasedUser },
|
||||
],
|
||||
]);
|
||||
expect(result).toEqual(responseData);
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue(new Error('Validation error'));
|
||||
|
||||
await expect(
|
||||
actions.addUser({ commit }, { policyId: 1, userData: {} })
|
||||
).rejects.toThrow(Error);
|
||||
|
||||
expect(commit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removeUser', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const policyId = 1;
|
||||
const userId = 2;
|
||||
axios.delete.mockResolvedValue({});
|
||||
|
||||
await actions.removeUser({ commit }, { policyId, userId });
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { isDeleting: true }],
|
||||
[types.DELETE_AGENT_CAPACITY_POLICIES_USERS, { policyId, userId }],
|
||||
[
|
||||
types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG,
|
||||
{ isDeleting: false },
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue(new Error('Not found'));
|
||||
|
||||
await expect(
|
||||
actions.removeUser({ commit }, { policyId: 1, userId: 2 })
|
||||
).rejects.toThrow(Error);
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { isDeleting: true }],
|
||||
[
|
||||
types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG,
|
||||
{ isDeleting: false },
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createInboxLimit', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const policyId = 1;
|
||||
const limitData = { inbox_id: 3, conversation_limit: 20 };
|
||||
const responseData = mockInboxLimits[2];
|
||||
const camelCasedData = camelCaseMockInboxLimits[2];
|
||||
|
||||
axios.post.mockResolvedValue({ data: responseData });
|
||||
camelcaseKeys.mockReturnValue(camelCasedData);
|
||||
|
||||
const result = await actions.createInboxLimit(
|
||||
{ commit },
|
||||
{ policyId, limitData }
|
||||
);
|
||||
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_CAPACITY_POLICIES_INBOXES, camelCasedData],
|
||||
]);
|
||||
expect(result).toEqual(responseData);
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue(new Error('Validation error'));
|
||||
|
||||
await expect(
|
||||
actions.createInboxLimit({ commit }, { policyId: 1, limitData: {} })
|
||||
).rejects.toThrow(Error);
|
||||
|
||||
expect(commit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateInboxLimit', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const policyId = 1;
|
||||
const limitId = 1;
|
||||
const limitData = { conversation_limit: 25 };
|
||||
const responseData = {
|
||||
...mockInboxLimits[0],
|
||||
conversation_limit: 25,
|
||||
};
|
||||
const camelCasedData = {
|
||||
...camelCaseMockInboxLimits[0],
|
||||
conversationLimit: 25,
|
||||
};
|
||||
|
||||
axios.put.mockResolvedValue({ data: responseData });
|
||||
camelcaseKeys.mockReturnValue(camelCasedData);
|
||||
|
||||
const result = await actions.updateInboxLimit(
|
||||
{ commit },
|
||||
{ policyId, limitId, limitData }
|
||||
);
|
||||
|
||||
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES, camelCasedData],
|
||||
]);
|
||||
expect(result).toEqual(responseData);
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.put.mockRejectedValue(new Error('Validation error'));
|
||||
|
||||
await expect(
|
||||
actions.updateInboxLimit(
|
||||
{ commit },
|
||||
{ policyId: 1, limitId: 1, limitData: {} }
|
||||
)
|
||||
).rejects.toThrow(Error);
|
||||
|
||||
expect(commit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteInboxLimit', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const policyId = 1;
|
||||
const limitId = 1;
|
||||
axios.delete.mockResolvedValue({});
|
||||
|
||||
await actions.deleteInboxLimit({ commit }, { policyId, limitId });
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES, { policyId, limitId }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue(new Error('Not found'));
|
||||
|
||||
await expect(
|
||||
actions.deleteInboxLimit({ commit }, { policyId: 1, limitId: 1 })
|
||||
).rejects.toThrow(Error);
|
||||
|
||||
expect(commit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -10,6 +10,20 @@ export default [
|
||||
created_at: '2024-01-01T10:00:00.000Z',
|
||||
updated_at: '2024-01-01T10:00:00.000Z',
|
||||
users: [],
|
||||
inbox_capacity_limits: [
|
||||
{
|
||||
id: 1,
|
||||
inbox_id: 1,
|
||||
conversation_limit: 15,
|
||||
agent_capacity_policy_id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
inbox_id: 2,
|
||||
conversation_limit: 8,
|
||||
agent_capacity_policy_id: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -21,7 +35,21 @@ export default [
|
||||
assigned_agent_count: 5,
|
||||
created_at: '2024-01-01T11:00:00.000Z',
|
||||
updated_at: '2024-01-01T11:00:00.000Z',
|
||||
users: [],
|
||||
users: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Agent Smith',
|
||||
email: 'agent.smith@example.com',
|
||||
capacity: 25,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Agent Johnson',
|
||||
email: 'agent.johnson@example.com',
|
||||
capacity: 18,
|
||||
},
|
||||
],
|
||||
inbox_capacity_limits: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -34,6 +62,7 @@ export default [
|
||||
created_at: '2024-01-01T12:00:00.000Z',
|
||||
updated_at: '2024-01-01T12:00:00.000Z',
|
||||
users: [],
|
||||
inbox_capacity_limits: [],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -49,6 +78,20 @@ export const camelCaseFixtures = [
|
||||
createdAt: '2024-01-01T10:00:00.000Z',
|
||||
updatedAt: '2024-01-01T10:00:00.000Z',
|
||||
users: [],
|
||||
inboxCapacityLimits: [
|
||||
{
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 15,
|
||||
agentCapacityPolicyId: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
inboxId: 2,
|
||||
conversationLimit: 8,
|
||||
agentCapacityPolicyId: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -60,7 +103,21 @@ export const camelCaseFixtures = [
|
||||
assignedAgentCount: 5,
|
||||
createdAt: '2024-01-01T11:00:00.000Z',
|
||||
updatedAt: '2024-01-01T11:00:00.000Z',
|
||||
users: [],
|
||||
users: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Agent Smith',
|
||||
email: 'agent.smith@example.com',
|
||||
capacity: 25,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Agent Johnson',
|
||||
email: 'agent.johnson@example.com',
|
||||
capacity: 18,
|
||||
},
|
||||
],
|
||||
inboxCapacityLimits: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -73,5 +130,70 @@ export const camelCaseFixtures = [
|
||||
createdAt: '2024-01-01T12:00:00.000Z',
|
||||
updatedAt: '2024-01-01T12:00:00.000Z',
|
||||
users: [],
|
||||
inboxCapacityLimits: [],
|
||||
},
|
||||
];
|
||||
|
||||
// Additional test data for user and inbox limit operations
|
||||
export const mockUsers = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Agent Smith',
|
||||
email: 'agent.smith@example.com',
|
||||
capacity: 25,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Agent Johnson',
|
||||
email: 'agent.johnson@example.com',
|
||||
capacity: 18,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Agent Brown',
|
||||
email: 'agent.brown@example.com',
|
||||
capacity: 12,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockInboxLimits = [
|
||||
{
|
||||
id: 1,
|
||||
inbox_id: 1,
|
||||
conversation_limit: 15,
|
||||
agent_capacity_policy_id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
inbox_id: 2,
|
||||
conversation_limit: 8,
|
||||
agent_capacity_policy_id: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
inbox_id: 3,
|
||||
conversation_limit: 20,
|
||||
agent_capacity_policy_id: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export const camelCaseMockInboxLimits = [
|
||||
{
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 15,
|
||||
agentCapacityPolicyId: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
inboxId: 2,
|
||||
conversationLimit: 8,
|
||||
agentCapacityPolicyId: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
inboxId: 3,
|
||||
conversationLimit: 20,
|
||||
agentCapacityPolicyId: 2,
|
||||
},
|
||||
];
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { mutations } from '../../agentCapacityPolicies';
|
||||
import types from '../../../mutation-types';
|
||||
import agentCapacityPoliciesList from './fixtures';
|
||||
import agentCapacityPoliciesList, { mockUsers } from './fixtures';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_AGENT_CAPACITY_POLICIES_UI_FLAG', () => {
|
||||
@@ -248,7 +248,7 @@ describe('#mutations', () => {
|
||||
|
||||
describe('#SET_AGENT_CAPACITY_POLICIES_USERS', () => {
|
||||
it('sets users for existing policy', () => {
|
||||
const mockUsers = [
|
||||
const testUsers = [
|
||||
{ id: 1, name: 'Agent 1', email: 'agent1@example.com', capacity: 15 },
|
||||
{ id: 2, name: 'Agent 2', email: 'agent2@example.com', capacity: 20 },
|
||||
];
|
||||
@@ -262,10 +262,10 @@ describe('#mutations', () => {
|
||||
|
||||
mutations[types.SET_AGENT_CAPACITY_POLICIES_USERS](state, {
|
||||
policyId: 1,
|
||||
users: mockUsers,
|
||||
users: testUsers,
|
||||
});
|
||||
|
||||
expect(state.records[0].users).toEqual(mockUsers);
|
||||
expect(state.records[0].users).toEqual(testUsers);
|
||||
expect(state.records[1].users).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -300,4 +300,320 @@ describe('#mutations', () => {
|
||||
expect(state).toEqual(originalState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ADD_AGENT_CAPACITY_POLICIES_USERS', () => {
|
||||
it('adds user to existing policy', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{ id: 1, name: 'Policy 1', users: [] },
|
||||
{ id: 2, name: 'Policy 2', users: [] },
|
||||
],
|
||||
};
|
||||
|
||||
mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, {
|
||||
policyId: 1,
|
||||
user: mockUsers[0],
|
||||
});
|
||||
|
||||
expect(state.records[0].users).toEqual([mockUsers[0]]);
|
||||
expect(state.records[1].users).toEqual([]);
|
||||
});
|
||||
|
||||
it('adds user to policy with existing users', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'Policy 1', users: [mockUsers[0]] }],
|
||||
};
|
||||
|
||||
mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, {
|
||||
policyId: 1,
|
||||
user: mockUsers[1],
|
||||
});
|
||||
|
||||
expect(state.records[0].users).toEqual([mockUsers[0], mockUsers[1]]);
|
||||
});
|
||||
|
||||
it('initializes users array if undefined', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'Policy 1' }],
|
||||
};
|
||||
|
||||
mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, {
|
||||
policyId: 1,
|
||||
user: mockUsers[0],
|
||||
});
|
||||
|
||||
expect(state.records[0].users).toEqual([mockUsers[0]]);
|
||||
});
|
||||
|
||||
it('updates assigned agent count', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'Policy 1', users: [] }],
|
||||
};
|
||||
|
||||
mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, {
|
||||
policyId: 1,
|
||||
user: mockUsers[0],
|
||||
});
|
||||
|
||||
expect(state.records[0].assignedAgentCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_AGENT_CAPACITY_POLICIES_USERS', () => {
|
||||
it('removes user from policy', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Policy 1',
|
||||
users: [mockUsers[0], mockUsers[1], mockUsers[2]],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_AGENT_CAPACITY_POLICIES_USERS](state, {
|
||||
policyId: 1,
|
||||
userId: 2,
|
||||
});
|
||||
|
||||
expect(state.records[0].users).toEqual([mockUsers[0], mockUsers[2]]);
|
||||
});
|
||||
|
||||
it('handles removing non-existent user', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Policy 1',
|
||||
users: [mockUsers[0]],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_AGENT_CAPACITY_POLICIES_USERS](state, {
|
||||
policyId: 1,
|
||||
userId: 999,
|
||||
});
|
||||
|
||||
expect(state.records[0].users).toEqual([mockUsers[0]]);
|
||||
});
|
||||
|
||||
it('updates assigned agent count', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'Policy 1', users: [mockUsers[0]] }],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_AGENT_CAPACITY_POLICIES_USERS](state, {
|
||||
policyId: 1,
|
||||
userId: 1,
|
||||
});
|
||||
|
||||
expect(state.records[0].assignedAgentCount).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_AGENT_CAPACITY_POLICIES_INBOXES', () => {
|
||||
it('adds inbox limit to policy', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Policy 1',
|
||||
inboxCapacityLimits: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const inboxLimitData = {
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 15,
|
||||
agentCapacityPolicyId: 1,
|
||||
};
|
||||
|
||||
mutations[types.SET_AGENT_CAPACITY_POLICIES_INBOXES](
|
||||
state,
|
||||
inboxLimitData
|
||||
);
|
||||
|
||||
expect(state.records[0].inboxCapacityLimits).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 15,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('does nothing if policy not found', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'Policy 1', inboxCapacityLimits: [] }],
|
||||
};
|
||||
|
||||
const originalState = JSON.parse(JSON.stringify(state));
|
||||
|
||||
mutations[types.SET_AGENT_CAPACITY_POLICIES_INBOXES](state, {
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 15,
|
||||
agentCapacityPolicyId: 999,
|
||||
});
|
||||
|
||||
expect(state).toEqual(originalState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#EDIT_AGENT_CAPACITY_POLICIES_INBOXES', () => {
|
||||
it('updates existing inbox limit', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Policy 1',
|
||||
inboxCapacityLimits: [
|
||||
{
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 15,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
inboxId: 2,
|
||||
conversationLimit: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mutations[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](state, {
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 25,
|
||||
agentCapacityPolicyId: 1,
|
||||
});
|
||||
|
||||
expect(state.records[0].inboxCapacityLimits[0]).toEqual({
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 25,
|
||||
});
|
||||
expect(state.records[0].inboxCapacityLimits[1]).toEqual({
|
||||
id: 2,
|
||||
inboxId: 2,
|
||||
conversationLimit: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('does nothing if limit not found', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Policy 1',
|
||||
inboxCapacityLimits: [
|
||||
{
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const originalLimits = [...state.records[0].inboxCapacityLimits];
|
||||
|
||||
mutations[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](state, {
|
||||
id: 999,
|
||||
inboxId: 1,
|
||||
conversationLimit: 25,
|
||||
agentCapacityPolicyId: 1,
|
||||
});
|
||||
|
||||
expect(state.records[0].inboxCapacityLimits).toEqual(originalLimits);
|
||||
});
|
||||
|
||||
it('does nothing if policy not found', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'Policy 1', inboxCapacityLimits: [] }],
|
||||
};
|
||||
|
||||
const originalState = JSON.parse(JSON.stringify(state));
|
||||
|
||||
mutations[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](state, {
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 25,
|
||||
agentCapacityPolicyId: 999,
|
||||
});
|
||||
|
||||
expect(state).toEqual(originalState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_AGENT_CAPACITY_POLICIES_INBOXES', () => {
|
||||
it('removes inbox limit from policy', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Policy 1',
|
||||
inboxCapacityLimits: [
|
||||
{
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 15,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
inboxId: 2,
|
||||
conversationLimit: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES](state, {
|
||||
policyId: 1,
|
||||
limitId: 1,
|
||||
});
|
||||
|
||||
expect(state.records[0].inboxCapacityLimits).toEqual([
|
||||
{
|
||||
id: 2,
|
||||
inboxId: 2,
|
||||
conversationLimit: 8,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles removing non-existent limit', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Policy 1',
|
||||
inboxCapacityLimits: [
|
||||
{
|
||||
id: 1,
|
||||
inboxId: 1,
|
||||
conversationLimit: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const originalLimits = [...state.records[0].inboxCapacityLimits];
|
||||
|
||||
mutations[types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES](state, {
|
||||
policyId: 1,
|
||||
limitId: 999,
|
||||
});
|
||||
|
||||
expect(state.records[0].inboxCapacityLimits).toEqual(originalLimits);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -372,4 +372,10 @@ export default {
|
||||
SET_AGENT_CAPACITY_POLICIES_USERS: 'SET_AGENT_CAPACITY_POLICIES_USERS',
|
||||
SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG:
|
||||
'SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG',
|
||||
ADD_AGENT_CAPACITY_POLICIES_USERS: 'ADD_AGENT_CAPACITY_POLICIES_USERS',
|
||||
DELETE_AGENT_CAPACITY_POLICIES_USERS: 'DELETE_AGENT_CAPACITY_POLICIES_USERS',
|
||||
SET_AGENT_CAPACITY_POLICIES_INBOXES: 'SET_AGENT_CAPACITY_POLICIES_INBOXES',
|
||||
EDIT_AGENT_CAPACITY_POLICIES_INBOXES: 'EDIT_AGENT_CAPACITY_POLICIES_INBOXES',
|
||||
DELETE_AGENT_CAPACITY_POLICIES_INBOXES:
|
||||
'DELETE_AGENT_CAPACITY_POLICIES_INBOXES',
|
||||
};
|
||||
|
Reference in New Issue
Block a user