diff --git a/app/livechat/client/views/app/tabbar/visitorForward.js b/app/livechat/client/views/app/tabbar/visitorForward.js
index 313b87e867da..84860188cf99 100644
--- a/app/livechat/client/views/app/tabbar/visitorForward.js
+++ b/app/livechat/client/views/app/tabbar/visitorForward.js
@@ -13,9 +13,6 @@ Template.visitorForward.helpers({
visitor() {
return Template.instance().visitor.get();
},
- hasDepartments() {
- return Template.instance().departments.get().filter((department) => department.enabled === true).length > 0;
- },
agentName() {
return this.name || this.username;
},
@@ -56,7 +53,8 @@ Template.visitorForward.helpers({
return Template.instance().onSelectDepartments;
},
departmentConditions() {
- return { enabled: true, numAgents: { $gt: 0 } };
+ const departmentForwardRestrictions = Template.instance().departmentForwardRestrictions.get();
+ return { enabled: true, numAgents: { $gt: 0 }, ...departmentForwardRestrictions };
},
});
@@ -66,6 +64,7 @@ Template.visitorForward.onCreated(async function() {
this.departments = new ReactiveVar([]);
this.selectedAgents = new ReactiveVar([]);
this.selectedDepartments = new ReactiveVar([]);
+ this.departmentForwardRestrictions = new ReactiveVar({});
this.onSelectDepartments = ({ item: department }) => {
department.text = department.name;
@@ -92,6 +91,12 @@ Template.visitorForward.onCreated(async function() {
this.autorun(() => {
this.room.set(ChatRoom.findOne({ _id: Template.currentData().roomId }));
+ const { departmentId } = this.room.get();
+ if (departmentId) {
+ Meteor.call('livechat:getDepartmentForwardRestrictions', departmentId, (err, result) => {
+ this.departmentForwardRestrictions.set(result);
+ });
+ }
});
const { departments } = await APIClient.v1.get('livechat/department');
diff --git a/app/livechat/imports/server/rest/departments.js b/app/livechat/imports/server/rest/departments.js
index 315fbcb30772..8a255185f86f 100644
--- a/app/livechat/imports/server/rest/departments.js
+++ b/app/livechat/imports/server/rest/departments.js
@@ -4,7 +4,7 @@ import { API } from '../../../../api';
import { hasPermission } from '../../../../authorization';
import { LivechatDepartment, LivechatDepartmentAgents } from '../../../../models';
import { Livechat } from '../../../server/lib/Livechat';
-import { findDepartments, findDepartmentById, findDepartmentsToAutocomplete } from '../../../server/api/lib/departments';
+import { findDepartments, findDepartmentById, findDepartmentsToAutocomplete, findDepartmentsBetweenIds } from '../../../server/api/lib/departments';
API.v1.addRoute('livechat/department', { authRequired: true }, {
get() {
@@ -147,3 +147,22 @@ API.v1.addRoute('livechat/department.autocomplete', { authRequired: true }, {
})));
},
});
+
+API.v1.addRoute('livechat/department.listByIds', { authRequired: true }, {
+ get() {
+ const { ids } = this.queryParams;
+ const { fields } = this.parseJsonQuery();
+ if (!ids) {
+ return API.v1.failure('The \'ids\' param is required');
+ }
+ if (!Array.isArray(ids)) {
+ return API.v1.failure('The \'ids\' param must be an array');
+ }
+
+ return API.v1.success(Promise.await(findDepartmentsBetweenIds({
+ uid: this.userId,
+ ids,
+ fields,
+ })));
+ },
+});
diff --git a/app/livechat/server/api/lib/departments.js b/app/livechat/server/api/lib/departments.js
index b9b73947937a..ef008ae6fd15 100644
--- a/app/livechat/server/api/lib/departments.js
+++ b/app/livechat/server/api/lib/departments.js
@@ -67,3 +67,12 @@ export async function findDepartmentsToAutocomplete({ uid, selector }) {
items,
};
}
+
+export async function findDepartmentsBetweenIds({ uid, ids, fields }) {
+ if (!await hasPermissionAsync(uid, 'view-livechat-departments') && !await hasPermissionAsync(uid, 'view-l-room')) {
+ throw new Error('error-not-authorized');
+ }
+
+ const departments = await LivechatDepartment.findInIds(ids, fields).toArray();
+ return { departments };
+}
diff --git a/app/livechat/server/index.js b/app/livechat/server/index.js
index fe61cd6f5825..f6deeb0f8933 100644
--- a/app/livechat/server/index.js
+++ b/app/livechat/server/index.js
@@ -69,6 +69,7 @@ import './methods/saveOfficeHours';
import './methods/sendTranscript';
import './methods/getFirstRoomMessage';
import './methods/getTagsList';
+import './methods/getDepartmentForwardRestrictions';
import './lib/Analytics';
import './lib/QueueManager';
import './lib/OfficeClock';
diff --git a/app/livechat/server/lib/Helper.js b/app/livechat/server/lib/Helper.js
index a29b47eade1e..b2b47dad6b9c 100644
--- a/app/livechat/server/lib/Helper.js
+++ b/app/livechat/server/lib/Helper.js
@@ -224,7 +224,7 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => {
if (!room || !room.open) {
return false;
}
-
+ callbacks.run('livechat.beforeForwardRoomToDepartment', { room, transferData });
const { _id: rid, servedBy: oldServedBy, departmentId: oldDepartmentId } = room;
const inquiry = LivechatInquiry.findOneByRoomId(rid);
diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js
index b808af5db862..323dd80b8a15 100644
--- a/app/livechat/server/lib/Livechat.js
+++ b/app/livechat/server/lib/Livechat.js
@@ -888,7 +888,13 @@ export const Livechat = {
throw new Meteor.Error('department-not-found', 'Department not found', { method: 'livechat:removeDepartment' });
}
- return LivechatDepartment.removeById(_id);
+ const ret = LivechatDepartment.removeById(_id);
+ if (ret) {
+ Meteor.defer(() => {
+ callbacks.run('livechat.afterRemoveDepartment', department);
+ });
+ }
+ return ret;
},
showConnecting() {
diff --git a/app/livechat/server/methods/getDepartmentForwardRestrictions.js b/app/livechat/server/methods/getDepartmentForwardRestrictions.js
new file mode 100644
index 000000000000..e08adc9792ba
--- /dev/null
+++ b/app/livechat/server/methods/getDepartmentForwardRestrictions.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+
+import { callbacks } from '../../../callbacks';
+
+Meteor.methods({
+ 'livechat:getDepartmentForwardRestrictions'(departmentId) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:getDepartmentForwardRestrictions' });
+ }
+
+ return callbacks.run('livechat.onLoadForwardDepartmentRestrictions', departmentId);
+ },
+});
diff --git a/app/utils/client/index.js b/app/utils/client/index.js
index 6b2060f5d0e2..8f536315da1d 100644
--- a/app/utils/client/index.js
+++ b/app/utils/client/index.js
@@ -17,7 +17,7 @@ export { getURL } from '../lib/getURL';
export { getValidRoomName } from '../lib/getValidRoomName';
export { placeholders } from '../lib/placeholders';
export { templateVarHandler } from '../lib/templateVarHandler';
-export { APIClient } from './lib/RestApiClient';
+export { APIClient, mountArrayQueryParameters } from './lib/RestApiClient';
export { canDeleteMessage } from './lib/canDeleteMessage';
export { mime } from '../lib/mimeTypes';
export { secondsToHHMMSS } from '../lib/timeConverter';
diff --git a/app/utils/client/lib/RestApiClient.js b/app/utils/client/lib/RestApiClient.js
index 50eba6f56ae1..511c9eb988ee 100644
--- a/app/utils/client/lib/RestApiClient.js
+++ b/app/utils/client/lib/RestApiClient.js
@@ -4,6 +4,11 @@ import { Accounts } from 'meteor/accounts-base';
import { baseURI } from './baseuri';
import { process2faReturn } from '../../../2fa/client/callWithTwoFactorRequired';
+export const mountArrayQueryParameters = (label, array) => array.reduce((acc, item) => {
+ acc += `${ label }[]=${ item }&`;
+ return acc;
+}, '');
+
export const APIClient = {
delete(endpoint, params) {
return APIClient._jqueryCall('DELETE', endpoint, params);
diff --git a/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.html b/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.html
index c0cd15e90ca8..264078cc53d5 100644
--- a/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.html
+++ b/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.html
@@ -2,25 +2,55 @@
-
+
+
\ No newline at end of file
diff --git a/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.js b/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.js
index 71ffdfde7384..a1851d16cea3 100644
--- a/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.js
+++ b/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.js
@@ -1,23 +1,58 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
-import { APIClient } from '../../../../../../../app/utils/client';
+import { APIClient, mountArrayQueryParameters } from '../../../../../../../app/utils/client';
import './livechatDepartmentCustomFieldsForm.html';
Template.livechatDepartmentCustomFieldsForm.helpers({
department() {
return Template.instance().department.get();
},
+ departmentModifier() {
+ return (filter, text = '') => {
+ const f = filter.get();
+ return `${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `
${ part }`) }`;
+ };
+ },
+ onClickTagDepartment() {
+ return Template.instance().onClickTagDepartment;
+ },
+ selectedDepartments() {
+ return Template.instance().selectedDepartments.get();
+ },
+ selectedDepartmentsIds() {
+ return Template.instance().selectedDepartments.get().map((dept) => dept._id);
+ },
+ onSelectDepartments() {
+ return Template.instance().onSelectDepartments;
+ },
+ exceptionsDepartments() {
+ const department = Template.instance().department.get();
+ return [department && department._id, ...Template.instance().selectedDepartments.get().map((dept) => dept._id)];
+ },
});
Template.livechatDepartmentCustomFieldsForm.onCreated(function() {
+ this.selectedDepartments = new ReactiveVar([]);
const { id: _id, department: contextDepartment } = this.data;
this.department = new ReactiveVar(contextDepartment);
+ this.onSelectDepartments = ({ item: department }) => {
+ department.text = department.name;
+ this.selectedDepartments.set(this.selectedDepartments.get().concat(department));
+ };
+
+ this.onClickTagDepartment = (department) => {
+ this.selectedDepartments.set(this.selectedDepartments.get().filter((dept) => dept._id !== department._id));
+ };
if (!contextDepartment && _id) {
this.autorun(async () => {
const { department } = await APIClient.v1.get(`livechat/department/${ _id }`);
+ if (department.departmentsAllowedToForward) {
+ const { departments } = await APIClient.v1.get(`livechat/department.listByIds?${ mountArrayQueryParameters('ids', department.departmentsAllowedToForward) }&fields=${ JSON.stringify({ fields: { name: 1 } }) }`);
+ this.selectedDepartments.set(departments.map((dept) => ({ _id: dept._id, text: dept.name })));
+ }
this.department.set(department);
});
}
diff --git a/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js b/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js
new file mode 100644
index 000000000000..4a101a0787fb
--- /dev/null
+++ b/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js
@@ -0,0 +1,10 @@
+import { callbacks } from '../../../../../app/callbacks';
+import { LivechatDepartment } from '../../../../../app/models/server';
+
+callbacks.add('livechat.afterRemoveDepartment', (department) => {
+ if (!department) {
+ return department;
+ }
+ LivechatDepartment.removeDepartmentFromForwardListById(department._id);
+ return department;
+}, callbacks.priority.HIGH, 'livechat-after-remove-department');
diff --git a/ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.js b/ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.js
new file mode 100644
index 000000000000..b0bf3f95d1f5
--- /dev/null
+++ b/ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.js
@@ -0,0 +1,26 @@
+import { Meteor } from 'meteor/meteor';
+
+import { callbacks } from '../../../../../app/callbacks';
+import { LivechatDepartment } from '../../../../../app/models/server';
+
+callbacks.add('livechat.beforeForwardRoomToDepartment', (options) => {
+ const { room, transferData } = options;
+ if (!room || !transferData) {
+ return options;
+ }
+ const { departmentId } = room;
+ if (!departmentId) {
+ return options;
+ }
+ const { department: departmentToTransfer } = transferData;
+ const currentDepartment = LivechatDepartment.findOneById(departmentId);
+ if (!currentDepartment) {
+ return options;
+ }
+ const { departmentsAllowedToForward } = currentDepartment;
+ const isAllowedToTransfer = !departmentsAllowedToForward || (Array.isArray(departmentsAllowedToForward) && departmentsAllowedToForward.includes(departmentToTransfer._id));
+ if (isAllowedToTransfer) {
+ return options;
+ }
+ throw new Meteor.Error('error-forwarding-department-target-not-allowed', 'The forwarding to the target department is not allowed.');
+}, callbacks.priority.HIGH, 'livechat-before-forward-room-to-department');
diff --git a/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js b/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js
new file mode 100644
index 000000000000..538ce151b30c
--- /dev/null
+++ b/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js
@@ -0,0 +1,17 @@
+import { callbacks } from '../../../../../app/callbacks';
+import { LivechatDepartment } from '../../../../../app/models/server';
+
+callbacks.add('livechat.onLoadForwardDepartmentRestrictions', (departmentId) => {
+ if (!departmentId) {
+ return {};
+ }
+ const department = LivechatDepartment.findOneById(departmentId, { fields: { departmentsAllowedToForward: 1 } });
+ if (!department) {
+ return {};
+ }
+ const { departmentsAllowedToForward } = department;
+ if (!departmentsAllowedToForward) {
+ return {};
+ }
+ return { _id: { $in: departmentsAllowedToForward } };
+}, callbacks.priority.MEDIUM, 'livechat-on-load-forward-department-restrictions');
diff --git a/ee/app/livechat-enterprise/server/index.js b/ee/app/livechat-enterprise/server/index.js
index e15a5bef4edc..297e6d1962b5 100644
--- a/ee/app/livechat-enterprise/server/index.js
+++ b/ee/app/livechat-enterprise/server/index.js
@@ -4,6 +4,9 @@ import './hooks/addDepartmentAncestors';
import './hooks/afterForwardChatToDepartment';
import './hooks/beforeListTags';
import './hooks/setPredictedVisitorAbandonmentTime';
+import './hooks/beforeForwardRoomToDepartment';
+import './hooks/afterRemoveDepartment';
+import './hooks/onLoadForwardDepartmentRestrictions';
import './methods/addMonitor';
import './methods/getUnitsFromUserRoles';
import './methods/removeMonitor';
diff --git a/ee/app/models/server/models/LivechatDepartment.js b/ee/app/models/server/models/LivechatDepartment.js
index d54c0db5d65f..2e4785282d91 100644
--- a/ee/app/models/server/models/LivechatDepartment.js
+++ b/ee/app/models/server/models/LivechatDepartment.js
@@ -32,6 +32,9 @@ overwriteClassOnLicense('livechat-enterprise', LivechatDepartment, {
if (args.length > 2 && !args[1].type) {
args[1].type = 'd';
}
+ if (args[1] && args[1].departmentsAllowedToForward) {
+ args[1].departmentsAllowedToForward = args[1].departmentsAllowedToForward.split(',');
+ }
return originalFn.apply(this, args);
},
@@ -49,4 +52,8 @@ overwriteClassOnLicense('livechat-enterprise', LivechatDepartment, {
},
});
+LivechatDepartment.prototype.removeDepartmentFromForwardListById = function(_id) {
+ return this.update({ departmentsAllowedToForward: _id }, { $pull: { departmentsAllowedToForward: _id } }, { multi: true });
+};
+
export default LivechatDepartment;
diff --git a/ee/i18n/en.i18n.json b/ee/i18n/en.i18n.json
index f9d3d4bd7b09..4d15e29fa152 100644
--- a/ee/i18n/en.i18n.json
+++ b/ee/i18n/en.i18n.json
@@ -13,6 +13,7 @@
"Enter_a_custom_message": "Enter a custom message",
"Enterprise_License": "Enterprise License",
"Enterprise_License_Description": "If your workspace is registered and license is provided by Rocket.Chat Cloud you don't need to manually update the license here.",
+ "error-forwarding-department-target-not-allowed": "The forwarding to the target department is not allowed.",
"Failed_to_add_monitor": "Failed to add monitor",
"Invalid Canned Response": "Invalid Canned Response",
"Invalid_Department": "Invalid Department",
@@ -25,6 +26,8 @@
"LDAP_Roles_To_Rocket_Chat_Roles_Description": "Role mapping in object format where the object key must be the LDAP role and the object value must be an array of RC roles. Example: { 'ldapRole': ['rcRole', 'anotherRCRole'] }",
"LDAP_Validate_Roles_For_Each_Login": "Validate mapping for each login",
"LDAP_Validate_Roles_For_Each_Login_Description": "If the validation should occurs for each login (Be careful with this setting because it will overwrite the user roles in each login, otherwise this will be validated only at the moment of user creation).",
+ "List_of_departments_for_forward": "List of departments allowed for forwarding (Optional)",
+ "List_of_departments_for_forward_description": "Allow to set a restricted list of departments that can receive chats from this department",
"Livechat_abandoned_rooms_closed_custom_message": "Custom message when room is automatically closed by visitor inactivity",
"Livechat_Monitors": "Monitors",
"Livechat_monitors": "Livechat monitors",
diff --git a/ee/i18n/pt-BR.i18n.json b/ee/i18n/pt-BR.i18n.json
index 58d93186aaf0..7ec29f0cbd8b 100644
--- a/ee/i18n/pt-BR.i18n.json
+++ b/ee/i18n/pt-BR.i18n.json
@@ -9,6 +9,7 @@
"Enter_a_custom_message": "Digite uma mensagem customizada",
"Enterprise_License": "Licença Enterprise",
"Enterprise_License_Description": "Se você registrou seu workspace e a licença foi fornecida pelo Rocket.Chat Cloud você não precisa atualizar a licença manualmente aqui.",
+ "error-forwarding-department-target-not-allowed": "O encaminhamento para o departamento selecionado não é permitido.",
"Invalid_Department": "Departamento inválido",
"LDAP_Default_Role_To_User": "Papel padrão para o usuário",
"LDAP_Default_Role_To_User_Description": "Papel padrão par ser aplicado ao usuário, caso ele tenha algum papel do LDAP que não esteja mapeado.",
@@ -19,6 +20,8 @@
"LDAP_Roles_To_Rocket_Chat_Roles_Description": "Mapeamento dos papéis que deve ser em formato de objeto onde a chave do objeto precisa ser o nome do papel LDAP e o valor deve ser um array de papéis Rocket.Chat. Exemplo: { 'ldapRole': ['rcRole', 'anotherRCRole'] }",
"LDAP_Validate_Roles_For_Each_Login": "Validar o mapeamento em cada login",
"LDAP_Validate_Roles_For_Each_Login_Description": " Se a validação deve ser feita a cada login(Tenha cuidado com essa configuração, pois ela vai sobrescrever os papéis de usuário a cada login, caso esteja desabilitado, a validação será feita apenas no momento da criação do usuário).",
+ "List_of_departments_for_forward": "Lista de departamentos permitidos para o encaminhamento(Opcional).",
+ "List_of_departments_for_forward_description": "Permite definir uma lista restrita de departamentos que podem receber conversas desse departamento.",
"Livechat_abandoned_rooms_closed_custom_message": "Mensagem customizada para usar quando a sala for automaticamente fechada por abandono do visitante",
"Livechat_Monitors": "Monitores",
"Livechat_monitors": "Monitores de Livechat",