diff --git a/app/controllers/api/v1/accounts/assignment_policies_controller.rb b/app/controllers/api/v1/accounts/assignment_policies_controller.rb
index 1807d6afb..0150cb677 100644
--- a/app/controllers/api/v1/accounts/assignment_policies_controller.rb
+++ b/app/controllers/api/v1/accounts/assignment_policies_controller.rb
@@ -30,7 +30,8 @@ class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseC
def assignment_policy_params
params.require(:assignment_policy).permit(
:name, :description, :assignment_order, :conversation_priority,
- :fair_distribution_limit, :fair_distribution_window, :enabled
+ :fair_distribution_limit, :fair_distribution_window, :enabled,
+ :exclude_older_than_hours
)
end
end
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index 5e2543698..0a87b4cbe 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -798,10 +798,14 @@
},
"FAIR_DISTRIBUTION": {
"LABEL": "Fair distribution policy",
- "DESCRIPTION": "Set the maximum number of conversations that can be assigned per agent within a time window to avoid overloading any one agent. This required field defaults to 100 conversations per hour.",
+ "DESCRIPTION": "Cap conversations per agent within a time window to avoid overload. Defaults to 100 per hour.",
"INPUT_MAX": "Assign max",
"DURATION": "Conversations per agent in every"
},
+ "EXCLUDE_OLDER_THAN": {
+ "LABEL": "Skip stale conversations",
+ "DESCRIPTION": "Skip unassigned conversations older than this. Defaults to 7 days; clear to disable."
+ },
"INBOXES": {
"LABEL": "Added inboxes",
"DESCRIPTION": "Add inboxes for which this policy will be applicable.",
diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/constants.js b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/constants.js
index 350faa60c..028688c75 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/constants.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/constants.js
@@ -10,6 +10,9 @@ export const LONGEST_WAITING = 'longest_waiting';
export const DEFAULT_FAIR_DISTRIBUTION_LIMIT = 100;
export const DEFAULT_FAIR_DISTRIBUTION_WINDOW = 3600;
+// Default age threshold for excluding stale unassigned conversations (7 days)
+export const DEFAULT_EXCLUDE_OLDER_THAN_HOURS = 168;
+
// Options groupings
export const OPTIONS = {
ORDER: [ROUND_ROBIN, BALANCED],
diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentEditPage.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentEditPage.vue
index dfae60350..0550e7364 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentEditPage.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentEditPage.vue
@@ -106,6 +106,7 @@ const formData = computed(() => ({
selectedPolicy.value?.conversationPriority || EARLIEST_CREATED,
fairDistributionLimit: selectedPolicy.value?.fairDistributionLimit || 100,
fairDistributionWindow: selectedPolicy.value?.fairDistributionWindow || 3600,
+ excludeOlderThanHours: selectedPolicy.value?.excludeOlderThanHours ?? null,
}));
const handleDeleteInbox = async inboxId => {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue
index cbf22b4d2..36c131ee2 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue
@@ -8,6 +8,8 @@ import RadioCard from 'dashboard/components-next/radioCard/RadioCard.vue';
import FairDistribution from 'dashboard/components-next/AssignmentPolicy/components/FairDistribution.vue';
import DataTable from 'dashboard/components-next/AssignmentPolicy/components/DataTable.vue';
import AddDataDropdown from 'dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue';
+import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
+import { DURATION_UNITS } from 'dashboard/components-next/input/constants';
import WithLabel from 'v3/components/Form/WithLabel.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import {
@@ -16,6 +18,7 @@ import {
EARLIEST_CREATED,
DEFAULT_FAIR_DISTRIBUTION_LIMIT,
DEFAULT_FAIR_DISTRIBUTION_WINDOW,
+ DEFAULT_EXCLUDE_OLDER_THAN_HOURS,
} from 'dashboard/routes/dashboard/settings/assignmentPolicy/constants';
const props = defineProps({
@@ -28,6 +31,7 @@ const props = defineProps({
conversationPriority: EARLIEST_CREATED,
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
fairDistributionWindow: DEFAULT_FAIR_DISTRIBUTION_WINDOW,
+ excludeOlderThanHours: DEFAULT_EXCLUDE_OLDER_THAN_HOURS,
}),
},
mode: {
@@ -56,7 +60,6 @@ const props = defineProps({
default: false,
},
});
-
const emit = defineEmits([
'submit',
'addInbox',
@@ -64,6 +67,9 @@ const emit = defineEmits([
'navigateToInbox',
'validationChange',
]);
+// Duration limits for the stale-conversation threshold: 1 hour to 999 days (in minutes)
+const MIN_EXCLUSION_MINUTES = 60;
+const MAX_EXCLUSION_MINUTES = 1438560;
const { t } = useI18n();
const route = useRoute();
@@ -83,12 +89,28 @@ const state = reactive({
conversationPriority: EARLIEST_CREATED,
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
fairDistributionWindow: DEFAULT_FAIR_DISTRIBUTION_WINDOW,
+ excludeOlderThanHours: DEFAULT_EXCLUDE_OLDER_THAN_HOURS,
});
const validationState = ref({
isValid: false,
});
+const exclusionUnit = ref(DURATION_UNITS.DAYS);
+
+// DurationInput works in minutes; the policy stores hours, so bridge the two
+const excludeOlderThanMinutes = computed({
+ get() {
+ return state.excludeOlderThanHours == null
+ ? null
+ : state.excludeOlderThanHours * 60;
+ },
+ set(minutes) {
+ state.excludeOlderThanHours =
+ minutes == null ? null : Math.round(minutes / 60);
+ },
+});
+
const createOption = (
type,
key,
@@ -170,6 +192,7 @@ const resetForm = () => {
conversationPriority: EARLIEST_CREATED,
fairDistributionLimit: DEFAULT_FAIR_DISTRIBUTION_LIMIT,
fairDistributionWindow: DEFAULT_FAIR_DISTRIBUTION_WINDOW,
+ excludeOlderThanHours: DEFAULT_EXCLUDE_OLDER_THAN_HOURS,
});
};
@@ -177,10 +200,17 @@ const handleSubmit = () => {
emit('submit', { ...state });
};
+// Pick the display unit from the stored value so non-day thresholds (e.g. 25h) don't get floored
+const detectExclusionUnit = hours => {
+ exclusionUnit.value =
+ hours && hours % 24 !== 0 ? DURATION_UNITS.HOURS : DURATION_UNITS.DAYS;
+};
+
watch(
() => props.initialData,
newData => {
Object.assign(state, newData);
+ detectExclusionUnit(newData.excludeOlderThanHours);
},
{ immediate: true, deep: true }
);
@@ -247,6 +277,27 @@ defineExpose({
v-model:window-unit="state.windowUnit"
/>
+
+
+
+
+
+ {{ t(`${BASE_KEY}.FORM.EXCLUDE_OLDER_THAN.DESCRIPTION`) }}
+
+
+
+
+
+