diff --git a/app/livechat/client/views/app/tabbar/visitorForward.js b/app/livechat/client/views/app/tabbar/visitorForward.js index 84860188cf996..871f6448f31a4 100644 --- a/app/livechat/client/views/app/tabbar/visitorForward.js +++ b/app/livechat/client/views/app/tabbar/visitorForward.js @@ -111,6 +111,7 @@ Template.visitorForward.events({ const transferData = { roomId: instance.room.get()._id, comment: event.target.comment.value, + clientAction: true, }; const [user] = instance.selectedAgents.get(); diff --git a/app/livechat/server/lib/Helper.js b/app/livechat/server/lib/Helper.js index f7a321a25e953..9341a3acb7326 100644 --- a/app/livechat/server/lib/Helper.js +++ b/app/livechat/server/lib/Helper.js @@ -13,6 +13,7 @@ import { Apps, AppEvents } from '../../../apps/server'; import notifications from '../../../notifications/server/lib/Notifications'; import { sendNotification } from '../../../lib/server'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { queueInquiry } from './QueueManager'; export const allowAgentSkipQueue = (agent) => { check(agent, Match.ObjectIncluding({ @@ -273,7 +274,7 @@ export const forwardRoomToAgent = async (room, transferData) => { return false; } - const { userId: agentId } = transferData; + const { userId: agentId, clientAction } = transferData; const user = Users.findOneOnlineAgentById(agentId); if (!user) { throw new Meteor.Error('error-user-is-offline', 'User is offline', { function: 'forwardRoomToAgent' }); @@ -291,9 +292,11 @@ export const forwardRoomToAgent = async (room, transferData) => { const { username } = user; const agent = { agentId, username }; - // There are some Enterprise features that may interrupt the fowarding process + // Remove department from inquiry to make sure the routing algorithm treat this as forwarding to agent and not as forwarding to department + inquiry.department = undefined; + // There are some Enterprise features that may interrupt the forwarding process // Due to that we need to check whether the agent has been changed or not - const roomTaken = await RoutingManager.takeInquiry(inquiry, agent); + const roomTaken = await RoutingManager.takeInquiry(inquiry, agent, { ...clientAction && { clientAction } }); if (!roomTaken) { return false; } @@ -356,7 +359,7 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { throw new Meteor.Error('error-forwarding-chat-same-department', 'The selected department and the current room department are the same', { function: 'forwardRoomToDepartment' }); } - const { userId: agentId } = transferData; + const { userId: agentId, clientAction } = transferData; if (agentId) { let user = Users.findOneOnlineAgentById(agentId); if (!user) { @@ -378,13 +381,13 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { // Fake the department to forward the inquiry - Case the forward process does not success // the inquiry will stay in the same original department inquiry.department = departmentId; - const roomTaken = await RoutingManager.delegateInquiry(inquiry, agent); + const roomTaken = await RoutingManager.delegateInquiry(inquiry, agent, { forwardingToDepartment: { oldDepartmentId }, ...clientAction && { clientAction } }); if (!roomTaken) { return false; } - const { servedBy } = roomTaken; - if (oldServedBy && servedBy && oldServedBy._id === servedBy._id) { + const { servedBy, chatQueued } = roomTaken; + if (!chatQueued && oldServedBy && servedBy && oldServedBy._id === servedBy._id) { return false; } @@ -392,12 +395,18 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { if (oldServedBy) { removeAgentFromSubscription(rid, oldServedBy); } - if (servedBy) { + if (!chatQueued && servedBy) { Messages.createUserJoinWithRoomIdAndUser(rid, servedBy); } updateChatDepartment({ rid, newDepartmentId: departmentId, oldDepartmentId }); + if (chatQueued) { + LivechatInquiry.readyInquiry(inquiry._id); + const newInquiry = LivechatInquiry.findOneById(inquiry._id); + await queueInquiry(room, newInquiry); + } + const { token } = guest; Livechat.setDepartmentForGuest({ token, department: departmentId }); diff --git a/app/livechat/server/lib/RoutingManager.js b/app/livechat/server/lib/RoutingManager.js index 9f147539d4637..7de0a4ce0a744 100644 --- a/app/livechat/server/lib/RoutingManager.js +++ b/app/livechat/server/lib/RoutingManager.js @@ -43,7 +43,7 @@ export const RoutingManager = { return this.getMethod().getNextAgent(department, ignoreAgentId); }, - async delegateInquiry(inquiry, agent) { + async delegateInquiry(inquiry, agent, options = {}) { const { department, rid } = inquiry; if (!agent || (agent.username && !Users.findOneOnlineAgentByUsername(agent.username) && !allowAgentSkipQueue(agent))) { agent = await this.getNextAgent(department); @@ -53,7 +53,7 @@ export const RoutingManager = { return LivechatRooms.findOneById(rid); } - return this.takeInquiry(inquiry, agent); + return this.takeInquiry(inquiry, agent, options); }, assignAgent(inquiry, agent) { @@ -134,8 +134,7 @@ export const RoutingManager = { agent = await callbacks.run('livechat.checkAgentBeforeTakeInquiry', { agent, inquiry, options }); if (!agent) { - await callbacks.run('livechat.onAgentAssignmentFailed', { inquiry, room, options }); - return null; + return callbacks.run('livechat.onAgentAssignmentFailed', { inquiry, room, options }); } if (room.onHold) { diff --git a/app/livechat/server/methods/transfer.js b/app/livechat/server/methods/transfer.js index 241a031761ae7..7f99c8f088757 100644 --- a/app/livechat/server/methods/transfer.js +++ b/app/livechat/server/methods/transfer.js @@ -17,6 +17,7 @@ Meteor.methods({ userId: Match.Optional(String), departmentId: Match.Optional(String), comment: Match.Optional(String), + clientAction: Match.Optional(Boolean), }); const room = LivechatRooms.findOneById(transferData.roomId); diff --git a/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx b/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx index 887e3384d910d..9cea78ed6a08a 100644 --- a/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx +++ b/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx @@ -137,12 +137,14 @@ const QuickActions: FC = ({ room, className }) => { } const transferData: { roomId: string; + clientAction: boolean; comment?: string; departmentId?: string; userId?: string; } = { roomId: rid, comment, + clientAction: true, }; if (departmentId) { @@ -153,10 +155,15 @@ const QuickActions: FC = ({ room, className }) => { } try { - await forwardChat(transferData); - closeModal(); + const result = await forwardChat(transferData); + if (!result) { + throw new Error( + departmentId ? t('error-no-agents-online-in-department') : t('error-forwarding-chat'), + ); + } toastr.success(t('Transferred')); FlowRouter.go('/'); + closeModal(); } catch (error) { handleError(error); } diff --git a/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js b/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js index 0f772543cf63f..b7fe80a221429 100644 --- a/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js +++ b/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js @@ -35,7 +35,7 @@ callbacks.add('livechat.checkAgentBeforeTakeInquiry', async ({ agent, inquiry, o const { queueInfo: { chats = 0 } = {} } = user; if (maxNumberSimultaneousChat <= chats) { callbacks.run('livechat.onMaxNumberSimultaneousChatsReached', inquiry); - if (options.clientAction) { + if (options.clientAction && !options.forwardingToDepartment) { throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed'); } diff --git a/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts b/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts index b8592fff228d7..f2a3e9c44d885 100644 --- a/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts +++ b/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts @@ -1,25 +1,45 @@ import { callbacks } from '../../../../../app/callbacks/server'; import { LivechatInquiry, Subscriptions, LivechatRooms } from '../../../../../app/models/server'; import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; +import { settings } from '../../../../../app/settings/server'; -const handleOnAgentAssignmentFailed = async ({ inquiry, room }: { inquiry: any; room: any }): Promise => { - if (!inquiry || !room || !room.onHold) { +const handleOnAgentAssignmentFailed = async ({ inquiry, room, options }: { inquiry: any; room: any; options: { forwardingToDepartment?: { oldDepartmentId: string; transferData: any }; clientAction?: boolean} }): Promise => { + if (!inquiry || !room) { return; } - const { _id: roomId, servedBy } = room; + if (room.onHold) { + const { _id: roomId } = room; - const { _id: inquiryId } = inquiry; - LivechatInquiry.readyInquiry(inquiryId); - LivechatInquiry.removeDefaultAgentById(inquiryId); - LivechatRooms.removeAgentByRoomId(roomId); - if (servedBy?._id) { - Subscriptions.removeByRoomIdAndUserId(roomId, servedBy._id); + const { _id: inquiryId } = inquiry; + LivechatInquiry.readyInquiry(inquiryId); + LivechatInquiry.removeDefaultAgentById(inquiryId); + LivechatRooms.removeAgentByRoomId(roomId); + Subscriptions.removeByRoomId(roomId); + const newInquiry = LivechatInquiry.findOneById(inquiryId); + + await queueInquiry(room, newInquiry); + + return; + } + + if (!settings.get('Livechat_waiting_queue')) { + return; } - const newInquiry = LivechatInquiry.findOneById(inquiryId); + const { forwardingToDepartment: { oldDepartmentId } = {}, forwardingToDepartment } = options; + if (!forwardingToDepartment) { + return; + } + + const { department: newDepartmentId } = inquiry; + + if (!newDepartmentId || !oldDepartmentId || newDepartmentId === oldDepartmentId) { + return; + } - await queueInquiry(room, newInquiry); + room.chatQueued = true; + return room; }; callbacks.add('livechat.onAgentAssignmentFailed', handleOnAgentAssignmentFailed, callbacks.priority.HIGH, 'livechat-agent-assignment-failed'); diff --git a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 8c11c466011ac..11d276937226e 100644 --- a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -5,7 +5,7 @@ import { Users } from '../../../../../app/models'; import { LivechatInquiry, OmnichannelQueue } from '../../../../../app/models/server/raw'; import LivechatUnit from '../../../models/server/models/LivechatUnit'; import LivechatTag from '../../../models/server/models/LivechatTag'; -import { LivechatRooms, Messages } from '../../../../../app/models/server'; +import { LivechatRooms, Subscriptions, Messages } from '../../../../../app/models/server'; import LivechatPriority from '../../../models/server/models/LivechatPriority'; import { addUserRoles, removeUserFromRoles } from '../../../../../app/authorization/server'; import { processWaitingQueue, removePriorityFromRooms, updateInquiryQueuePriority, updatePriorityInquiries, updateRoomPriorityHistory } from './Helper'; @@ -173,6 +173,7 @@ export const LivechatEnterprise = { return false; } LivechatRooms.setOnHold(roomId); + Subscriptions.setOnHold(roomId); Messages.createOnHoldHistoryWithRoomIdMessageAndUser(roomId, comment, onHoldBy); Meteor.defer(() => { @@ -190,6 +191,7 @@ export const LivechatEnterprise = { await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); LivechatRooms.unsetAllOnHoldFieldsByRoomId(roomId); + Subscriptions.unsetOnHold(roomId); }, }; diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index 5caae4ae074cb..f0ed7c7271e08 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { LivechatRooms, LivechatInquiry, Messages, Users, LivechatVisitors } from '../../../../../app/models/server'; -import { LivechatEnterprise } from '../lib/LivechatEnterprise'; +import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { callbacks } from '../../../../../app/callbacks/server'; const resolveOnHoldCommentInfo = (options: { clientAction: boolean }, room: any, onHoldChatResumedBy: any): string => { @@ -38,7 +38,8 @@ Meteor.methods({ throw new Meteor.Error('inquiry-not-found', 'Error! No inquiry found for this room', { method: 'livechat:resumeOnHold' }); } - LivechatEnterprise.releaseOnHoldChat(room); + const { servedBy: { _id: agentId, username } } = room; + await RoutingManager.takeInquiry(inquiry, { agentId, username }, options); const onHoldChatResumedBy = options.clientAction ? Meteor.user() : Users.findOneById('rocket.cat'); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index b4d179a68e2ee..ff6289cb56b43 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1612,6 +1612,7 @@ "error-essential-app-disabled": "Error: a Rocket.Chat App that is essential for this is disabled. Please contact your administrator", "error-field-unavailable": "__field__ is already in use :(", "error-file-too-large": "File is too large", + "error-forwarding-chat": "Something went wrong while forwarding the chat, Please try again later.", "error-forwarding-chat-same-department": "The selected department and the current room department are the same", "error-forwarding-department-target-not-allowed": "The forwarding to the target department is not allowed.", "error-guests-cant-have-other-roles": "Guest users can't have any other role.", @@ -1672,6 +1673,7 @@ "error-message-size-exceeded": "Message size exceeds Message_MaxAllowedSize", "error-missing-unsubscribe-link": "You must provide the [unsubscribe] link.", "error-no-tokens-for-this-user": "There are no tokens for this user", + "error-no-agents-online-in-department": "No agents online in the department", "error-no-message-for-unread": "There are no messages to mark unread", "error-not-allowed": "Not allowed", "error-not-authorized": "Not authorized", @@ -4501,4 +4503,4 @@ "Your_temporary_password_is_password": "Your temporary password is [password].", "Your_TOTP_has_been_reset": "Your Two Factor TOTP has been reset.", "Your_workspace_is_ready": "Your workspace is ready to use 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 6c5b220357b46..342cfb55b26b6 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -1424,6 +1424,7 @@ "error-message-editing-blocked": "Edição de mensagens está bloqueada", "error-message-size-exceeded": "O tamanho da mensagem excede Message_MaxAllowedSize", "error-missing-unsubscribe-link": "Você deve fornecer o link para desinscrever-se: [unsubscribe].", + "error-no-agents-online-in-department": "Nenhum agente online no departamento", "error-no-tokens-for-this-user": "Não existem tokens para este usuário", "error-no-message-for-unread": "Não há mensagens para serem marcadas como não lidas", "error-not-allowed": "Não permitido", @@ -3661,4 +3662,4 @@ "Your_question": "A sua pergunta", "Your_server_link": "O link do seu servidor", "Your_workspace_is_ready": "O seu espaço de trabalho está pronto a usar 🎉" -} \ No newline at end of file +}