diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index d373d74f4c6f..0b6453f92ab7 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -311,7 +311,10 @@ export const useQuickActions = (): { const canSendTranscriptPDF = usePermission('request-pdf-transcript'); const canCloseRoom = usePermission('close-livechat-room'); const canCloseOthersRoom = usePermission('close-others-livechat-room'); - const canPlaceChatOnHold = Boolean(!room.onHold && room.u && !(room as any).lastMessage?.token && manualOnHoldAllowed); + const restrictedOnHold = useSetting('Livechat_allow_manual_on_hold_upon_agent_engagement_only'); + const canRoomBePlacedOnHold = !room.onHold && room.u; + const canAgentPlaceOnHold = !room.lastMessage?.token; + const canPlaceChatOnHold = Boolean(manualOnHoldAllowed && canRoomBePlacedOnHold && (!restrictedOnHold || canAgentPlaceOnHold)); const isRoomOverMacLimit = useIsRoomOverMacLimit(room); const hasPermissionButtons = (id: string): boolean => { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts index e6c9311ee82d..579299c57ffa 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts @@ -15,10 +15,10 @@ API.v1.addRoute( async post() { const { roomId } = this.bodyParams; - type Room = Pick; + type Room = Pick; const room = await LivechatRooms.findOneById(roomId, { - projection: { _id: 1, t: 1, open: 1, onHold: 1, lastMessage: 1, servedBy: 1 }, + projection: { _id: 1, t: 1, open: 1, onHold: 1, u: 1, lastMessage: 1, servedBy: 1 }, }); if (!room) { throw new Error('error-invalid-room'); @@ -51,7 +51,7 @@ API.v1.addRoute( throw new Error('invalid-param'); } - type Room = Pick; + type Room = Pick; const room = await LivechatRooms.findOneById(roomId, { projection: { t: 1, open: 1, onHold: 1, servedBy: 1 }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts index 3ef9633267c8..0d6c42d769ee 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts @@ -8,6 +8,7 @@ import { LivechatRooms, Subscriptions, LivechatInquiry } from '@rocket.chat/mode import { dispatchAgentDelegated } from '../../../../../app/livechat/server/lib/Helper'; import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; +import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelEEService { @@ -40,8 +41,12 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE if (room.onHold) { throw new Error('error-room-is-already-on-hold'); } - if (room.lastMessage?.token) { - throw new Error('error-contact-sent-last-message-so-cannot-place-on-hold'); + const restrictedOnHold = settings.get('Livechat_allow_manual_on_hold_upon_agent_engagement_only'); + const canRoomBePlacedOnHold = !room.onHold; + const canAgentPlaceOnHold = !room.lastMessage?.token; + const canPlaceChatOnHold = canRoomBePlacedOnHold && (!restrictedOnHold || canAgentPlaceOnHold); + if (!canPlaceChatOnHold) { + throw new Error('error-cannot-place-chat-on-hold'); } if (!room.servedBy) { throw new Error('error-unserved-rooms-cannot-be-placed-onhold'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/settings.ts b/apps/meteor/ee/app/livechat-enterprise/server/settings.ts index 0f21374ebf83..2863284d6cff 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/settings.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/settings.ts @@ -198,6 +198,17 @@ export const createSettings = async (): Promise => { enableQuery: omnichannelEnabledQuery, }); + await settingsRegistry.add('Livechat_allow_manual_on_hold_upon_agent_engagement_only', true, { + type: 'boolean', + group: 'Omnichannel', + section: 'Sessions', + enterprise: true, + invalidValue: false, + public: true, + modules: ['livechat-enterprise'], + enableQuery: { _id: 'Livechat_allow_manual_on_hold', value: true }, + }); + await settingsRegistry.add('Livechat_auto_transfer_chat_timeout', 0, { type: 'int', group: 'Omnichannel', diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index a41db5875efa..ea1db6cc4787 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2110,6 +2110,7 @@ "error-you-are-last-owner": "You are the last owner. Please set new owner before leaving the room.", "error-saving-sla": "An error ocurred while saving the SLA", "error-duplicated-sla": "An SLA with the same name or due time already exists", + "error-cannot-place-chat-on-hold": "You cannot place chat on-hold", "error-contact-sent-last-message-so-cannot-place-on-hold": "You cannot place chat on-hold, when the Contact has sent the last message", "error-unserved-rooms-cannot-be-placed-onhold": "Room cannot be placed on hold before being served", "Workspace_exceeded_MAC_limit_disclaimer": "The workspace has exceeded the monthly limit of active contacts. Talk to your workspace admin to address this issue.", @@ -3075,7 +3076,9 @@ "Livechat_agents": "Omnichannel agents", "Livechat_Agents": "Agents", "Livechat_allow_manual_on_hold": "Allow agents to manually place chat On Hold", - "Livechat_allow_manual_on_hold_Description": "If enabled, the agent will get a new option to place a chat On Hold, provided the agent has sent the last message", + "Livechat_allow_manual_on_hold_Description": "If enabled, the agent will get the option to place a chat On Hold", + "Livechat_allow_manual_on_hold_upon_agent_engagement_only" : "Chats on hold only after agent engagement", + "Livechat_allow_manual_on_hold_upon_agent_engagement_only_Description": "Only allow chats to be put on hold if the agent is the one who sent the last message in the conversation.", "Livechat_AllowedDomainsList": "Livechat Allowed Domains", "Livechat_Appearance": "Livechat Appearance", "Livechat_auto_close_on_hold_chats_custom_message": "Custom message for closed chats in On Hold queue", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index c0fd1664119a..775b6bfc60af 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -1829,6 +1829,7 @@ "error-no-permission-team-channel": "Você não tem permissão para incluir este canal à equipe", "error-no-owner-channel": "Apenas proprietários podem adicionar este canal à equipe", "error-you-are-last-owner": "Você é o último proprietário da sala. Defina um novo proprietário antes de sair.", + "error-cannot-place-chat-on-hold": "Você não pode colocar a conversa em espera", "Errors_and_Warnings": "Erros e avisos", "Esc_to": "Esc para", "Estimated_wait_time": "Tempo estimado de espera (tempo em minutos)", @@ -2634,7 +2635,9 @@ "Livechat_agents": "Agentes do omnichannel", "Livechat_Agents": "Agentes", "Livechat_allow_manual_on_hold": "Permitir que agentes coloquem a conversa em espera manualmente", - "Livechat_allow_manual_on_hold_Description": "Se habilitado, o agente terá uma nova opção para colocar a conversa em espera, desde que o agente tenha enviado a última mensagem", + "Livechat_allow_manual_on_hold_Description": "Se habilitado, o agente terá uma nova opção para colocar a conversa em espera", + "Livechat_allow_manual_on_hold_upon_agent_engagement_only": "Conversas em espera somente após interação do agente", + "Livechat_allow_manual_on_hold_upon_agent_engagement_only_Description": "Permitir que conversas sejam colocados em espera apenas quando o agente for quem enviou a última mensagem", "Livechat_AllowedDomainsList": "Domínios permitidos em Livechat", "Livechat_Appearance": "Aparência do livechat", "Livechat_auto_close_on_hold_chats_custom_message": "Mensagem personalizada para conversas encerradas na fila em espera", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json index e31b0d86bb0f..7fe110f038ee 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -1281,6 +1281,7 @@ "error-user-registration-disabled": "O registo de utilizadores está desabilitado", "error-user-registration-secret": "O registo de utilizadores só é permitido via URL privada", "error-you-are-last-owner": "É o último proprietário da sala. Por favor defina um novo proprietário antes de sair.", + "error-cannot-place-chat-on-hold": "Você não pode colocar a conversa em espera", "Errors_and_Warnings": "Erros e Avisos", "Esc_to": "Prima Esc para", "Event_Trigger": "Gerador de Eventos", @@ -3174,4 +3175,4 @@ "registration.component.form.invalidConfirmPass": "A confirmação da senha não é igual à senha", "registration.component.form.confirmPassword": "Confirmar a senha", "registration.component.form.sendConfirmationEmail": "Enviar email de confirmação" -} \ No newline at end of file +} diff --git a/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts b/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts index 040997133040..780836077d1a 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts @@ -39,6 +39,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const user: IUser = await createUser(); const userCredentials = await login(user.username, password); await createAgent(user.username); + await updateSetting('Livechat_allow_manual_on_hold', true); agent2 = { user, @@ -48,8 +49,9 @@ import { IS_EE } from '../../../e2e/config/constants'; after(async () => { await deleteUser(agent2.user); + await updateSetting('Livechat_allow_manual_on_hold', false); + await updateSetting('Livechat_allow_manual_on_hold_upon_agent_engagement_only', true); }); - describe('livechat/room.onHold', () => { it('should fail if user doesnt have on-hold-livechat-room permission', async () => { await updatePermission('on-hold-livechat-room', []); @@ -115,7 +117,7 @@ import { IS_EE } from '../../../e2e/config/constants'; .expect(400); expect(response.body.success).to.be.false; - expect(response.body.error).to.be.equal('error-contact-sent-last-message-so-cannot-place-on-hold'); + expect(response.body.error).to.be.equal('error-cannot-place-chat-on-hold'); }); it('should fail if room is closed', async () => { const visitor = await createVisitor(); @@ -151,7 +153,6 @@ import { IS_EE } from '../../../e2e/config/constants'; it('should put room on hold', async () => { const { room } = await startANewLivechatRoomAndTakeIt(); await sendAgentMessage(room._id); - const response = await request .post(api('livechat/room.onHold')) .set(credentials) @@ -165,6 +166,35 @@ import { IS_EE } from '../../../e2e/config/constants'; const updatedRoom = await getLivechatRoomInfo(room._id); expect(updatedRoom.onHold).to.be.true; }); + it('Should put room on hold, even in the visitor sent the last message', async () => { + const { room, visitor } = await startANewLivechatRoomAndTakeIt(); + await updateSetting('Livechat_allow_manual_on_hold_upon_agent_engagement_only', false); + await sendMessage(room._id, '-', visitor.token); + const response = await request + .post(api('livechat/room.onHold')) + .set(credentials) + .send({ + roomId: room._id, + }) + .expect(200); + expect(response.body.success).to.be.true; + const updatedRoom = await getLivechatRoomInfo(room._id); + expect(updatedRoom.onHold).to.be.true; + }); + it('should not put room on hold when visitor sent the last message', async () => { + const { room, visitor } = await startANewLivechatRoomAndTakeIt(); + await updateSetting('Livechat_allow_manual_on_hold_upon_agent_engagement_only', true); + await sendMessage(room._id, '-', visitor.token); + const response = await request + .post(api('livechat/room.onHold')) + .set(credentials) + .send({ + roomId: room._id, + }) + .expect(400); + expect(response.body.success).to.be.false; + expect(response.body.error).to.be.equal('error-cannot-place-chat-on-hold'); + }); }); describe('livechat/room.resumeOnHold', () => { diff --git a/packages/core-services/src/types/IOmnichannelEEService.ts b/packages/core-services/src/types/IOmnichannelEEService.ts index 8c8a4b75db1d..78d2666858fd 100644 --- a/packages/core-services/src/types/IOmnichannelEEService.ts +++ b/packages/core-services/src/types/IOmnichannelEEService.ts @@ -4,13 +4,13 @@ import type { IServiceClass } from './ServiceClass'; export interface IOmnichannelEEService extends IServiceClass { placeRoomOnHold( - room: Pick, + room: Pick, comment: string, onHoldBy: Pick, ): Promise; resumeRoomOnHold( - room: Pick, + room: Pick, comment: string, resumeBy: Pick, clientAction?: boolean,