diff --git a/app/livechat/client/views/app/livechatDepartmentForm.html b/app/livechat/client/views/app/livechatDepartmentForm.html index 3305382d02ec..6621458a767e 100644 --- a/app/livechat/client/views/app/livechatDepartmentForm.html +++ b/app/livechat/client/views/app/livechatDepartmentForm.html @@ -55,6 +55,50 @@ + +
+
+ {{#if hasAvailableTags}} +
+ +
+ {{else}} +
+ +
+ {{/if}} +
+
+ +
+
+ {{{_ "Conversation_closing_tags_description"}}} +
+
+ {{#if hasChatClosingTags}} +
+ +
+ {{/if}} {{#if customFieldsTemplate}} {{> Template.dynamic template=customFieldsTemplate data=data }} {{/if}} diff --git a/app/livechat/client/views/app/livechatDepartmentForm.js b/app/livechat/client/views/app/livechatDepartmentForm.js index 47458c4b0013..4af23b45ddcf 100644 --- a/app/livechat/client/views/app/livechatDepartmentForm.js +++ b/app/livechat/client/views/app/livechatDepartmentForm.js @@ -79,6 +79,18 @@ Template.livechatDepartmentForm.helpers({ .some((button) => button.groups .some((group) => group.startsWith('livechat-department'))); }, + chatClosingTags() { + return Template.instance().chatClosingTags.get(); + }, + availableDepartmentTags() { + return Template.instance().availableDepartmentTags.get(); + }, + hasAvailableTags() { + return [...Template.instance().availableTags.get()].length > 0; + }, + hasChatClosingTags() { + return [...Template.instance().chatClosingTags.get()].length > 0; + }, }); Template.livechatDepartmentForm.events({ @@ -98,7 +110,7 @@ Template.livechatDepartmentForm.events({ const email = instance.$('input[name=email]').val(); const showOnOfflineForm = instance.$('input[name=showOnOfflineForm]:checked').val(); const requestTagBeforeClosingChat = instance.$('input[name=requestTagBeforeClosingChat]:checked').val(); - + const chatClosingTags = instance.chatClosingTags.get(); if (enabled !== '1' && enabled !== '0') { return toastr.error(t('Please_select_enabled_yes_or_no')); } @@ -119,6 +131,7 @@ Template.livechatDepartmentForm.events({ showOnOfflineForm: showOnOfflineForm === '1', requestTagBeforeClosingChat: requestTagBeforeClosingChat === '1', email: email.trim(), + chatClosingTags, }; } @@ -190,6 +203,33 @@ Template.livechatDepartmentForm.events({ instance.departmentAgents.set(instance.departmentAgents.get().filter((agent) => agent.agentId !== this.agentId)); }, + + 'click #addTag'(e, instance) { + e.stopPropagation(); + e.preventDefault(); + + const isSelect = [...instance.availableTags.get()].length > 0; + const elId = isSelect ? '#tagSelect' : '#tagInput'; + const elDefault = isSelect ? 'placeholder' : ''; + + const tag = $(elId).val(); + const chatClosingTags = [...instance.chatClosingTags.get()]; + if (tag === '' || chatClosingTags.indexOf(tag) > -1) { + return; + } + + chatClosingTags.push(tag); + instance.chatClosingTags.set(chatClosingTags); + $(elId).val(elDefault); + }, + + 'click .remove-tag'(e, instance) { + e.stopPropagation(); + e.preventDefault(); + + const chatClosingTags = [...instance.chatClosingTags.get()].filter((el) => el !== this.valueOf()); + instance.chatClosingTags.set(chatClosingTags); + }, }); Template.livechatDepartmentForm.onCreated(async function() { @@ -199,21 +239,37 @@ Template.livechatDepartmentForm.onCreated(async function() { this.tabBar = new RocketChatTabBar(); this.tabBar.showGroup(FlowRouter.current().route.name); this.tabBarData = new ReactiveVar(); + this.chatClosingTags = new ReactiveVar([]); + this.availableTags = new ReactiveVar([]); + this.availableDepartmentTags = new ReactiveVar([]); this.onSelectAgents = ({ item: agent }) => { this.selectedAgents.set([agent]); }; - this.onClickTagAgent = ({ username }) => { + this.onClickTagAgents = ({ username }) => { this.selectedAgents.set(this.selectedAgents.get().filter((user) => user.username !== username)); }; + this.loadAvailableTags = (departmentId) => { + Meteor.call('livechat:getTagsList', (err, tagsList) => { + this.availableTags.set(tagsList || []); + const tags = this.availableTags.get(); + const availableTags = tags + .filter(({ departments }) => departments.length === 0 || departments.indexOf(departmentId) > -1) + .map(({ name }) => name); + this.availableDepartmentTags.set(availableTags); + }); + }; + this.autorun(async () => { const id = FlowRouter.getParam('_id'); if (id) { const { department, agents } = await APIClient.v1.get(`livechat/department/${ FlowRouter.getParam('_id') }`); this.department.set(department); this.departmentAgents.set(agents); + this.chatClosingTags.set((department && department.chatClosingTags) || []); + this.loadAvailableTags(id); } }); }); diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 08b59ea10c04..3ebc6cc66a46 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -231,7 +231,7 @@ Template.visitorInfo.events({ 'click .close-livechat'(event) { event.preventDefault(); - const closeRoom = (comment) => Meteor.call('livechat:closeRoom', this.rid, comment, function(error/* , result*/) { + const closeRoom = (comment) => Meteor.call('livechat:closeRoom', this.rid, comment, { clientAction: true }, function(error/* , result*/) { if (error) { return handleError(error); } diff --git a/app/livechat/server/hooks/beforeCloseRoom.js b/app/livechat/server/hooks/beforeCloseRoom.js index 0a2f84b489b8..de4f13a7f8c5 100644 --- a/app/livechat/server/hooks/beforeCloseRoom.js +++ b/app/livechat/server/hooks/beforeCloseRoom.js @@ -3,20 +3,34 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../callbacks'; import { LivechatDepartment } from '../../../models'; -callbacks.add('livechat.beforeCloseRoom', (room) => { - const { departmentId } = room; +const concatUnique = (...arrays) => [...new Set([].concat(...arrays.filter(Array.isArray)))]; + +callbacks.add('livechat.beforeCloseRoom', ({ room, options }) => { + const { departmentId, tags: roomTags } = room; if (!departmentId) { - return room; + return; } const department = LivechatDepartment.findOneById(departmentId); - if (!department || !department.requestTagBeforeClosingChat) { - return room; + if (!department) { + return; + } + + const { requestTagBeforeClosingChat, chatClosingTags } = department; + const extraData = { + tags: concatUnique(roomTags, chatClosingTags), + }; + + if (!requestTagBeforeClosingChat) { + return extraData; } - if (room.tags && room.tags.length > 0) { - return room; + const { clientAction } = options; + const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0); + const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0; + if (!checkRoomTags || !checkDepartmentTags) { + throw new Meteor.Error('error-tags-must-be-assigned-before-closing-chat', 'Tag(s) must be assigned before closing the chat', { method: 'livechat.beforeCloseRoom' }); } - throw new Meteor.Error('error-tags-must-be-assigned-before-closing-chat', 'Tag(s) must be assigned before closing the chat', { method: 'livechat.beforeCloseRoom' }); + return extraData; }, callbacks.priority.HIGH, 'livechat-before-close-Room'); diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index 36ee6fc697c0..ac6734708319 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -314,18 +314,19 @@ export const Livechat = { return ret; }, - closeRoom({ user, visitor, room, comment }) { + closeRoom({ user, visitor, room, comment, options = {} }) { if (!room || room.t !== 'l' || !room.open) { return false; } - callbacks.run('livechat.beforeCloseRoom', room); + const extraData = callbacks.run('livechat.beforeCloseRoom', { room, options }); const now = new Date(); const closeData = { closedAt: now, chatDuration: (now.getTime() - room.ts) / 1000, + ...extraData, }; if (user) { @@ -807,6 +808,8 @@ export const Livechat = { showOnRegistration: Boolean, email: String, showOnOfflineForm: Boolean, + requestTagBeforeClosingChat: Match.Optional(Boolean), + chatClosingTags: Match.Optional([String]), }; // The Livechat Form department support addition/custom fields, so those fields need to be added before validating @@ -825,6 +828,11 @@ export const Livechat = { }), ])); + const { requestTagBeforeClosingChat, chatClosingTags } = departmentData; + if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { + throw new Meteor.Error('error-validating-department-chat-closing-tags', 'At least one closing tag is required when the department requires tag(s) on closing conversations.', { method: 'livechat:saveDepartment' }); + } + if (_id) { const department = LivechatDepartment.findOneById(_id); if (!department) { diff --git a/app/livechat/server/methods/closeRoom.js b/app/livechat/server/methods/closeRoom.js index f7f2d18942d5..d4b1776da579 100644 --- a/app/livechat/server/methods/closeRoom.js +++ b/app/livechat/server/methods/closeRoom.js @@ -5,7 +5,7 @@ import { Subscriptions, LivechatRooms } from '../../../models'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ - 'livechat:closeRoom'(roomId, comment) { + 'livechat:closeRoom'(roomId, comment, options = {}) { const userId = Meteor.userId(); if (!userId || !hasPermission(userId, 'close-livechat-room')) { throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:closeRoom' }); @@ -31,6 +31,7 @@ Meteor.methods({ user, room: LivechatRooms.findOneById(roomId), comment, + options, }); }, }); diff --git a/app/models/server/models/LivechatRooms.js b/app/models/server/models/LivechatRooms.js index 91e560ea4f64..f4de7632962b 100644 --- a/app/models/server/models/LivechatRooms.js +++ b/app/models/server/models/LivechatRooms.js @@ -421,16 +421,19 @@ export class LivechatRooms extends Base { closeByRoomId(roomId, closeInfo) { + const { closer, closedBy, closedAt, chatDuration, ...extraData } = closeInfo; + return this.update({ _id: roomId, t: 'l', }, { $set: { - closer: closeInfo.closer, - closedBy: closeInfo.closedBy, - closedAt: closeInfo.closedAt, - 'metrics.chatDuration': closeInfo.chatDuration, + closer, + closedBy, + closedAt, + 'metrics.chatDuration': chatDuration, 'v.status': 'offline', + ...extraData, }, $unset: { open: 1, diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index bcef536dfcfc..1932dbd50e57 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -737,6 +737,8 @@ "Conversation": "Conversation", "Conversations": "Conversations", "Conversation_closed": "Conversation closed: __comment__.", + "Conversation_closing_tags": "Conversation closing tags", + "Conversation_closing_tags_description": "Closing tags will be automatically assigned to conversations at closing.", "Conversation_finished": "Conversation Finished", "Conversation_finished_message": "Conversation Finished Message", "Conversation_finished_text": "Conversation Finished Text", @@ -1402,6 +1404,7 @@ "error-logged-user-not-in-room": "You are not in the room `%s`", "error-user-registration-disabled": "User registration is disabled", "error-user-registration-secret": "User registration is only allowed via Secret URL", + "error-validating-department-chat-closing-tags": "At least one closing tag is required when the department requires tag(s) on closing conversations.", "error-you-are-last-owner": "You are the last owner. Please set new owner before leaving the room.", "error-starring-message": "Message could not be stared", "Error_404": "Error:404", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 6b18a52a876d..95d1e3091854 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -677,6 +677,8 @@ "Conversation": "Conversa", "Conversations": "Conversas", "Conversation_closed": "Chat encerrado: __comment__.", + "Conversation_closing_tags": "Tags de encerramento de conversa", + "Conversation_closing_tags_description": "As tags de encerramento serão automaticamente atribuídas as conversas no seu encerramento.", "Conversation_finished": "Conversa concluída", "Conversation_finished_message": "Mensagem de conversa concluída", "Conversation_finished_text": "Texto de conversa concluída", @@ -1303,6 +1305,7 @@ "error-logged-user-not-in-room": "Você não está na sala `%s`", "error-user-registration-disabled": "O registro do usuário está desativado", "error-user-registration-secret": "O registro de usuário é permitido somente via URL secreta", + "error-validating-department-chat-closing-tags": "Pelo menos uma tag de encerramento é necessária quando o departamento exige tags no encerramento de conversas.", "error-you-are-last-owner": "Você é o último proprietário da sala. Por favor defina um novo proprietário antes de sair.", "Error_404": "Erro 404", "Error_changing_password": "Erro ao alterar senha",