fix: V2 Assignment service enhancements (#13036)
## Linear Ticket: https://linear.app/chatwoot/issue/CW-6081/review-feedback ## Description Assignment V2 Service Enhancements - Enable Assignment V2 on plan upgrade - Fix UI issue with fair distribution policy display - Add advanced assignment feature flag and enhance Assignment V2 capabilities ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? This has been tested using the UI. ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes auto-assignment execution paths, rate limiting defaults, and feature-flag gating (including premium plan behavior), which could affect which conversations get assigned and when. UI rewires inbox settings and policy flows, so regressions are possible around navigation/linking and feature visibility. > > **Overview** > **Adds a new premium `advanced_assignment` feature flag** and uses it to gate capacity/balanced assignment features in the UI (sidebar entry, settings routes, assignment-policy landing cards) and backend (Enterprise balanced selector + capacity filtering). `advanced_assignment` is marked premium, included in Business plan entitlements, and auto-synced in Enterprise accounts when `assignment_v2` is toggled. > > **Improves Assignment V2 policy UX** by adding an inbox-level “Conversation Assignment” section (behind `assignment_v2`) that can link/unlink an assignment policy, navigate to create/edit policy flows with `inboxId` query context, and show an inbox-link prompt after creating a policy. The policy form now defaults to enabled, disables the `balanced` option with a premium badge/message when unavailable, and inbox lists support click-to-navigate. > > **Tightens/adjusts auto-assignment behavior**: bulk assignment now requires `inbox.enable_auto_assignment?`, conversation ordering uses the attached `assignment_policy` priority, and rate limiting uses `assignment_policy` config with an infinite default limit while still tracking assignments. Tests and i18n strings are updated accordingly. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 23bc03bf75ee4376071e4d7fc7cd564c601d33d7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
parent
8f95fafff4
commit
7b512bd00e
@ -39,7 +39,6 @@ const policyA = withCount({
|
|||||||
description: 'Distributes conversations evenly among available agents',
|
description: 'Distributes conversations evenly among available agents',
|
||||||
assignmentOrder: 'round_robin',
|
assignmentOrder: 'round_robin',
|
||||||
conversationPriority: 'high',
|
conversationPriority: 'high',
|
||||||
enabled: true,
|
|
||||||
inboxes: [mockInboxes[0], mockInboxes[1]],
|
inboxes: [mockInboxes[0], mockInboxes[1]],
|
||||||
isFetchingInboxes: false,
|
isFetchingInboxes: false,
|
||||||
});
|
});
|
||||||
@ -50,7 +49,6 @@ const policyB = withCount({
|
|||||||
description: 'Assigns based on capacity and workload',
|
description: 'Assigns based on capacity and workload',
|
||||||
assignmentOrder: 'capacity_based',
|
assignmentOrder: 'capacity_based',
|
||||||
conversationPriority: 'medium',
|
conversationPriority: 'medium',
|
||||||
enabled: true,
|
|
||||||
inboxes: [mockInboxes[2], mockInboxes[3]],
|
inboxes: [mockInboxes[2], mockInboxes[3]],
|
||||||
isFetchingInboxes: false,
|
isFetchingInboxes: false,
|
||||||
});
|
});
|
||||||
@ -61,7 +59,6 @@ const emptyPolicy = withCount({
|
|||||||
description: 'Policy with no assigned inboxes',
|
description: 'Policy with no assigned inboxes',
|
||||||
assignmentOrder: 'manual',
|
assignmentOrder: 'manual',
|
||||||
conversationPriority: 'low',
|
conversationPriority: 'low',
|
||||||
enabled: false,
|
|
||||||
inboxes: [],
|
inboxes: [],
|
||||||
isFetchingInboxes: false,
|
isFetchingInboxes: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,7 +15,6 @@ const props = defineProps({
|
|||||||
assignmentOrder: { type: String, default: '' },
|
assignmentOrder: { type: String, default: '' },
|
||||||
conversationPriority: { type: String, default: '' },
|
conversationPriority: { type: String, default: '' },
|
||||||
assignedInboxCount: { type: Number, default: 0 },
|
assignedInboxCount: { type: Number, default: 0 },
|
||||||
enabled: { type: Boolean, default: false },
|
|
||||||
inboxes: { type: Array, default: () => [] },
|
inboxes: { type: Array, default: () => [] },
|
||||||
isFetchingInboxes: { type: Boolean, default: false },
|
isFetchingInboxes: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
@ -65,22 +64,6 @@ const handleFetchInboxes = () => {
|
|||||||
{{ name }}
|
{{ name }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex items-center rounded-md bg-n-alpha-2 h-6 px-2">
|
|
||||||
<span
|
|
||||||
class="text-xs"
|
|
||||||
:class="enabled ? 'text-n-teal-11' : 'text-n-slate-12'"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
enabled
|
|
||||||
? t(
|
|
||||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.ACTIVE'
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.INACTIVE'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<CardPopover
|
<CardPopover
|
||||||
:title="
|
:title="
|
||||||
t(
|
t(
|
||||||
|
|||||||
@ -19,11 +19,15 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['delete']);
|
const emit = defineEmits(['delete', 'navigate']);
|
||||||
|
|
||||||
const handleDelete = itemId => {
|
const handleDelete = itemId => {
|
||||||
emit('delete', itemId);
|
emit('delete', itemId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNavigate = item => {
|
||||||
|
emit('navigate', item);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -47,7 +51,11 @@ const handleDelete = itemId => {
|
|||||||
:key="item.id"
|
: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"
|
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">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center gap-2 col-span-2 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 rounded-lg py-1 px-1.5 -ml-1.5 transition-colors cursor-pointer group"
|
||||||
|
@click="handleNavigate(item)"
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
v-if="item.icon"
|
v-if="item.icon"
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
@ -61,10 +69,16 @@ const handleDelete = itemId => {
|
|||||||
:size="20"
|
:size="20"
|
||||||
rounded-full
|
rounded-full
|
||||||
/>
|
/>
|
||||||
<span class="text-sm text-n-slate-12 truncate min-w-0">
|
<span
|
||||||
|
class="text-sm text-n-slate-12 truncate min-w-0 group-hover:text-n-blue-11 dark:group-hover:text-n-blue-10 transition-colors"
|
||||||
|
>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<Icon
|
||||||
|
icon="i-lucide-external-link"
|
||||||
|
class="size-3.5 text-n-slate-10 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="flex items-start gap-2 col-span-1">
|
<div class="flex items-start gap-2 col-span-1">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import Input from 'dashboard/components-next/input/Input.vue';
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
|
import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
|
||||||
@ -15,6 +15,9 @@ const fairDistributionLimit = defineModel('fairDistributionLimit', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The model value is in seconds (for the backend/DB)
|
||||||
|
// DurationInput works in minutes internally
|
||||||
|
// We need to convert between seconds and minutes
|
||||||
const fairDistributionWindow = defineModel('fairDistributionWindow', {
|
const fairDistributionWindow = defineModel('fairDistributionWindow', {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 3600,
|
default: 3600,
|
||||||
@ -25,6 +28,17 @@ const fairDistributionWindow = defineModel('fairDistributionWindow', {
|
|||||||
|
|
||||||
const windowUnit = ref(DURATION_UNITS.MINUTES);
|
const windowUnit = ref(DURATION_UNITS.MINUTES);
|
||||||
|
|
||||||
|
// Convert seconds to minutes for DurationInput
|
||||||
|
const windowInMinutes = computed({
|
||||||
|
get() {
|
||||||
|
return Math.floor((fairDistributionWindow.value || 0) / 60);
|
||||||
|
},
|
||||||
|
set(minutes) {
|
||||||
|
fairDistributionWindow.value = minutes * 60;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detect unit based on minutes (converted from seconds)
|
||||||
const detectUnit = minutes => {
|
const detectUnit = minutes => {
|
||||||
const m = Number(minutes) || 0;
|
const m = Number(minutes) || 0;
|
||||||
if (m === 0) return DURATION_UNITS.MINUTES;
|
if (m === 0) return DURATION_UNITS.MINUTES;
|
||||||
@ -34,7 +48,7 @@ const detectUnit = minutes => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
windowUnit.value = detectUnit(fairDistributionWindow.value);
|
windowUnit.value = detectUnit(windowInMinutes.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -73,9 +87,9 @@ onMounted(() => {
|
|||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 flex-1 [&>select]:!bg-n-alpha-2 [&>select]:!outline-none [&>select]:hover:brightness-110"
|
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 -->
|
<!-- allow 10 mins to 999 days (in minutes) -->
|
||||||
<DurationInput
|
<DurationInput
|
||||||
v-model:model-value="fairDistributionWindow"
|
v-model:model-value="windowInMinutes"
|
||||||
v-model:unit="windowUnit"
|
v-model:unit="windowUnit"
|
||||||
:min="10"
|
:min="10"
|
||||||
:max="1438560"
|
:max="1438560"
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -16,12 +18,22 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
disabledMessage: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['select']);
|
const emit = defineEmits(['select']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const handleChange = () => {
|
const handleChange = () => {
|
||||||
if (!props.isActive) {
|
if (!props.isActive && !props.disabled) {
|
||||||
emit('select', props.id);
|
emit('select', props.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -29,9 +41,11 @@ const handleChange = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<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="relative 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="[
|
:class="[
|
||||||
isActive ? 'outline-n-blue-9' : 'outline-n-weak hover:outline-n-strong',
|
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
isActive ? 'outline-n-blue-9' : 'outline-n-weak',
|
||||||
|
!disabled && !isActive ? 'hover:outline-n-strong' : '',
|
||||||
]"
|
]"
|
||||||
@click="handleChange"
|
@click="handleChange"
|
||||||
>
|
>
|
||||||
@ -41,6 +55,7 @@ const handleChange = () => {
|
|||||||
:checked="isActive"
|
:checked="isActive"
|
||||||
:value="id"
|
:value="id"
|
||||||
:name="id"
|
:name="id"
|
||||||
|
:disabled="disabled"
|
||||||
type="radio"
|
type="radio"
|
||||||
class="h-4 w-4 border-n-slate-6 text-n-brand focus:ring-n-brand focus:ring-offset-0"
|
class="h-4 w-4 border-n-slate-6 text-n-brand focus:ring-n-brand focus:ring-offset-0"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
@ -49,11 +64,23 @@ const handleChange = () => {
|
|||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex flex-col gap-3 items-start">
|
<div class="flex flex-col gap-3 items-start">
|
||||||
<h3 class="text-sm font-medium text-n-slate-12">
|
<div class="flex items-center gap-2">
|
||||||
{{ label }}
|
<h3 class="text-sm font-medium text-n-slate-12">
|
||||||
</h3>
|
{{ label }}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
v-if="disabled"
|
||||||
|
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-n-yellow-3 text-n-yellow-11"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.FORM.ASSIGNMENT_ORDER.BALANCED.PREMIUM_BADGE'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<p class="text-sm text-n-slate-11">
|
<p class="text-sm text-n-slate-11">
|
||||||
{{ description }}
|
{{ disabled && disabledMessage ? disabledMessage : description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,7 +6,6 @@ const policyName = ref('Round Robin Policy');
|
|||||||
const description = ref(
|
const description = ref(
|
||||||
'Distributes conversations evenly among available agents'
|
'Distributes conversations evenly among available agents'
|
||||||
);
|
);
|
||||||
const enabled = ref(true);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -19,13 +18,10 @@ const enabled = ref(true);
|
|||||||
<BaseInfo
|
<BaseInfo
|
||||||
v-model:policy-name="policyName"
|
v-model:policy-name="policyName"
|
||||||
v-model:description="description"
|
v-model:description="description"
|
||||||
v-model:enabled="enabled"
|
|
||||||
name-label="Policy Name"
|
name-label="Policy Name"
|
||||||
name-placeholder="Enter policy name"
|
name-placeholder="Enter policy name"
|
||||||
description-label="Description"
|
description-label="Description"
|
||||||
description-placeholder="Enter policy description"
|
description-placeholder="Enter policy description"
|
||||||
status-label="Status"
|
|
||||||
status-placeholder="Active"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Variant>
|
</Variant>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { useStore } from 'vuex';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
||||||
import { vOnClickOutside } from '@vueuse/components';
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
import { useWindowSize, useEventListener } from '@vueuse/core';
|
import { useWindowSize, useEventListener } from '@vueuse/core';
|
||||||
import { emitter } from 'shared/helpers/mitt';
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
@ -50,6 +51,18 @@ const isRTL = useMapGetter('accounts/isRTL');
|
|||||||
const { width: windowWidth } = useWindowSize();
|
const { width: windowWidth } = useWindowSize();
|
||||||
const isMobile = computed(() => windowWidth.value < 768);
|
const isMobile = computed(() => windowWidth.value < 768);
|
||||||
|
|
||||||
|
const accountId = useMapGetter('getCurrentAccountId');
|
||||||
|
const isFeatureEnabledonAccount = useMapGetter(
|
||||||
|
'accounts/isFeatureEnabledonAccount'
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasAdvancedAssignment = computed(() => {
|
||||||
|
return isFeatureEnabledonAccount.value(
|
||||||
|
accountId.value,
|
||||||
|
FEATURE_FLAGS.ADVANCED_ASSIGNMENT
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const toggleShortcutModalFn = show => {
|
const toggleShortcutModalFn = show => {
|
||||||
if (show) {
|
if (show) {
|
||||||
emit('openKeyShortcutModal');
|
emit('openKeyShortcutModal');
|
||||||
@ -584,12 +597,16 @@ const menuItems = computed(() => {
|
|||||||
icon: 'i-lucide-users',
|
icon: 'i-lucide-users',
|
||||||
to: accountScopedRoute('settings_teams_list'),
|
to: accountScopedRoute('settings_teams_list'),
|
||||||
},
|
},
|
||||||
{
|
...(hasAdvancedAssignment.value
|
||||||
name: 'Settings Agent Assignment',
|
? [
|
||||||
label: t('SIDEBAR.AGENT_ASSIGNMENT'),
|
{
|
||||||
icon: 'i-lucide-user-cog',
|
name: 'Settings Agent Assignment',
|
||||||
to: accountScopedRoute('assignment_policy_index'),
|
label: t('SIDEBAR.AGENT_ASSIGNMENT'),
|
||||||
},
|
icon: 'i-lucide-user-cog',
|
||||||
|
to: accountScopedRoute('assignment_policy_index'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
name: 'Settings Inboxes',
|
name: 'Settings Inboxes',
|
||||||
label: t('SIDEBAR.INBOXES'),
|
label: t('SIDEBAR.INBOXES'),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export const FEATURE_FLAGS = {
|
|||||||
AGENT_BOTS: 'agent_bots',
|
AGENT_BOTS: 'agent_bots',
|
||||||
AGENT_MANAGEMENT: 'agent_management',
|
AGENT_MANAGEMENT: 'agent_management',
|
||||||
ASSIGNMENT_V2: 'assignment_v2',
|
ASSIGNMENT_V2: 'assignment_v2',
|
||||||
|
ADVANCED_ASSIGNMENT: 'advanced_assignment',
|
||||||
AUTO_RESOLVE_CONVERSATIONS: 'auto_resolve_conversations',
|
AUTO_RESOLVE_CONVERSATIONS: 'auto_resolve_conversations',
|
||||||
AUTOMATIONS: 'automations',
|
AUTOMATIONS: 'automations',
|
||||||
CAMPAIGNS: 'campaigns',
|
CAMPAIGNS: 'campaigns',
|
||||||
@ -56,4 +57,5 @@ export const PREMIUM_FEATURES = [
|
|||||||
FEATURE_FLAGS.HELP_CENTER,
|
FEATURE_FLAGS.HELP_CENTER,
|
||||||
FEATURE_FLAGS.SAML,
|
FEATURE_FLAGS.SAML,
|
||||||
FEATURE_FLAGS.CONVERSATION_REQUIRED_ATTRIBUTES,
|
FEATURE_FLAGS.CONVERSATION_REQUIRED_ATTRIBUTES,
|
||||||
|
FEATURE_FLAGS.ADVANCED_ASSIGNMENT,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -766,6 +766,53 @@
|
|||||||
"MAX_ASSIGNMENT_LIMIT_RANGE_ERROR": "Please enter a value greater than 0",
|
"MAX_ASSIGNMENT_LIMIT_RANGE_ERROR": "Please enter a value greater than 0",
|
||||||
"MAX_ASSIGNMENT_LIMIT_SUB_TEXT": "Limit the maximum number of conversations from this inbox that can be auto assigned to an agent"
|
"MAX_ASSIGNMENT_LIMIT_SUB_TEXT": "Limit the maximum number of conversations from this inbox that can be auto assigned to an agent"
|
||||||
},
|
},
|
||||||
|
"ASSIGNMENT": {
|
||||||
|
"TITLE": "Conversation Assignment",
|
||||||
|
"DESCRIPTION": "Automatically assign incoming conversations to available agents based on assignment policies",
|
||||||
|
"ENABLE_AUTO_ASSIGNMENT": "Enable automatic conversation assignment",
|
||||||
|
"DEFAULT_RULES_TITLE": "Default assignment rules",
|
||||||
|
"DEFAULT_RULES_DESCRIPTION": "Using the default assignment behavior for all conversations",
|
||||||
|
"DEFAULT_RULE_1": "Earliest created conversations first",
|
||||||
|
"DEFAULT_RULE_2": "Round robin distribution",
|
||||||
|
"CUSTOMIZE_WITH_POLICY": "Customize with assignment policy",
|
||||||
|
"USING_POLICY": "Using custom assignment policy for this inbox",
|
||||||
|
"CUSTOMIZE_POLICY": "Customize with assignment policy",
|
||||||
|
"DELETE_POLICY": "Delete policy",
|
||||||
|
"POLICY_LABEL": "Assignment policy",
|
||||||
|
"ASSIGNMENT_ORDER_LABEL": "Assignment Order",
|
||||||
|
"ASSIGNMENT_METHOD_LABEL": "Assignment Method",
|
||||||
|
"POLICY_STATUS": {
|
||||||
|
"ACTIVE": "Active",
|
||||||
|
"INACTIVE": "Inactive"
|
||||||
|
},
|
||||||
|
"PRIORITY": {
|
||||||
|
"EARLIEST_CREATED": "Earliest created",
|
||||||
|
"LONGEST_WAITING": "Longest waiting"
|
||||||
|
},
|
||||||
|
"METHOD": {
|
||||||
|
"ROUND_ROBIN": "Round robin",
|
||||||
|
"BALANCED": "Balanced assignment"
|
||||||
|
},
|
||||||
|
"UPGRADE_PROMPT": "Custom assignment policies are available on the Business plan",
|
||||||
|
"UPGRADE_TO_BUSINESS": "Upgrade to Business",
|
||||||
|
"DEFAULT_POLICY_LINKED": "Default policy linked",
|
||||||
|
"DEFAULT_POLICY_DESCRIPTION": "Link a custom assignment policy to customize how conversations are assigned to agents in this inbox.",
|
||||||
|
"LINK_EXISTING_POLICY": "Link existing policy",
|
||||||
|
"CREATE_NEW_POLICY": "Create new policy",
|
||||||
|
"NO_POLICIES": "No assignment policies found",
|
||||||
|
"VIEW_ALL_POLICIES": "View all policies",
|
||||||
|
"CURRENT_BEHAVIOR": "Currently using default assignment behavior:",
|
||||||
|
"LINK_SUCCESS": "Assignment policy linked successfully",
|
||||||
|
"LINK_ERROR": "Failed to link assignment policy"
|
||||||
|
},
|
||||||
|
"ASSIGNMENT_POLICY": {
|
||||||
|
"DELETE_CONFIRM_TITLE": "Delete assignment policy?",
|
||||||
|
"DELETE_CONFIRM_MESSAGE": "Are you sure you want to remove this assignment policy from this inbox? The inbox will revert to default assignment rules.",
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"CONFIRM_DELETE": "Delete",
|
||||||
|
"DELETE_SUCCESS": "Assignment policy removed successfully",
|
||||||
|
"DELETE_ERROR": "Failed to remove assignment policy"
|
||||||
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "Reauthorize",
|
"TITLE": "Reauthorize",
|
||||||
"SUBTITLE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
"SUBTITLE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
||||||
|
|||||||
@ -694,7 +694,8 @@
|
|||||||
"CREATE_BUTTON": "Create policy",
|
"CREATE_BUTTON": "Create policy",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Assignment policy created successfully",
|
"SUCCESS_MESSAGE": "Assignment policy created successfully",
|
||||||
"ERROR_MESSAGE": "Failed to create assignment policy"
|
"ERROR_MESSAGE": "Failed to create assignment policy",
|
||||||
|
"INBOX_LINKED": "Inbox has been linked to the policy"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EDIT": {
|
"EDIT": {
|
||||||
@ -708,6 +709,12 @@
|
|||||||
"CONFIRM_BUTTON_LABEL": "Continue",
|
"CONFIRM_BUTTON_LABEL": "Continue",
|
||||||
"CANCEL_BUTTON_LABEL": "Cancel"
|
"CANCEL_BUTTON_LABEL": "Cancel"
|
||||||
},
|
},
|
||||||
|
"INBOX_LINK_PROMPT": {
|
||||||
|
"TITLE": "Link inbox to policy",
|
||||||
|
"DESCRIPTION": "Would you like to link this inbox to the assignment policy?",
|
||||||
|
"LINK_BUTTON": "Link inbox",
|
||||||
|
"CANCEL_BUTTON": "Skip"
|
||||||
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Assignment policy updated successfully",
|
"SUCCESS_MESSAGE": "Assignment policy updated successfully",
|
||||||
"ERROR_MESSAGE": "Failed to update assignment policy"
|
"ERROR_MESSAGE": "Failed to update assignment policy"
|
||||||
@ -746,7 +753,9 @@
|
|||||||
},
|
},
|
||||||
"BALANCED": {
|
"BALANCED": {
|
||||||
"LABEL": "Balanced",
|
"LABEL": "Balanced",
|
||||||
"DESCRIPTION": "Assign conversations based on available capacity."
|
"DESCRIPTION": "Assign conversations based on available capacity.",
|
||||||
|
"PREMIUM_MESSAGE": "Upgrade to access balanced assignment and agent capacity management.",
|
||||||
|
"PREMIUM_BADGE": "Premium"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ASSIGNMENT_PRIORITY": {
|
"ASSIGNMENT_PRIORITY": {
|
||||||
@ -832,6 +841,20 @@
|
|||||||
"SUCCESS_MESSAGE": "Agent removed from policy successfully",
|
"SUCCESS_MESSAGE": "Agent removed from policy successfully",
|
||||||
"ERROR_MESSAGE": "Failed to remove agent from policy"
|
"ERROR_MESSAGE": "Failed to remove agent from policy"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"INBOX_LIMIT_API": {
|
||||||
|
"ADD": {
|
||||||
|
"SUCCESS_MESSAGE": "Inbox limit added successfully",
|
||||||
|
"ERROR_MESSAGE": "Failed to add inbox limit"
|
||||||
|
},
|
||||||
|
"UPDATE": {
|
||||||
|
"SUCCESS_MESSAGE": "Inbox limit updated successfully",
|
||||||
|
"ERROR_MESSAGE": "Failed to update inbox limit"
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"SUCCESS_MESSAGE": "Inbox limit deleted successfully",
|
||||||
|
"ERROR_MESSAGE": "Failed to delete inbox limit"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"FORM": {
|
"FORM": {
|
||||||
|
|||||||
@ -1,54 +1,81 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||||
import SettingsLayout from '../SettingsLayout.vue';
|
import SettingsLayout from '../SettingsLayout.vue';
|
||||||
import AssignmentCard from 'dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.vue';
|
import AssignmentCard from 'dashboard/components-next/AssignmentPolicy/AssignmentCard/AssignmentCard.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const agentAssignments = computed(() => [
|
const accountId = computed(() => Number(route.params.accountId));
|
||||||
{
|
const isFeatureEnabledonAccount = useMapGetter(
|
||||||
key: 'agent_assignment_policy_index',
|
'accounts/isFeatureEnabledonAccount'
|
||||||
title: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.TITLE'),
|
);
|
||||||
description: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.DESCRIPTION'),
|
|
||||||
features: [
|
const agentAssignments = computed(() => {
|
||||||
{
|
const assignments = [
|
||||||
icon: 'i-lucide-circle-fading-arrow-up',
|
{
|
||||||
label: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.FEATURES.0'),
|
key: 'agent_assignment_policy_index',
|
||||||
},
|
title: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.TITLE'),
|
||||||
{
|
description: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.DESCRIPTION'),
|
||||||
icon: 'i-lucide-scale',
|
features: [
|
||||||
label: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.FEATURES.1'),
|
{
|
||||||
},
|
icon: 'i-lucide-circle-fading-arrow-up',
|
||||||
{
|
label: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.FEATURES.0'),
|
||||||
icon: 'i-lucide-inbox',
|
},
|
||||||
label: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.FEATURES.2'),
|
{
|
||||||
},
|
icon: 'i-lucide-scale',
|
||||||
],
|
label: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.FEATURES.1'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'agent_capacity_policy_index',
|
icon: 'i-lucide-inbox',
|
||||||
title: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.TITLE'),
|
label: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.FEATURES.2'),
|
||||||
description: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.DESCRIPTION'),
|
},
|
||||||
features: [
|
],
|
||||||
{
|
},
|
||||||
icon: 'i-lucide-glass-water',
|
];
|
||||||
label: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.FEATURES.0'),
|
|
||||||
},
|
// Only show Agent Capacity if BOTH assignment_v2 AND advanced_assignment are enabled
|
||||||
{
|
// advanced_assignment identifies premium users
|
||||||
icon: 'i-lucide-circle-minus',
|
const hasAssignmentV2 = isFeatureEnabledonAccount.value(
|
||||||
label: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.FEATURES.1'),
|
accountId.value,
|
||||||
},
|
'assignment_v2'
|
||||||
{
|
);
|
||||||
icon: 'i-lucide-users-round',
|
const hasAdvancedAssignment = isFeatureEnabledonAccount.value(
|
||||||
label: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.FEATURES.2'),
|
accountId.value,
|
||||||
},
|
'advanced_assignment'
|
||||||
],
|
);
|
||||||
},
|
|
||||||
]);
|
if (hasAssignmentV2 && hasAdvancedAssignment) {
|
||||||
|
assignments.push({
|
||||||
|
key: 'agent_capacity_policy_index',
|
||||||
|
title: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.TITLE'),
|
||||||
|
description: t(
|
||||||
|
'ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.DESCRIPTION'
|
||||||
|
),
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
icon: 'i-lucide-glass-water',
|
||||||
|
label: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.FEATURES.0'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'i-lucide-circle-minus',
|
||||||
|
label: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.FEATURES.1'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'i-lucide-users-round',
|
||||||
|
label: t('ASSIGNMENT_POLICY.INDEX.AGENT_CAPACITY_POLICY.FEATURES.2'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return assignments;
|
||||||
|
});
|
||||||
|
|
||||||
const handleClick = key => {
|
const handleClick = key => {
|
||||||
router.push({ name: key });
|
router.push({ name: key });
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export default {
|
|||||||
name: 'agent_capacity_policy_index',
|
name: 'agent_capacity_policy_index',
|
||||||
component: AgentCapacityIndex,
|
component: AgentCapacityIndex,
|
||||||
meta: {
|
meta: {
|
||||||
featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2,
|
featureFlag: FEATURE_FLAGS.ADVANCED_ASSIGNMENT,
|
||||||
permissions: ['administrator'],
|
permissions: ['administrator'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -71,7 +71,7 @@ export default {
|
|||||||
name: 'agent_capacity_policy_create',
|
name: 'agent_capacity_policy_create',
|
||||||
component: AgentCapacityCreate,
|
component: AgentCapacityCreate,
|
||||||
meta: {
|
meta: {
|
||||||
featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2,
|
featureFlag: FEATURE_FLAGS.ADVANCED_ASSIGNMENT,
|
||||||
permissions: ['administrator'],
|
permissions: ['administrator'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -80,7 +80,7 @@ export default {
|
|||||||
name: 'agent_capacity_policy_edit',
|
name: 'agent_capacity_policy_edit',
|
||||||
component: AgentCapacityEdit,
|
component: AgentCapacityEdit,
|
||||||
meta: {
|
meta: {
|
||||||
featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2,
|
featureFlag: FEATURE_FLAGS.ADVANCED_ASSIGNMENT,
|
||||||
permissions: ['administrator'],
|
permissions: ['administrator'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,13 +2,14 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||||
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||||
import AssignmentPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue';
|
import AssignmentPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@ -16,20 +17,50 @@ const { t } = useI18n();
|
|||||||
const formRef = ref(null);
|
const formRef = ref(null);
|
||||||
const uiFlags = useMapGetter('assignmentPolicies/getUIFlags');
|
const uiFlags = useMapGetter('assignmentPolicies/getUIFlags');
|
||||||
|
|
||||||
const breadcrumbItems = computed(() => [
|
const inboxIdFromQuery = computed(() => {
|
||||||
{
|
const id = route.query.inboxId;
|
||||||
label: t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.HEADER.TITLE'),
|
return id ? Number(id) : null;
|
||||||
routeName: 'agent_assignment_policy_index',
|
});
|
||||||
},
|
|
||||||
{
|
const breadcrumbItems = computed(() => {
|
||||||
label: t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.CREATE.HEADER.TITLE'),
|
if (inboxIdFromQuery.value) {
|
||||||
},
|
return [
|
||||||
]);
|
{
|
||||||
|
label: t('INBOX_MGMT.SETTINGS'),
|
||||||
|
routeName: 'settings_inbox_show',
|
||||||
|
params: { inboxId: inboxIdFromQuery.value },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.CREATE.HEADER.TITLE'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
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 => {
|
const handleBreadcrumbClick = item => {
|
||||||
router.push({
|
if (item.params) {
|
||||||
name: item.routeName,
|
const accountId = route.params.accountId;
|
||||||
});
|
const inboxId = item.params.inboxId;
|
||||||
|
// Navigate using explicit path to ensure tab parameter is included
|
||||||
|
router.push(
|
||||||
|
`/app/accounts/${accountId}/settings/inboxes/${inboxId}/collaborators`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
router.push({
|
||||||
|
name: item.routeName,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async formState => {
|
const handleSubmit = async formState => {
|
||||||
@ -45,6 +76,8 @@ const handleSubmit = async formState => {
|
|||||||
params: {
|
params: {
|
||||||
id: policy.id,
|
id: policy.id,
|
||||||
},
|
},
|
||||||
|
// Pass inboxId to edit page to show link prompt
|
||||||
|
query: inboxIdFromQuery.value ? { inboxId: inboxIdFromQuery.value } : {},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
useAlert(
|
useAlert(
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
|||||||
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
|
||||||
import AssignmentPolicyForm from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.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';
|
import ConfirmInboxDialog from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmInboxDialog.vue';
|
||||||
|
import InboxLinkDialog from 'dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/InboxLinkDialog.vue';
|
||||||
|
|
||||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY';
|
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY';
|
||||||
|
|
||||||
@ -36,13 +37,46 @@ const confirmInboxDialogRef = ref(null);
|
|||||||
// Store the policy linked to the inbox when adding a new inbox
|
// Store the policy linked to the inbox when adding a new inbox
|
||||||
const inboxLinkedPolicy = ref(null);
|
const inboxLinkedPolicy = ref(null);
|
||||||
|
|
||||||
const breadcrumbItems = computed(() => [
|
// Inbox linking prompt from create flow
|
||||||
{
|
const inboxIdFromQuery = computed(() => {
|
||||||
label: t(`${BASE_KEY}.INDEX.HEADER.TITLE`),
|
const id = route.query.inboxId;
|
||||||
routeName: 'agent_assignment_policy_index',
|
return id ? Number(id) : null;
|
||||||
},
|
});
|
||||||
{ label: t(`${BASE_KEY}.EDIT.HEADER.TITLE`) },
|
|
||||||
]);
|
const suggestedInbox = computed(() => {
|
||||||
|
if (!inboxIdFromQuery.value || !inboxes.value) return null;
|
||||||
|
return inboxes.value.find(inbox => inbox.id === inboxIdFromQuery.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLinkingInbox = ref(false);
|
||||||
|
|
||||||
|
const dismissInboxLinkPrompt = () => {
|
||||||
|
router.replace({
|
||||||
|
name: route.name,
|
||||||
|
params: route.params,
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const breadcrumbItems = computed(() => {
|
||||||
|
if (inboxIdFromQuery.value) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('INBOX_MGMT.SETTINGS'),
|
||||||
|
routeName: 'settings_inbox_show',
|
||||||
|
params: { inboxId: inboxIdFromQuery.value },
|
||||||
|
},
|
||||||
|
{ label: t(`${BASE_KEY}.EDIT.HEADER.TITLE`) },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t(`${BASE_KEY}.INDEX.HEADER.TITLE`),
|
||||||
|
routeName: 'agent_assignment_policy_index',
|
||||||
|
},
|
||||||
|
{ label: t(`${BASE_KEY}.EDIT.HEADER.TITLE`) },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
const buildInboxList = allInboxes =>
|
const buildInboxList = allInboxes =>
|
||||||
allInboxes?.map(({ name, id, email, phoneNumber, channelType, medium }) => ({
|
allInboxes?.map(({ name, id, email, phoneNumber, channelType, medium }) => ({
|
||||||
@ -66,22 +100,48 @@ const inboxList = computed(() =>
|
|||||||
const formData = computed(() => ({
|
const formData = computed(() => ({
|
||||||
name: selectedPolicy.value?.name || '',
|
name: selectedPolicy.value?.name || '',
|
||||||
description: selectedPolicy.value?.description || '',
|
description: selectedPolicy.value?.description || '',
|
||||||
enabled: selectedPolicy.value?.enabled || false,
|
enabled: true,
|
||||||
assignmentOrder: selectedPolicy.value?.assignmentOrder || ROUND_ROBIN,
|
assignmentOrder: selectedPolicy.value?.assignmentOrder || ROUND_ROBIN,
|
||||||
conversationPriority:
|
conversationPriority:
|
||||||
selectedPolicy.value?.conversationPriority || EARLIEST_CREATED,
|
selectedPolicy.value?.conversationPriority || EARLIEST_CREATED,
|
||||||
fairDistributionLimit: selectedPolicy.value?.fairDistributionLimit || 10,
|
fairDistributionLimit: selectedPolicy.value?.fairDistributionLimit || 100,
|
||||||
fairDistributionWindow: selectedPolicy.value?.fairDistributionWindow || 60,
|
fairDistributionWindow: selectedPolicy.value?.fairDistributionWindow || 3600,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleDeleteInbox = inboxId =>
|
const handleDeleteInbox = async inboxId => {
|
||||||
store.dispatch('assignmentPolicies/removeInboxPolicy', {
|
try {
|
||||||
policyId: selectedPolicy.value?.id,
|
await store.dispatch('assignmentPolicies/removeInboxPolicy', {
|
||||||
inboxId,
|
policyId: selectedPolicy.value?.id,
|
||||||
});
|
inboxId,
|
||||||
|
});
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.INBOX_API.REMOVE.SUCCESS_MESSAGE`));
|
||||||
|
} catch {
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.INBOX_API.REMOVE.ERROR_MESSAGE`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleBreadcrumbClick = ({ routeName }) =>
|
const handleBreadcrumbClick = ({ routeName, params }) => {
|
||||||
router.push({ name: routeName });
|
if (params) {
|
||||||
|
const accountId = route.params.accountId;
|
||||||
|
const inboxId = params.inboxId;
|
||||||
|
// Navigate using explicit path to ensure tab parameter is included
|
||||||
|
router.push(
|
||||||
|
`/app/accounts/${accountId}/settings/inboxes/${inboxId}/collaborators`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
router.push({ name: routeName });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigateToInbox = inbox => {
|
||||||
|
router.push({
|
||||||
|
name: 'settings_inbox_show',
|
||||||
|
params: {
|
||||||
|
accountId: route.params.accountId,
|
||||||
|
inboxId: inbox.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const setInboxPolicy = async (inboxId, policyId) => {
|
const setInboxPolicy = async (inboxId, policyId) => {
|
||||||
try {
|
try {
|
||||||
@ -122,6 +182,26 @@ const handleAddInbox = async inbox => {
|
|||||||
await setInboxPolicy(inbox?.id, selectedPolicy.value?.id);
|
await setInboxPolicy(inbox?.id, selectedPolicy.value?.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLinkSuggestedInbox = async () => {
|
||||||
|
if (!suggestedInbox.value) return;
|
||||||
|
|
||||||
|
isLinkingInbox.value = true;
|
||||||
|
const inbox = {
|
||||||
|
id: suggestedInbox.value.id,
|
||||||
|
name: suggestedInbox.value.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
await handleAddInbox(inbox);
|
||||||
|
|
||||||
|
// Clear the query param after linking
|
||||||
|
router.replace({
|
||||||
|
name: route.name,
|
||||||
|
params: route.params,
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
isLinkingInbox.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const handleConfirmAddInbox = async inboxId => {
|
const handleConfirmAddInbox = async inboxId => {
|
||||||
const success = await setInboxPolicy(inboxId, selectedPolicy.value?.id);
|
const success = await setInboxPolicy(inboxId, selectedPolicy.value?.id);
|
||||||
|
|
||||||
@ -155,6 +235,11 @@ const handleSubmit = async formState => {
|
|||||||
const fetchPolicyData = async () => {
|
const fetchPolicyData = async () => {
|
||||||
if (!routeId.value) return;
|
if (!routeId.value) return;
|
||||||
|
|
||||||
|
// Fetch inboxes if not already loaded (needed for inbox link prompt)
|
||||||
|
if (!inboxes.value?.length) {
|
||||||
|
store.dispatch('inboxes/get');
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch policy if not available
|
// Fetch policy if not available
|
||||||
if (!selectedPolicy.value?.id)
|
if (!selectedPolicy.value?.id)
|
||||||
await store.dispatch('assignmentPolicies/show', routeId.value);
|
await store.dispatch('assignmentPolicies/show', routeId.value);
|
||||||
@ -186,6 +271,7 @@ watch(routeId, fetchPolicyData, { immediate: true });
|
|||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
@add-inbox="handleAddInbox"
|
@add-inbox="handleAddInbox"
|
||||||
@delete-inbox="handleDeleteInbox"
|
@delete-inbox="handleDeleteInbox"
|
||||||
|
@navigate-to-inbox="handleNavigateToInbox"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -193,5 +279,12 @@ watch(routeId, fetchPolicyData, { immediate: true });
|
|||||||
ref="confirmInboxDialogRef"
|
ref="confirmInboxDialogRef"
|
||||||
@add="handleConfirmAddInbox"
|
@add="handleConfirmAddInbox"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<InboxLinkDialog
|
||||||
|
:inbox="suggestedInbox"
|
||||||
|
:is-linking="isLinkingInbox"
|
||||||
|
@link="handleLinkSuggestedInbox"
|
||||||
|
@dismiss="dismissInboxLinkPrompt"
|
||||||
|
/>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -92,43 +92,68 @@ const formData = computed(() => ({
|
|||||||
const handleBreadcrumbClick = ({ routeName }) =>
|
const handleBreadcrumbClick = ({ routeName }) =>
|
||||||
router.push({ name: routeName });
|
router.push({ name: routeName });
|
||||||
|
|
||||||
const handleDeleteUser = agentId => {
|
const handleDeleteUser = async agentId => {
|
||||||
store.dispatch('agentCapacityPolicies/removeUser', {
|
try {
|
||||||
policyId: selectedPolicyId.value,
|
await store.dispatch('agentCapacityPolicies/removeUser', {
|
||||||
userId: agentId,
|
policyId: selectedPolicyId.value,
|
||||||
});
|
userId: agentId,
|
||||||
|
});
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.AGENT_API.REMOVE.SUCCESS_MESSAGE`));
|
||||||
|
} catch {
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.AGENT_API.REMOVE.ERROR_MESSAGE`));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddUser = agent => {
|
const handleAddUser = async agent => {
|
||||||
store.dispatch('agentCapacityPolicies/addUser', {
|
try {
|
||||||
policyId: selectedPolicyId.value,
|
await store.dispatch('agentCapacityPolicies/addUser', {
|
||||||
userData: { id: agent.id, capacity: 20 },
|
policyId: selectedPolicyId.value,
|
||||||
});
|
userData: { id: agent.id, capacity: 20 },
|
||||||
|
});
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.AGENT_API.ADD.SUCCESS_MESSAGE`));
|
||||||
|
} catch {
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.AGENT_API.ADD.ERROR_MESSAGE`));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteInboxLimit = limitId => {
|
const handleDeleteInboxLimit = async limitId => {
|
||||||
store.dispatch('agentCapacityPolicies/deleteInboxLimit', {
|
try {
|
||||||
policyId: selectedPolicyId.value,
|
await store.dispatch('agentCapacityPolicies/deleteInboxLimit', {
|
||||||
limitId,
|
policyId: selectedPolicyId.value,
|
||||||
});
|
limitId,
|
||||||
|
});
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.DELETE.SUCCESS_MESSAGE`));
|
||||||
|
} catch {
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.DELETE.ERROR_MESSAGE`));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddInboxLimit = limit => {
|
const handleAddInboxLimit = async limit => {
|
||||||
store.dispatch('agentCapacityPolicies/createInboxLimit', {
|
try {
|
||||||
policyId: selectedPolicyId.value,
|
await store.dispatch('agentCapacityPolicies/createInboxLimit', {
|
||||||
limitData: {
|
policyId: selectedPolicyId.value,
|
||||||
inboxId: limit.inboxId,
|
limitData: {
|
||||||
conversationLimit: limit.conversationLimit,
|
inboxId: limit.inboxId,
|
||||||
},
|
conversationLimit: limit.conversationLimit,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.ADD.SUCCESS_MESSAGE`));
|
||||||
|
} catch {
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.ADD.ERROR_MESSAGE`));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLimitChange = limit => {
|
const handleLimitChange = async limit => {
|
||||||
store.dispatch('agentCapacityPolicies/updateInboxLimit', {
|
try {
|
||||||
policyId: selectedPolicyId.value,
|
await store.dispatch('agentCapacityPolicies/updateInboxLimit', {
|
||||||
limitId: limit.id,
|
policyId: selectedPolicyId.value,
|
||||||
limitData: { conversationLimit: limit.conversationLimit },
|
limitId: limit.id,
|
||||||
});
|
limitData: { conversationLimit: limit.conversationLimit },
|
||||||
|
});
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.UPDATE.SUCCESS_MESSAGE`));
|
||||||
|
} catch {
|
||||||
|
useAlert(t(`${BASE_KEY}.EDIT.INBOX_LIMIT_API.UPDATE.ERROR_MESSAGE`));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async formState => {
|
const handleSubmit = async formState => {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref, watch } from 'vue';
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useConfig } from 'dashboard/composables/useConfig';
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
import BaseInfo from 'dashboard/components-next/AssignmentPolicy/components/BaseInfo.vue';
|
import BaseInfo from 'dashboard/components-next/AssignmentPolicy/components/BaseInfo.vue';
|
||||||
import RadioCard from 'dashboard/components-next/AssignmentPolicy/components/RadioCard.vue';
|
import RadioCard from 'dashboard/components-next/AssignmentPolicy/components/RadioCard.vue';
|
||||||
import FairDistribution from 'dashboard/components-next/AssignmentPolicy/components/FairDistribution.vue';
|
import FairDistribution from 'dashboard/components-next/AssignmentPolicy/components/FairDistribution.vue';
|
||||||
@ -23,7 +24,6 @@ const props = defineProps({
|
|||||||
default: () => ({
|
default: () => ({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
enabled: false,
|
|
||||||
assignmentOrder: ROUND_ROBIN,
|
assignmentOrder: ROUND_ROBIN,
|
||||||
conversationPriority: EARLIEST_CREATED,
|
conversationPriority: EARLIEST_CREATED,
|
||||||
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||||
@ -61,18 +61,24 @@ const emit = defineEmits([
|
|||||||
'submit',
|
'submit',
|
||||||
'addInbox',
|
'addInbox',
|
||||||
'deleteInbox',
|
'deleteInbox',
|
||||||
|
'navigateToInbox',
|
||||||
'validationChange',
|
'validationChange',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { isEnterprise } = useConfig();
|
const route = useRoute();
|
||||||
|
|
||||||
|
const accountId = computed(() => Number(route.params.accountId));
|
||||||
|
const isFeatureEnabledonAccount = useMapGetter(
|
||||||
|
'accounts/isFeatureEnabledonAccount'
|
||||||
|
);
|
||||||
|
|
||||||
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY';
|
const BASE_KEY = 'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY';
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
enabled: false,
|
enabled: true,
|
||||||
assignmentOrder: ROUND_ROBIN,
|
assignmentOrder: ROUND_ROBIN,
|
||||||
conversationPriority: EARLIEST_CREATED,
|
conversationPriority: EARLIEST_CREATED,
|
||||||
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||||
@ -83,20 +89,42 @@ const validationState = ref({
|
|||||||
isValid: false,
|
isValid: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createOption = (type, key, stateKey) => ({
|
const createOption = (
|
||||||
|
type,
|
||||||
|
key,
|
||||||
|
stateKey,
|
||||||
|
disabled = false,
|
||||||
|
disabledMessage = ''
|
||||||
|
) => ({
|
||||||
key,
|
key,
|
||||||
label: t(`${BASE_KEY}.FORM.${type}.${key.toUpperCase()}.LABEL`),
|
label: t(`${BASE_KEY}.FORM.${type}.${key.toUpperCase()}.LABEL`),
|
||||||
description: t(`${BASE_KEY}.FORM.${type}.${key.toUpperCase()}.DESCRIPTION`),
|
description: t(`${BASE_KEY}.FORM.${type}.${key.toUpperCase()}.DESCRIPTION`),
|
||||||
isActive: state[stateKey] === key,
|
isActive: state[stateKey] === key,
|
||||||
|
disabled,
|
||||||
|
disabledMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const assignmentOrderOptions = computed(() => {
|
const assignmentOrderOptions = computed(() => {
|
||||||
const options = OPTIONS.ORDER.filter(
|
const hasAdvancedAssignment = isFeatureEnabledonAccount.value(
|
||||||
key => isEnterprise || key !== 'balanced'
|
accountId.value,
|
||||||
);
|
'advanced_assignment'
|
||||||
return options.map(key =>
|
|
||||||
createOption('ASSIGNMENT_ORDER', key, 'assignmentOrder')
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return OPTIONS.ORDER.map(key => {
|
||||||
|
const isBalanced = key === 'balanced';
|
||||||
|
const disabled = isBalanced && !hasAdvancedAssignment;
|
||||||
|
const disabledMessage = disabled
|
||||||
|
? t(`${BASE_KEY}.FORM.ASSIGNMENT_ORDER.BALANCED.PREMIUM_MESSAGE`)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return createOption(
|
||||||
|
'ASSIGNMENT_ORDER',
|
||||||
|
key,
|
||||||
|
'assignmentOrder',
|
||||||
|
disabled,
|
||||||
|
disabledMessage
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const assignmentPriorityOptions = computed(() =>
|
const assignmentPriorityOptions = computed(() =>
|
||||||
@ -131,7 +159,7 @@ const resetForm = () => {
|
|||||||
Object.assign(state, {
|
Object.assign(state, {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
enabled: false,
|
enabled: true,
|
||||||
assignmentOrder: ROUND_ROBIN,
|
assignmentOrder: ROUND_ROBIN,
|
||||||
conversationPriority: EARLIEST_CREATED,
|
conversationPriority: EARLIEST_CREATED,
|
||||||
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
|
||||||
@ -162,15 +190,10 @@ defineExpose({
|
|||||||
<BaseInfo
|
<BaseInfo
|
||||||
v-model:policy-name="state.name"
|
v-model:policy-name="state.name"
|
||||||
v-model:description="state.description"
|
v-model:description="state.description"
|
||||||
v-model:enabled="state.enabled"
|
|
||||||
:name-label="t(`${BASE_KEY}.FORM.NAME.LABEL`)"
|
:name-label="t(`${BASE_KEY}.FORM.NAME.LABEL`)"
|
||||||
:name-placeholder="t(`${BASE_KEY}.FORM.NAME.PLACEHOLDER`)"
|
:name-placeholder="t(`${BASE_KEY}.FORM.NAME.PLACEHOLDER`)"
|
||||||
:description-label="t(`${BASE_KEY}.FORM.DESCRIPTION.LABEL`)"
|
:description-label="t(`${BASE_KEY}.FORM.DESCRIPTION.LABEL`)"
|
||||||
:description-placeholder="t(`${BASE_KEY}.FORM.DESCRIPTION.PLACEHOLDER`)"
|
: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"
|
@validation-change="handleValidationChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -193,6 +216,8 @@ defineExpose({
|
|||||||
:label="option.label"
|
:label="option.label"
|
||||||
:description="option.description"
|
:description="option.description"
|
||||||
:is-active="option.isActive"
|
:is-active="option.isActive"
|
||||||
|
:disabled="option.disabled"
|
||||||
|
:disabled-message="option.disabledMessage"
|
||||||
@select="state[section.key] = $event"
|
@select="state[section.key] = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -251,6 +276,7 @@ defineExpose({
|
|||||||
:is-fetching="isInboxLoading"
|
:is-fetching="isInboxLoading"
|
||||||
:empty-state-message="t(`${BASE_KEY}.FORM.INBOXES.EMPTY_STATE`)"
|
:empty-state-message="t(`${BASE_KEY}.FORM.INBOXES.EMPTY_STATE`)"
|
||||||
@delete="$emit('deleteInbox', $event)"
|
@delete="$emit('deleteInbox', $event)"
|
||||||
|
@navigate="$emit('navigateToInbox', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -0,0 +1,116 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { getInboxIconByType } from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
inbox: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
isLinking: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['link', 'dismiss']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const dialogRef = ref(null);
|
||||||
|
|
||||||
|
const inboxName = computed(() => props.inbox?.name || '');
|
||||||
|
|
||||||
|
const inboxIcon = computed(() => {
|
||||||
|
if (!props.inbox) return 'i-lucide-inbox';
|
||||||
|
return getInboxIconByType(
|
||||||
|
props.inbox.channelType,
|
||||||
|
props.inbox.medium,
|
||||||
|
'line'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const openDialog = () => {
|
||||||
|
dialogRef.value?.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
dialogRef.value?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emit('link');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('dismiss');
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.inbox,
|
||||||
|
async newInbox => {
|
||||||
|
if (newInbox) {
|
||||||
|
await nextTick();
|
||||||
|
openDialog();
|
||||||
|
} else {
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose({ openDialog, closeDialog });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
type="edit"
|
||||||
|
:title="
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.INBOX_LINK_PROMPT.TITLE'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:confirm-button-label="
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.INBOX_LINK_PROMPT.LINK_BUTTON'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:cancel-button-label="
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.INBOX_LINK_PROMPT.CANCEL_BUTTON'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:is-loading="isLinking"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
<p class="text-sm text-n-slate-11">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.EDIT.INBOX_LINK_PROMPT.DESCRIPTION'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 p-3 rounded-xl border border-n-weak bg-n-alpha-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 size-10 rounded-lg bg-n-alpha-2 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i :class="inboxIcon" class="text-lg text-n-slate-11" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col min-w-0">
|
||||||
|
<span class="text-sm font-medium text-n-slate-12 truncate">
|
||||||
|
{{ inboxName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@ -1,122 +1,321 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { mapGetters } from 'vuex';
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { minValue } from '@vuelidate/validators';
|
import { minValue } from '@vuelidate/validators';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useConfig } from 'dashboard/composables/useConfig';
|
import { useConfig } from 'dashboard/composables/useConfig';
|
||||||
import SettingsSection from '../../../../../components/SettingsSection.vue';
|
import SettingsSection from '../../../../../components/SettingsSection.vue';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Switch from 'dashboard/components-next/switch/Switch.vue';
|
||||||
|
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||||
|
import assignmentPoliciesAPI from 'dashboard/api/assignmentPolicies';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
components: {
|
inbox: {
|
||||||
SettingsSection,
|
type: Object,
|
||||||
NextButton,
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
props: {
|
});
|
||||||
inbox: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const { isEnterprise } = useConfig();
|
|
||||||
|
|
||||||
return { v$: useVuelidate(), isEnterprise };
|
const store = useStore();
|
||||||
},
|
const route = useRoute();
|
||||||
data() {
|
const router = useRouter();
|
||||||
return {
|
const { t } = useI18n();
|
||||||
selectedAgents: [],
|
const { isEnterprise } = useConfig();
|
||||||
isAgentListUpdating: false,
|
|
||||||
enableAutoAssignment: false,
|
const selectedAgents = ref([]);
|
||||||
maxAssignmentLimit: null,
|
const isAgentListUpdating = ref(false);
|
||||||
};
|
const enableAutoAssignment = ref(false);
|
||||||
},
|
const maxAssignmentLimit = ref(null);
|
||||||
computed: {
|
const assignmentPolicy = ref(null);
|
||||||
...mapGetters({
|
const isLoadingPolicy = ref(false);
|
||||||
agentList: 'agents/getAgents',
|
const isDeletingPolicy = ref(false);
|
||||||
}),
|
const showDeleteConfirmModal = ref(false);
|
||||||
maxAssignmentLimitErrors() {
|
const availablePolicies = ref([]);
|
||||||
if (this.v$.maxAssignmentLimit.$error) {
|
const isLoadingPolicies = ref(false);
|
||||||
return this.$t(
|
const showPolicyDropdown = ref(false);
|
||||||
'INBOX_MGMT.AUTO_ASSIGNMENT.MAX_ASSIGNMENT_LIMIT_RANGE_ERROR'
|
const isLinkingPolicy = ref(false);
|
||||||
);
|
|
||||||
}
|
const agentList = computed(() => store.getters['agents/getAgents']);
|
||||||
return '';
|
|
||||||
},
|
const isFeatureEnabled = feature => {
|
||||||
},
|
const accountId = Number(route.params.accountId);
|
||||||
watch: {
|
return store.getters['accounts/isFeatureEnabledonAccount'](
|
||||||
inbox() {
|
accountId,
|
||||||
this.setDefaults();
|
feature
|
||||||
},
|
);
|
||||||
},
|
};
|
||||||
mounted() {
|
|
||||||
this.setDefaults();
|
const hasAdvancedAssignment = computed(() => {
|
||||||
},
|
return isFeatureEnabled('advanced_assignment');
|
||||||
methods: {
|
});
|
||||||
setDefaults() {
|
|
||||||
this.enableAutoAssignment = this.inbox.enable_auto_assignment;
|
const hasAssignmentV2 = computed(() => {
|
||||||
this.maxAssignmentLimit =
|
return isFeatureEnabled('assignment_v2');
|
||||||
this.inbox?.auto_assignment_config?.max_assignment_limit || null;
|
});
|
||||||
this.fetchAttachedAgents();
|
|
||||||
},
|
const showAdvancedAssignmentUI = computed(() => {
|
||||||
async fetchAttachedAgents() {
|
return hasAdvancedAssignment.value && hasAssignmentV2.value;
|
||||||
try {
|
});
|
||||||
const response = await this.$store.dispatch('inboxMembers/get', {
|
|
||||||
inboxId: this.inbox.id,
|
const assignmentOrderLabel = computed(() => {
|
||||||
});
|
if (!assignmentPolicy.value) return '';
|
||||||
const {
|
const priority = assignmentPolicy.value.conversation_priority;
|
||||||
data: { payload: inboxMembers },
|
if (priority === 'earliest_created') {
|
||||||
} = response;
|
return t('INBOX_MGMT.ASSIGNMENT.PRIORITY.EARLIEST_CREATED');
|
||||||
this.selectedAgents = inboxMembers;
|
}
|
||||||
} catch (error) {
|
if (priority === 'longest_waiting') {
|
||||||
// Handle error
|
return t('INBOX_MGMT.ASSIGNMENT.PRIORITY.LONGEST_WAITING');
|
||||||
}
|
}
|
||||||
},
|
return priority;
|
||||||
handleEnableAutoAssignment() {
|
});
|
||||||
this.updateInbox();
|
|
||||||
},
|
const assignmentMethodLabel = computed(() => {
|
||||||
async updateAgents() {
|
if (!assignmentPolicy.value) return '';
|
||||||
const agentList = this.selectedAgents.map(el => el.id);
|
const order = assignmentPolicy.value.assignment_order;
|
||||||
this.isAgentListUpdating = true;
|
if (order === 'round_robin') {
|
||||||
try {
|
return t('INBOX_MGMT.ASSIGNMENT.METHOD.ROUND_ROBIN');
|
||||||
await this.$store.dispatch('inboxMembers/create', {
|
}
|
||||||
inboxId: this.inbox.id,
|
if (order === 'balanced') {
|
||||||
agentList,
|
return t('INBOX_MGMT.ASSIGNMENT.METHOD.BALANCED');
|
||||||
});
|
}
|
||||||
useAlert(this.$t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
return order;
|
||||||
} catch (error) {
|
});
|
||||||
useAlert(this.$t('AGENT_MGMT.EDIT.API.ERROR_MESSAGE'));
|
|
||||||
}
|
// Vuelidate validation rules
|
||||||
this.isAgentListUpdating = false;
|
const rules = {
|
||||||
},
|
maxAssignmentLimit: {
|
||||||
async updateInbox() {
|
minValue: minValue(1),
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
id: this.inbox.id,
|
|
||||||
formData: false,
|
|
||||||
enable_auto_assignment: this.enableAutoAssignment,
|
|
||||||
auto_assignment_config: {
|
|
||||||
max_assignment_limit: this.maxAssignmentLimit,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await this.$store.dispatch('inboxes/updateInbox', payload);
|
|
||||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
|
||||||
} catch (error) {
|
|
||||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
validations: {
|
|
||||||
selectedAgents: {
|
|
||||||
isEmpty() {
|
|
||||||
return !!this.selectedAgents.length;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
maxAssignmentLimit: {
|
|
||||||
minValue: minValue(1),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(rules, { maxAssignmentLimit });
|
||||||
|
|
||||||
|
const maxAssignmentLimitErrors = computed(() => {
|
||||||
|
if (v$.value.maxAssignmentLimit.$error) {
|
||||||
|
return t('INBOX_MGMT.AUTO_ASSIGNMENT.MAX_ASSIGNMENT_LIMIT_RANGE_ERROR');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchAttachedAgents = async () => {
|
||||||
|
try {
|
||||||
|
const response = await store.dispatch('inboxMembers/get', {
|
||||||
|
inboxId: props.inbox.id,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
data: { payload: inboxMembers },
|
||||||
|
} = response;
|
||||||
|
selectedAgents.value = inboxMembers;
|
||||||
|
} catch (error) {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAssignmentPolicy = async () => {
|
||||||
|
if (!props.inbox.id) return;
|
||||||
|
|
||||||
|
isLoadingPolicy.value = true;
|
||||||
|
try {
|
||||||
|
const response = await assignmentPoliciesAPI.getInboxPolicy(props.inbox.id);
|
||||||
|
assignmentPolicy.value = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
// No policy attached, which is fine
|
||||||
|
assignmentPolicy.value = null;
|
||||||
|
} finally {
|
||||||
|
isLoadingPolicy.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAvailablePolicies = async () => {
|
||||||
|
isLoadingPolicies.value = true;
|
||||||
|
try {
|
||||||
|
const response = await assignmentPoliciesAPI.get();
|
||||||
|
availablePolicies.value = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
availablePolicies.value = [];
|
||||||
|
} finally {
|
||||||
|
isLoadingPolicies.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkPolicyToInbox = async policy => {
|
||||||
|
isLinkingPolicy.value = true;
|
||||||
|
try {
|
||||||
|
await assignmentPoliciesAPI.setInboxPolicy(props.inbox.id, policy.id);
|
||||||
|
assignmentPolicy.value = policy;
|
||||||
|
showPolicyDropdown.value = false;
|
||||||
|
useAlert(t('INBOX_MGMT.ASSIGNMENT.LINK_SUCCESS'));
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t('INBOX_MGMT.ASSIGNMENT.LINK_ERROR'));
|
||||||
|
} finally {
|
||||||
|
isLinkingPolicy.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToAssignmentPolicies = () => {
|
||||||
|
const accountId = route.params.accountId;
|
||||||
|
router.push({
|
||||||
|
name: 'agent_assignment_policy_index',
|
||||||
|
params: { accountId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const policyMenuItems = computed(() => {
|
||||||
|
const items = availablePolicies.value.map(policy => ({
|
||||||
|
action: 'select_policy',
|
||||||
|
value: policy.id,
|
||||||
|
label: policy.name,
|
||||||
|
icon: 'i-lucide-zap',
|
||||||
|
policy,
|
||||||
|
}));
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
action: 'view_all',
|
||||||
|
value: 'view_all',
|
||||||
|
label: t('INBOX_MGMT.ASSIGNMENT.VIEW_ALL_POLICIES'),
|
||||||
|
icon: 'i-lucide-arrow-right',
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePolicyMenuAction = ({ action, policy }) => {
|
||||||
|
if (action === 'select_policy' && policy) {
|
||||||
|
linkPolicyToInbox(policy);
|
||||||
|
} else if (action === 'view_all') {
|
||||||
|
navigateToAssignmentPolicies();
|
||||||
|
}
|
||||||
|
showPolicyDropdown.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePolicyDropdown = () => {
|
||||||
|
if (!showPolicyDropdown.value && availablePolicies.value.length === 0) {
|
||||||
|
fetchAvailablePolicies();
|
||||||
|
}
|
||||||
|
showPolicyDropdown.value = !showPolicyDropdown.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closePolicyDropdown = () => {
|
||||||
|
showPolicyDropdown.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAutoAssignment = async () => {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
id: props.inbox.id,
|
||||||
|
formData: false,
|
||||||
|
enable_auto_assignment: enableAutoAssignment.value,
|
||||||
|
};
|
||||||
|
await store.dispatch('inboxes/updateInbox', payload);
|
||||||
|
useAlert(t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAgents = async () => {
|
||||||
|
const agentListIds = selectedAgents.value.map(el => el.id);
|
||||||
|
isAgentListUpdating.value = true;
|
||||||
|
try {
|
||||||
|
await store.dispatch('inboxMembers/create', {
|
||||||
|
inboxId: props.inbox.id,
|
||||||
|
agentList: agentListIds,
|
||||||
|
});
|
||||||
|
useAlert(t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t('AGENT_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
isAgentListUpdating.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateInbox = async () => {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
id: props.inbox.id,
|
||||||
|
formData: false,
|
||||||
|
enable_auto_assignment: enableAutoAssignment.value,
|
||||||
|
auto_assignment_config: {
|
||||||
|
max_assignment_limit: maxAssignmentLimit.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await store.dispatch('inboxes/updateInbox', payload);
|
||||||
|
useAlert(t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToCreatePolicy = () => {
|
||||||
|
const accountId = route.params.accountId;
|
||||||
|
router.push({
|
||||||
|
name: 'agent_assignment_policy_create',
|
||||||
|
params: { accountId },
|
||||||
|
query: { inboxId: props.inbox.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToAssignmentPolicyEdit = () => {
|
||||||
|
if (!assignmentPolicy.value?.id) return;
|
||||||
|
const accountId = route.params.accountId;
|
||||||
|
router.push({
|
||||||
|
name: 'agent_assignment_policy_edit',
|
||||||
|
params: { accountId, id: assignmentPolicy.value.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToBilling = () => {
|
||||||
|
const accountId = route.params.accountId;
|
||||||
|
router.push({
|
||||||
|
name: 'billing_settings_index',
|
||||||
|
params: { accountId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDeletePolicy = () => {
|
||||||
|
showDeleteConfirmModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelDeletePolicy = () => {
|
||||||
|
showDeleteConfirmModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAssignmentPolicy = async () => {
|
||||||
|
if (isDeletingPolicy.value) return;
|
||||||
|
isDeletingPolicy.value = true;
|
||||||
|
try {
|
||||||
|
await assignmentPoliciesAPI.removeInboxPolicy(props.inbox.id);
|
||||||
|
assignmentPolicy.value = null;
|
||||||
|
showDeleteConfirmModal.value = false;
|
||||||
|
useAlert(t('INBOX_MGMT.ASSIGNMENT_POLICY.DELETE_SUCCESS'));
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t('INBOX_MGMT.ASSIGNMENT_POLICY.DELETE_ERROR'));
|
||||||
|
} finally {
|
||||||
|
isDeletingPolicy.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDefaults = () => {
|
||||||
|
enableAutoAssignment.value = props.inbox.enable_auto_assignment;
|
||||||
|
maxAssignmentLimit.value =
|
||||||
|
props.inbox.auto_assignment_config?.max_assignment_limit || null;
|
||||||
|
fetchAttachedAgents();
|
||||||
|
if (showAdvancedAssignmentUI.value) {
|
||||||
|
fetchAssignmentPolicy();
|
||||||
|
fetchAvailablePolicies();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch only inbox.id to avoid unnecessary refetches when other properties change
|
||||||
|
watch(() => props.inbox.id, setDefaults);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setDefaults();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -138,7 +337,6 @@ export default {
|
|||||||
selected-label
|
selected-label
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
||||||
@select="v$.selectedAgents.$touch"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NextButton
|
<NextButton
|
||||||
@ -152,44 +350,325 @@ export default {
|
|||||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.AGENT_ASSIGNMENT')"
|
:title="$t('INBOX_MGMT.SETTINGS_POPUP.AGENT_ASSIGNMENT')"
|
||||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.AGENT_ASSIGNMENT_SUB_TEXT')"
|
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.AGENT_ASSIGNMENT_SUB_TEXT')"
|
||||||
>
|
>
|
||||||
<label class="w-3/4 settings-item">
|
<!-- New UI for assignment_v2 -->
|
||||||
<div class="flex items-center gap-2">
|
<template v-if="hasAssignmentV2">
|
||||||
<input
|
<div class="flex items-start gap-3">
|
||||||
id="enableAutoAssignment"
|
<Switch
|
||||||
v-model="enableAutoAssignment"
|
v-model="enableAutoAssignment"
|
||||||
type="checkbox"
|
class="flex-shrink-0 mt-0.5"
|
||||||
@change="handleEnableAutoAssignment"
|
@change="handleToggleAutoAssignment"
|
||||||
/>
|
/>
|
||||||
<label for="enableAutoAssignment">
|
<div class="flex-grow">
|
||||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT') }}
|
<label class="text-sm text-n-slate-12 font-medium mb-1">
|
||||||
</label>
|
{{ $t('INBOX_MGMT.ASSIGNMENT.ENABLE_AUTO_ASSIGNMENT') }}
|
||||||
|
</label>
|
||||||
|
<p class="text-sm text-n-slate-11">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.DESCRIPTION') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="pb-1 text-sm not-italic text-n-slate-11">
|
<Transition
|
||||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT_SUB_TEXT') }}
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
</p>
|
enter-from-class="opacity-0 -translate-y-2"
|
||||||
</label>
|
enter-to-class="opacity-100 translate-y-0"
|
||||||
|
leave-active-class="transition-all duration-150 ease-in"
|
||||||
|
leave-from-class="opacity-100 translate-y-0"
|
||||||
|
leave-to-class="opacity-0 -translate-y-2"
|
||||||
|
>
|
||||||
|
<div v-if="enableAutoAssignment" class="mt-6">
|
||||||
|
<!-- Policy Card - When policy is attached -->
|
||||||
|
<div
|
||||||
|
v-if="showAdvancedAssignmentUI && assignmentPolicy"
|
||||||
|
class="p-4 rounded-xl outline-1 outline-n-weak outline bg-n-solid-1 dark:bg-n-slate-1"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 size-12 rounded-xl bg-n-slate-3 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span class="i-lucide-zap text-xl text-n-slate-11" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<div class="flex items-start justify-between gap-4 mb-4">
|
||||||
|
<div class="flex flex-col items-start">
|
||||||
|
<span class="text-base font-medium text-n-slate-12 mb-1">
|
||||||
|
{{ assignmentPolicy.name }}
|
||||||
|
</span>
|
||||||
|
<p class="text-sm text-n-slate-11">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.POLICY_LABEL') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<NextButton
|
||||||
|
icon="i-lucide-trash-2"
|
||||||
|
ghost
|
||||||
|
ruby
|
||||||
|
sm
|
||||||
|
@click="confirmDeletePolicy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="enableAutoAssignment && isEnterprise" class="py-3">
|
<ul class="space-y-2 mb-6">
|
||||||
<woot-input
|
<li class="flex items-center gap-2">
|
||||||
v-model="maxAssignmentLimit"
|
<span
|
||||||
type="number"
|
class="w-1.5 h-1.5 rounded-full bg-n-slate-11 flex-shrink-0"
|
||||||
:class="{ error: v$.maxAssignmentLimit.$error }"
|
/>
|
||||||
:error="maxAssignmentLimitErrors"
|
<span class="text-sm text-n-slate-12">
|
||||||
:label="$t('INBOX_MGMT.AUTO_ASSIGNMENT.MAX_ASSIGNMENT_LIMIT')"
|
{{ assignmentOrderLabel }}
|
||||||
@blur="v$.maxAssignmentLimit.$touch"
|
</span>
|
||||||
/>
|
</li>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="w-1.5 h-1.5 rounded-full bg-n-slate-11 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-n-slate-12">
|
||||||
|
{{ assignmentMethodLabel }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<p class="pb-1 text-sm not-italic text-n-slate-11">
|
<div class="w-full h-px my-4 bg-n-weak" />
|
||||||
{{ $t('INBOX_MGMT.AUTO_ASSIGNMENT.MAX_ASSIGNMENT_LIMIT_SUB_TEXT') }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<NextButton
|
<NextButton
|
||||||
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
:label="$t('INBOX_MGMT.ASSIGNMENT.CUSTOMIZE_POLICY')"
|
||||||
:disabled="v$.maxAssignmentLimit.$invalid"
|
icon="i-lucide-arrow-right"
|
||||||
@click="updateInbox"
|
trailing-icon
|
||||||
/>
|
link
|
||||||
</div>
|
class="mb-2"
|
||||||
|
@click="navigateToAssignmentPolicyEdit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Default Policy - When no custom policy attached but feature enabled -->
|
||||||
|
<div
|
||||||
|
v-else-if="
|
||||||
|
showAdvancedAssignmentUI &&
|
||||||
|
!assignmentPolicy &&
|
||||||
|
!isLoadingPolicy
|
||||||
|
"
|
||||||
|
class="rounded-xl outline-1 outline-n-weak outline"
|
||||||
|
>
|
||||||
|
<!-- Default Policy Header -->
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-12 h-12 rounded-xl bg-n-slate-3 dark:bg-n-slate-4 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i class="i-lucide-zap text-xl text-n-slate-11" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h4 class="text-base font-medium text-n-slate-12 mb-1">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.DEFAULT_POLICY_LINKED') }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-sm text-n-slate-11">
|
||||||
|
{{
|
||||||
|
$t('INBOX_MGMT.ASSIGNMENT.DEFAULT_POLICY_DESCRIPTION')
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="mt-5 flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
v-if="!isLoadingPolicies && availablePolicies.length > 0"
|
||||||
|
v-on-click-outside="closePolicyDropdown"
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white bg-n-brand hover:bg-n-brand/90 rounded-lg transition-colors"
|
||||||
|
@click="togglePolicyDropdown"
|
||||||
|
>
|
||||||
|
<i class="i-lucide-link text-sm" />
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.LINK_EXISTING_POLICY') }}
|
||||||
|
<i
|
||||||
|
class="i-lucide-chevron-down text-sm transition-transform"
|
||||||
|
:class="{ 'rotate-180': showPolicyDropdown }"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<DropdownMenu
|
||||||
|
v-if="showPolicyDropdown"
|
||||||
|
class="top-full left-0 mt-2 min-w-72"
|
||||||
|
:menu-items="policyMenuItems"
|
||||||
|
:is-searching="isLoadingPolicies"
|
||||||
|
@action="handlePolicyMenuAction"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-n-slate-12 bg-n-slate-3 dark:bg-n-slate-4 hover:bg-n-slate-4 dark:hover:bg-n-slate-5 rounded-lg transition-colors"
|
||||||
|
@click="navigateToCreatePolicy"
|
||||||
|
>
|
||||||
|
<i class="i-lucide-plus text-sm" />
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.CREATE_NEW_POLICY') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Default Rules Info -->
|
||||||
|
<div class="px-4 py-4 border-t border-n-weak bg-n-slate-2">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<i class="i-lucide-info text-base text-n-slate-10 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-n-slate-11 mb-2">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.CURRENT_BEHAVIOR') }}
|
||||||
|
</p>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="w-1 h-1 rounded-full bg-n-slate-10 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-n-slate-11">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.DEFAULT_RULE_1') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="w-1 h-1 rounded-full bg-n-slate-10 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-n-slate-11">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.DEFAULT_RULE_2') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Default Rules Card - Feature not enabled (no advanced_assignment) -->
|
||||||
|
<div
|
||||||
|
v-else-if="!showAdvancedAssignmentUI"
|
||||||
|
class="p-4 rounded-xl outline outline-1 outline-n-weak -outline-offset-1"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-12 h-12 rounded-xl bg-n-slate-3 dark:bg-n-slate-4 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i class="i-lucide-zap text-xl text-n-slate-11" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h4 class="text-base font-medium text-n-slate-12 mb-1">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.DEFAULT_RULES_TITLE') }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-sm text-n-slate-11 mb-4">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.DEFAULT_RULES_DESCRIPTION') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="space-y-2 mb-6">
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="w-1.5 h-1.5 rounded-full bg-n-slate-11 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.DEFAULT_RULE_1') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="w-1.5 h-1.5 rounded-full bg-n-slate-11 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.DEFAULT_RULE_2') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="w-full h-px bg-n-weak my-4" />
|
||||||
|
|
||||||
|
<!-- Upgrade prompt when advanced_assignment is not enabled -->
|
||||||
|
<div v-if="!hasAdvancedAssignment">
|
||||||
|
<p class="text-sm text-n-slate-11 mb-1">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT.UPGRADE_PROMPT') }}
|
||||||
|
</p>
|
||||||
|
<NextButton
|
||||||
|
:label="$t('INBOX_MGMT.ASSIGNMENT.UPGRADE_TO_BUSINESS')"
|
||||||
|
icon="i-lucide-arrow-right"
|
||||||
|
trailing-icon
|
||||||
|
link
|
||||||
|
@click="navigateToBilling"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Old UI for non-assignment_v2 -->
|
||||||
|
<template v-else>
|
||||||
|
<label class="w-3/4 settings-item">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id="enableAutoAssignment"
|
||||||
|
v-model="enableAutoAssignment"
|
||||||
|
type="checkbox"
|
||||||
|
@change="handleToggleAutoAssignment"
|
||||||
|
/>
|
||||||
|
<label for="enableAutoAssignment">
|
||||||
|
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="pb-1 text-sm not-italic text-n-slate-11">
|
||||||
|
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT_SUB_TEXT') }}
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div v-if="enableAutoAssignment && isEnterprise" class="py-3">
|
||||||
|
<woot-input
|
||||||
|
v-model="maxAssignmentLimit"
|
||||||
|
type="number"
|
||||||
|
:class="{ error: v$.maxAssignmentLimit.$error }"
|
||||||
|
:error="maxAssignmentLimitErrors"
|
||||||
|
:label="$t('INBOX_MGMT.AUTO_ASSIGNMENT.MAX_ASSIGNMENT_LIMIT')"
|
||||||
|
@blur="v$.maxAssignmentLimit.$touch"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p class="pb-1 text-sm not-italic text-n-slate-11">
|
||||||
|
{{ $t('INBOX_MGMT.AUTO_ASSIGNMENT.MAX_ASSIGNMENT_LIMIT_SUB_TEXT') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<NextButton
|
||||||
|
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
||||||
|
:disabled="v$.maxAssignmentLimit.$invalid"
|
||||||
|
@click="updateInbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
<woot-modal
|
||||||
|
v-if="showDeleteConfirmModal"
|
||||||
|
:show="showDeleteConfirmModal"
|
||||||
|
:on-close="cancelDeletePolicy"
|
||||||
|
>
|
||||||
|
<div class="p-6">
|
||||||
|
<h3 class="text-lg font-medium text-n-slate-12 mb-4">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT_POLICY.DELETE_CONFIRM_TITLE') }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-n-slate-11 mb-6 ml-13">
|
||||||
|
{{ $t('INBOX_MGMT.ASSIGNMENT_POLICY.DELETE_CONFIRM_MESSAGE') }}
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<NextButton
|
||||||
|
color="slate"
|
||||||
|
:label="$t('INBOX_MGMT.ASSIGNMENT_POLICY.CANCEL')"
|
||||||
|
@click="cancelDeletePolicy"
|
||||||
|
/>
|
||||||
|
<NextButton
|
||||||
|
color="ruby"
|
||||||
|
:label="$t('INBOX_MGMT.ASSIGNMENT_POLICY.CONFIRM_DELETE')"
|
||||||
|
:is-loading="isDeletingPolicy"
|
||||||
|
@click="deleteAssignmentPolicy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</woot-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ class AutoAssignment::AssignmentService
|
|||||||
|
|
||||||
def perform_bulk_assignment(limit: 100)
|
def perform_bulk_assignment(limit: 100)
|
||||||
return 0 unless inbox.auto_assignment_v2_enabled?
|
return 0 unless inbox.auto_assignment_v2_enabled?
|
||||||
|
return 0 unless inbox.enable_auto_assignment?
|
||||||
|
|
||||||
assigned_count = 0
|
assigned_count = 0
|
||||||
|
|
||||||
@ -32,7 +33,9 @@ class AutoAssignment::AssignmentService
|
|||||||
def unassigned_conversations(limit)
|
def unassigned_conversations(limit)
|
||||||
scope = inbox.conversations.unassigned.open
|
scope = inbox.conversations.unassigned.open
|
||||||
|
|
||||||
scope = if assignment_config['conversation_priority'].to_s == 'longest_waiting'
|
# Apply conversation priority using assignment policy if available
|
||||||
|
policy = inbox.assignment_policy
|
||||||
|
scope = if policy&.longest_waiting?
|
||||||
scope.reorder(last_activity_at: :asc, created_at: :asc)
|
scope.reorder(last_activity_at: :asc, created_at: :asc)
|
||||||
else
|
else
|
||||||
scope.reorder(created_at: :asc)
|
scope.reorder(created_at: :asc)
|
||||||
@ -81,10 +84,6 @@ class AutoAssignment::AssignmentService
|
|||||||
def round_robin_selector
|
def round_robin_selector
|
||||||
@round_robin_selector ||= AutoAssignment::RoundRobinSelector.new(inbox: inbox)
|
@round_robin_selector ||= AutoAssignment::RoundRobinSelector.new(inbox: inbox)
|
||||||
end
|
end
|
||||||
|
|
||||||
def assignment_config
|
|
||||||
@assignment_config ||= inbox.auto_assignment_config || {}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
AutoAssignment::AssignmentService.prepend_mod_with('AutoAssignment::AssignmentService')
|
AutoAssignment::AssignmentService.prepend_mod_with('AutoAssignment::AssignmentService')
|
||||||
|
|||||||
@ -8,8 +8,6 @@ class AutoAssignment::RateLimiter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def track_assignment(conversation)
|
def track_assignment(conversation)
|
||||||
return unless enabled?
|
|
||||||
|
|
||||||
assignment_key = build_assignment_key(conversation.id)
|
assignment_key = build_assignment_key(conversation.id)
|
||||||
Redis::Alfred.set(assignment_key, conversation.id.to_s, ex: window)
|
Redis::Alfred.set(assignment_key, conversation.id.to_s, ex: window)
|
||||||
end
|
end
|
||||||
@ -24,11 +22,11 @@ class AutoAssignment::RateLimiter
|
|||||||
private
|
private
|
||||||
|
|
||||||
def enabled?
|
def enabled?
|
||||||
limit.present? && limit.positive?
|
config.present? && limit.positive?
|
||||||
end
|
end
|
||||||
|
|
||||||
def limit
|
def limit
|
||||||
config&.fair_distribution_limit&.to_i || Math
|
config&.fair_distribution_limit.present? ? config.fair_distribution_limit.to_i : Float::INFINITY
|
||||||
end
|
end
|
||||||
|
|
||||||
def window
|
def window
|
||||||
|
|||||||
@ -241,3 +241,7 @@
|
|||||||
display_name: Required Conversation Attributes
|
display_name: Required Conversation Attributes
|
||||||
enabled: false
|
enabled: false
|
||||||
premium: true
|
premium: true
|
||||||
|
- name: advanced_assignment
|
||||||
|
display_name: Advanced Assignment
|
||||||
|
enabled: false
|
||||||
|
premium: true
|
||||||
|
|||||||
@ -3,6 +3,12 @@ module Enterprise::Account
|
|||||||
# this is a temporary method since current administrate doesn't support virtual attributes
|
# this is a temporary method since current administrate doesn't support virtual attributes
|
||||||
def manually_managed_features; end
|
def manually_managed_features; end
|
||||||
|
|
||||||
|
# Auto-sync advanced_assignment with assignment_v2 when features are bulk-updated via admin UI
|
||||||
|
def selected_feature_flags=(features)
|
||||||
|
super
|
||||||
|
sync_assignment_features
|
||||||
|
end
|
||||||
|
|
||||||
def mark_for_deletion(reason = 'manual_deletion')
|
def mark_for_deletion(reason = 'manual_deletion')
|
||||||
reason = reason.to_s == 'manual_deletion' ? 'manual_deletion' : 'inactivity'
|
reason = reason.to_s == 'manual_deletion' ? 'manual_deletion' : 'inactivity'
|
||||||
|
|
||||||
@ -31,4 +37,21 @@ module Enterprise::Account
|
|||||||
def saml_enabled?
|
def saml_enabled?
|
||||||
saml_settings&.saml_enabled? || false
|
saml_settings&.saml_enabled? || false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sync_assignment_features
|
||||||
|
if feature_enabled?('assignment_v2')
|
||||||
|
# Enable advanced_assignment for Business/Enterprise plans
|
||||||
|
send('feature_advanced_assignment=', true) if business_or_enterprise_plan?
|
||||||
|
else
|
||||||
|
# Disable advanced_assignment when assignment_v2 is disabled
|
||||||
|
send('feature_advanced_assignment=', false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def business_or_enterprise_plan?
|
||||||
|
plan_name = custom_attributes['plan_name']
|
||||||
|
%w[Business Enterprise].include?(plan_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -19,7 +19,8 @@ module Enterprise::AutoAssignment::AssignmentService
|
|||||||
agents = filter_agents_by_capacity(agents) if capacity_filtering_enabled?
|
agents = filter_agents_by_capacity(agents) if capacity_filtering_enabled?
|
||||||
return nil if agents.empty?
|
return nil if agents.empty?
|
||||||
|
|
||||||
selector = policy&.balanced? ? balanced_selector : round_robin_selector
|
# Use balanced selector only if advanced_assignment feature is enabled
|
||||||
|
selector = policy&.balanced? && account.feature_enabled?('advanced_assignment') ? balanced_selector : round_robin_selector
|
||||||
selector.select_agent(agents)
|
selector.select_agent(agents)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ module Enterprise::AutoAssignment::AssignmentService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def capacity_filtering_enabled?
|
def capacity_filtering_enabled?
|
||||||
account.feature_enabled?('assignment_v2') &&
|
account.feature_enabled?('advanced_assignment') &&
|
||||||
account.account_users.joins(:agent_capacity_policy).exists?
|
account.account_users.joins(:agent_capacity_policy).exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class Enterprise::Billing::HandleStripeEventService
|
|||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# Additional features available starting with the Business plan
|
# Additional features available starting with the Business plan
|
||||||
BUSINESS_PLAN_FEATURES = %w[sla custom_roles csat_review_notes conversation_required_attributes].freeze
|
BUSINESS_PLAN_FEATURES = %w[sla custom_roles csat_review_notes conversation_required_attributes advanced_assignment].freeze
|
||||||
|
|
||||||
# Additional features available only in the Enterprise plan
|
# Additional features available only in the Enterprise plan
|
||||||
ENTERPRISE_PLAN_FEATURES = %w[audit_logs disable_branding saml].freeze
|
ENTERPRISE_PLAN_FEATURES = %w[audit_logs disable_branding saml].freeze
|
||||||
|
|||||||
@ -16,8 +16,9 @@ RSpec.describe Enterprise::AutoAssignment::AssignmentService, type: :service do
|
|||||||
# Link inbox to assignment policy
|
# Link inbox to assignment policy
|
||||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
||||||
|
|
||||||
allow(account).to receive(:feature_enabled?).and_return(false)
|
# Enable assignment_v2 (base) and advanced_assignment (premium) features
|
||||||
allow(account).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
|
account.enable_features('assignment_v2')
|
||||||
|
account.save!
|
||||||
|
|
||||||
# Set agents as online
|
# Set agents as online
|
||||||
OnlineStatusTracker.update_presence(account.id, 'User', agent1.id)
|
OnlineStatusTracker.update_presence(account.id, 'User', agent1.id)
|
||||||
|
|||||||
@ -52,8 +52,9 @@ RSpec.describe Enterprise::AutoAssignment::CapacityService, type: :service do
|
|||||||
agent_at_capacity.id.to_s => 'online'
|
agent_at_capacity.id.to_s => 'online'
|
||||||
})
|
})
|
||||||
|
|
||||||
# Enable assignment_v2 feature
|
# Enable assignment_v2 (base) and advanced_assignment (premium) features
|
||||||
allow(account).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
|
account.enable_features('assignment_v2', 'advanced_assignment')
|
||||||
|
account.save!
|
||||||
|
|
||||||
# Create existing assignments for agent_at_capacity (at limit)
|
# Create existing assignments for agent_at_capacity (at limit)
|
||||||
3.times do
|
3.times do
|
||||||
|
|||||||
@ -14,13 +14,13 @@ RSpec.describe AutoAssignment::PeriodicAssignmentJob, type: :job do
|
|||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
context 'when account has assignment_v2 feature enabled' do
|
context 'when account has assignment_v2 feature enabled' do
|
||||||
before do
|
before do
|
||||||
allow(account).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
|
account.enable_features('assignment_v2')
|
||||||
|
account.save!
|
||||||
allow(Account).to receive(:find_in_batches).and_yield([account])
|
allow(Account).to receive(:find_in_batches).and_yield([account])
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when inbox has auto_assignment_v2 enabled' do
|
context 'when inbox has assignment policy or auto assignment enabled' do
|
||||||
before do
|
before do
|
||||||
allow(inbox).to receive(:auto_assignment_v2_enabled?).and_return(true)
|
|
||||||
inbox_relation = instance_double(ActiveRecord::Relation)
|
inbox_relation = instance_double(ActiveRecord::Relation)
|
||||||
allow(account).to receive(:inboxes).and_return(inbox_relation)
|
allow(account).to receive(:inboxes).and_return(inbox_relation)
|
||||||
allow(inbox_relation).to receive(:joins).with(:assignment_policy).and_return(inbox_relation)
|
allow(inbox_relation).to receive(:joins).with(:assignment_policy).and_return(inbox_relation)
|
||||||
@ -41,8 +41,8 @@ RSpec.describe AutoAssignment::PeriodicAssignmentJob, type: :job do
|
|||||||
policy2 = create(:assignment_policy, account: account2)
|
policy2 = create(:assignment_policy, account: account2)
|
||||||
create(:inbox_assignment_policy, inbox: inbox2, assignment_policy: policy2)
|
create(:inbox_assignment_policy, inbox: inbox2, assignment_policy: policy2)
|
||||||
|
|
||||||
allow(account2).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
|
account2.enable_features('assignment_v2')
|
||||||
allow(inbox2).to receive(:auto_assignment_v2_enabled?).and_return(true)
|
account2.save!
|
||||||
|
|
||||||
inbox_relation2 = instance_double(ActiveRecord::Relation)
|
inbox_relation2 = instance_double(ActiveRecord::Relation)
|
||||||
allow(account2).to receive(:inboxes).and_return(inbox_relation2)
|
allow(account2).to receive(:inboxes).and_return(inbox_relation2)
|
||||||
@ -58,9 +58,10 @@ RSpec.describe AutoAssignment::PeriodicAssignmentJob, type: :job do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when inbox does not have auto_assignment_v2 enabled' do
|
context 'when inbox does not have assignment policy or auto assignment enabled' do
|
||||||
before do
|
before do
|
||||||
allow(inbox).to receive(:auto_assignment_v2_enabled?).and_return(false)
|
inbox.update!(enable_auto_assignment: false)
|
||||||
|
InboxAssignmentPolicy.where(inbox: inbox).destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not queue assignment job' do
|
it 'does not queue assignment job' do
|
||||||
@ -73,7 +74,6 @@ RSpec.describe AutoAssignment::PeriodicAssignmentJob, type: :job do
|
|||||||
|
|
||||||
context 'when account does not have assignment_v2 feature enabled' do
|
context 'when account does not have assignment_v2 feature enabled' do
|
||||||
before do
|
before do
|
||||||
allow(account).to receive(:feature_enabled?).with('assignment_v2').and_return(false)
|
|
||||||
allow(Account).to receive(:find_in_batches).and_yield([account])
|
allow(Account).to receive(:find_in_batches).and_yield([account])
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -90,11 +90,11 @@ RSpec.describe AutoAssignment::PeriodicAssignmentJob, type: :job do
|
|||||||
# Create multiple accounts
|
# Create multiple accounts
|
||||||
5.times do |_i|
|
5.times do |_i|
|
||||||
acc = create(:account)
|
acc = create(:account)
|
||||||
|
acc.enable_features('assignment_v2')
|
||||||
|
acc.save!
|
||||||
inb = create(:inbox, account: acc, enable_auto_assignment: true)
|
inb = create(:inbox, account: acc, enable_auto_assignment: true)
|
||||||
policy = create(:assignment_policy, account: acc)
|
policy = create(:assignment_policy, account: acc)
|
||||||
create(:inbox_assignment_policy, inbox: inb, assignment_policy: policy)
|
create(:inbox_assignment_policy, inbox: inb, assignment_policy: policy)
|
||||||
allow(acc).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
|
|
||||||
allow(inb).to receive(:auto_assignment_v2_enabled?).and_return(true)
|
|
||||||
|
|
||||||
inbox_relation = instance_double(ActiveRecord::Relation)
|
inbox_relation = instance_double(ActiveRecord::Relation)
|
||||||
allow(acc).to receive(:inboxes).and_return(inbox_relation)
|
allow(acc).to receive(:inboxes).and_return(inbox_relation)
|
||||||
|
|||||||
@ -10,8 +10,9 @@ RSpec.describe AutoAssignment::AssignmentService do
|
|||||||
let(:conversation) { create(:conversation, inbox: inbox, assignee: nil) }
|
let(:conversation) { create(:conversation, inbox: inbox, assignee: nil) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
# Enable assignment_v2 feature for the account
|
# Enable assignment_v2 feature for the account (basic assignment features)
|
||||||
allow(account).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
|
account.enable_features('assignment_v2')
|
||||||
|
account.save!
|
||||||
# Link inbox to assignment policy
|
# Link inbox to assignment policy
|
||||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
||||||
create(:inbox_member, inbox: inbox, user: agent)
|
create(:inbox_member, inbox: inbox, user: agent)
|
||||||
|
|||||||
@ -59,8 +59,9 @@ RSpec.describe AutoAssignment::RateLimiter do
|
|||||||
allow(inbox).to receive(:assignment_policy).and_return(nil)
|
allow(inbox).to receive(:assignment_policy).and_return(nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not track the assignment' do
|
it 'still tracks the assignment with default window' do
|
||||||
expect(Redis::Alfred).not_to receive(:set)
|
expected_key = format(Redis::RedisKeys::ASSIGNMENT_KEY, inbox_id: inbox.id, agent_id: agent.id, conversation_id: conversation.id)
|
||||||
|
expect(Redis::Alfred).to receive(:set).with(expected_key, conversation.id.to_s, ex: 24.hours.to_i)
|
||||||
rate_limiter.track_assignment(conversation)
|
rate_limiter.track_assignment(conversation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user