diff --git a/apps/meteor/app/livechat/lib/messageTypes.ts b/apps/meteor/app/livechat/lib/messageTypes.ts index 65872433a25ff..c30916d4b4c9b 100644 --- a/apps/meteor/app/livechat/lib/messageTypes.ts +++ b/apps/meteor/app/livechat/lib/messageTypes.ts @@ -48,8 +48,20 @@ MessageTypes.registerType({ ...(comment && { comment }), }), queue: (): string => - TAPi18n.__('Livechat_transfer_return_to_the_queue', { + TAPi18n.__(`Livechat_transfer_return_to_the_queue${commentLabel}`, { from, + ...(comment && { comment }), + }), + autoTransferUnansweredChatsToAgent: (): string => + TAPi18n.__(`Livechat_transfer_to_agent_auto_transfer_unanswered_chat`, { + from, + to: message?.transferData?.transferredTo?.name || message?.transferData?.transferredTo?.username || '', + duration: comment, + }), + autoTransferUnansweredChatsToQueue: (): string => + TAPi18n.__(`Livechat_transfer_return_to_the_queue_auto_transfer_unanswered_chat`, { + from, + duration: comment, }), }; return { diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 9505f15c1af8c..af5f779a214c4 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -751,7 +751,7 @@ export const Livechat = { return RoutingManager.transferRoom(room, guest, transferData); }, - returnRoomAsInquiry(rid, departmentId) { + returnRoomAsInquiry(rid, departmentId, overrideTransferData = {}) { Livechat.logger.debug(`Transfering room ${rid} to ${departmentId ? 'department' : ''} queue`); const room = LivechatRooms.findOneById(rid); if (!room) { @@ -791,7 +791,7 @@ export const Livechat = { const transferredBy = normalizeTransferredByData(user, room); Livechat.logger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); - const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy }; + const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; try { this.saveTransferHistory(room, transferData); RoutingManager.unassignAgent(inquiry, departmentId); diff --git a/apps/meteor/app/models/server/models/LivechatRooms.js b/apps/meteor/app/models/server/models/LivechatRooms.js index cef4529b21b0b..9759b85b1488c 100644 --- a/apps/meteor/app/models/server/models/LivechatRooms.js +++ b/apps/meteor/app/models/server/models/LivechatRooms.js @@ -835,45 +835,6 @@ export class LivechatRooms extends Base { return this.update(query, update); } - setAutoTransferredAtById(roomId) { - const query = { - _id: roomId, - }; - const update = { - $set: { - autoTransferredAt: new Date(), - }, - }; - - return this.update(query, update); - } - - setAutoTransferOngoingById(roomId) { - const query = { - _id: roomId, - }; - const update = { - $set: { - autoTransferOngoing: true, - }, - }; - - return this.update(query, update); - } - - unsetAutoTransferOngoingById(roomId) { - const query = { - _id: roomId, - }; - const update = { - $unset: { - autoTransferOngoing: 1, - }, - }; - - return this.update(query, update); - } - changeVisitorByRoomId(roomId, { _id, username, token }) { const query = { _id: roomId, diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx index b45f0771fd09e..e0417ef2d4722 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx @@ -21,6 +21,7 @@ export const OmnichannelLivechatToggle = (): ReactElement => { return ( => { const { _id: rid } = room; @@ -23,7 +20,12 @@ const handleAfterOnHold = async (room: any = {}): Promise => { } cbLogger.debug(`Scheduling room ${rid} to be closed in ${autoCloseOnHoldChatTimeout} seconds`); - await AutoCloseOnHoldScheduler.scheduleRoom(room._id, autoCloseOnHoldChatTimeout, customCloseMessage); + const closeComment = + settings.get('Livechat_auto_close_on_hold_chats_custom_message') || + TAPi18n.__('Closed_automatically_because_chat_was_onhold_for_seconds', { + onHoldTime: autoCloseOnHoldChatTimeout, + }); + await AutoCloseOnHoldScheduler.scheduleRoom(room._id, autoCloseOnHoldChatTimeout, closeComment); }; settings.watch('Livechat_auto_close_on_hold_chats_timeout', (value) => { @@ -33,7 +35,3 @@ settings.watch('Livechat_auto_close_on_hold_chats_timeout', (value) => { } callbacks.add('livechat:afterOnHold', handleAfterOnHold, callbacks.priority.HIGH, 'livechat-auto-close-on-hold'); }); - -settings.watch('Livechat_auto_close_on_hold_chats_custom_message', (value) => { - customCloseMessage = (value as string) || DEFAULT_CLOSED_MESSAGE; -}); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts index 3d782764a3a77..e0b71d2d8005e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts @@ -2,16 +2,18 @@ import { Agenda } from '@rocket.chat/agenda'; import { MongoInternals } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; +import { LivechatRooms, Users } from '@rocket.chat/models'; +import type { IUser } from '@rocket.chat/core-typings'; import { Livechat } from '../../../../../app/livechat/server'; -import { LivechatRooms, Users } from '../../../../../app/models/server'; -const schedulerUser = Users.findOneById('rocket.cat'); const SCHEDULER_NAME = 'omnichannel_auto_close_on_hold_scheduler'; class AutoCloseOnHoldSchedulerClass { scheduler: Agenda; + schedulerUser: IUser; + running: boolean; public init(): void { @@ -48,8 +50,8 @@ class AutoCloseOnHoldSchedulerClass { const { roomId, comment } = data; const payload = { - user: schedulerUser, - room: LivechatRooms.findOneById(roomId), + user: await this.getSchedulerUser(), + room: await LivechatRooms.findOneById(roomId), comment, options: {}, visitor: undefined, @@ -57,6 +59,18 @@ class AutoCloseOnHoldSchedulerClass { Livechat.closeRoom(payload); } + + private async getSchedulerUser(): Promise { + if (!this.schedulerUser) { + const schedulerUser = await Users.findOneById('rocket.cat'); + if (!schedulerUser) { + throw new Error('Scheduler user not found'); + } + this.schedulerUser = schedulerUser; + } + + return this.schedulerUser; + } } export const AutoCloseOnHoldScheduler = new AutoCloseOnHoldSchedulerClass(); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index dca87efc6465e..83670508ea46c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -1,12 +1,14 @@ import { Agenda } from '@rocket.chat/agenda'; import { MongoInternals } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; +import { LivechatRooms } from '@rocket.chat/models'; import type { IUser } from '@rocket.chat/core-typings'; -import { LivechatRooms, Users } from '../../../../../app/models/server'; +import { Users } from '../../../../../app/models/server'; import { Livechat } from '../../../../../app/livechat/server'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { forwardRoomToAgent } from '../../../../../app/livechat/server/lib/Helper'; +import { settings } from '../../../../../app/settings/server'; const schedulerUser = Users.findOneById('rocket.cat'); const SCHEDULER_NAME = 'omnichannel_scheduler'; @@ -53,7 +55,7 @@ class AutoTransferChatSchedulerClass { } private async transferRoom(roomId: string): Promise { - const room = LivechatRooms.findOneById(roomId, { + const room = await LivechatRooms.findOneById(roomId, { _id: 1, v: 1, servedBy: 1, @@ -69,8 +71,14 @@ class AutoTransferChatSchedulerClass { servedBy: { _id: ignoreAgentId }, } = room; + const timeoutDuration = settings.get('Livechat_auto_transfer_chat_timeout').toString(); + if (!RoutingManager.getConfig().autoAssignAgent) { - return Livechat.returnRoomAsInquiry(room._id, departmentId); + return Livechat.returnRoomAsInquiry(room._id, departmentId, { + scope: 'autoTransferUnansweredChatsToQueue', + comment: timeoutDuration, + transferredBy: schedulerUser, + }); } const agent = await RoutingManager.getNextAgent(departmentId, ignoreAgentId); @@ -79,6 +87,8 @@ class AutoTransferChatSchedulerClass { userId: agent.agentId, transferredBy: schedulerUser, transferredTo: agent, + scope: 'autoTransferUnansweredChatsToAgent', + comment: timeoutDuration, }); } @@ -89,7 +99,7 @@ class AutoTransferChatSchedulerClass { const { roomId } = data; if (await this.transferRoom(roomId)) { - LivechatRooms.setAutoTransferredAtById(roomId); + await LivechatRooms.setAutoTransferredAtById(roomId); } await this.unscheduleRoom(roomId); diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 515088dd09c26..2bc25e88e0250 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -984,6 +984,7 @@ "Closed": "Closed", "Closed_At": "Closed at", "Closed_automatically": "Closed automatically by the system", + "Closed_automatically_because_chat_was_onhold_for_seconds": "Closed automatically because chat was On Hold for __onHoldTime__ seconds", "Closed_automatically_chat_queued_too_long": "Closed automatically by the system (queue maximum time exceeded)", "Closed_by_visitor": "Closed by visitor", "Closing_chat": "Closing chat", @@ -2938,8 +2939,11 @@ "Livechat_transcript_request_has_been_canceled": "The chat transcription request has been canceled.", "Livechat_transcript_sent": "Omnichannel transcript sent", "Livechat_transfer_return_to_the_queue": "__from__ returned the chat to the queue", + "Livechat_transfer_return_to_the_queue_with_a_comment": "__from__ returned the chat to the queue with a comment: __comment__", + "Livechat_transfer_return_to_the_queue_auto_transfer_unanswered_chat": "__from__ returned the chat to the queue since it was unanswered for __duration__ seconds", "Livechat_transfer_to_agent": "__from__ transferred the chat to __to__", "Livechat_transfer_to_agent_with_a_comment": "__from__ transferred the chat to __to__ with a comment: __comment__", + "Livechat_transfer_to_agent_auto_transfer_unanswered_chat": "__from__ transferred the chat to __to__ since it was unanswered for __duration__ seconds", "Livechat_transfer_to_department": "__from__ transferred the chat to the department __to__", "Livechat_transfer_to_department_with_a_comment": "__from__ transferred the chat to the department __to__ with a comment: __comment__", "Livechat_transfer_failed_fallback": "The original department ( __from__ ) doesn't have online agents. Chat succesfully transferred to __to__", @@ -5499,4 +5503,4 @@ "onboarding.form.standaloneServerForm.manuallyIntegrate": "Need to manually integrate with external services", "Something_Went_Wrong": "Something went wrong", "Toolbox_room_actions": "Primary Room actions" -} \ No newline at end of file +} diff --git a/apps/meteor/server/models/raw/LivechatRooms.js b/apps/meteor/server/models/raw/LivechatRooms.js index 6006c15983000..112bbc57f11d4 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.js +++ b/apps/meteor/server/models/raw/LivechatRooms.js @@ -1196,4 +1196,43 @@ export class LivechatRoomsRaw extends BaseRaw { findOpen() { return this.find({ t: 'l', open: true }); } + + setAutoTransferOngoingById(roomId) { + const query = { + _id: roomId, + }; + const update = { + $set: { + autoTransferOngoing: true, + }, + }; + + return this.updateOne(query, update); + } + + unsetAutoTransferOngoingById(roomId) { + const query = { + _id: roomId, + }; + const update = { + $unset: { + autoTransferOngoing: 1, + }, + }; + + return this.updateOne(query, update); + } + + setAutoTransferredAtById(roomId) { + const query = { + _id: roomId, + }; + const update = { + $set: { + autoTransferredAt: new Date(), + }, + }; + + return this.updateOne(query, update); + } } diff --git a/apps/meteor/tests/e2e/omnichannel-auto-onhold-chat-closing.spec.ts b/apps/meteor/tests/e2e/omnichannel-auto-onhold-chat-closing.spec.ts new file mode 100644 index 0000000000000..202d186c1e216 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel-auto-onhold-chat-closing.spec.ts @@ -0,0 +1,104 @@ +import { faker } from '@faker-js/faker'; +import type { Browser, Page } from '@playwright/test'; + +import { test, expect } from './utils/test'; +import { OmnichannelLiveChat, HomeChannel } from './page-objects'; +import { IS_EE } from './config/constants'; + +const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { + const page = await browser.newPage({ storageState }); + const poHomeChannel = new HomeChannel(page); + await page.goto('/'); + await page.locator('.main-content').waitFor(); + + return { page, poHomeChannel }; +}; + +test.describe('omnichannel-auto-onhold-chat-closing', () => { + test.skip(!IS_EE, 'Enterprise Only'); + + let poLiveChat: OmnichannelLiveChat; + let newVisitor: { email: string; name: string }; + + let agent: { page: Page; poHomeChannel: HomeChannel }; + + test.beforeAll(async ({ api, browser }) => { + // make "user-1" an agent and manager + let statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status(); + expect(statusCode).toBe(200); + + // turn on auto selection routing + statusCode = (await api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' })).status(); + expect(statusCode).toBe(200); + + // make auto close on-hold chats timeout to be 5 seconds + statusCode = (await api.post('/settings/Livechat_auto_close_on_hold_chats_timeout', { value: 5 })).status(); + expect(statusCode).toBe(200); + + // allow agents to manually place chats on-hold + statusCode = (await api.post('/settings/Livechat_allow_manual_on_hold', { value: true })).status(); + expect(statusCode).toBe(200); + + agent = await createAuxContext(browser, 'user1-session.json'); + }); + + test.beforeEach(async ({ page }) => { + // make "user-1" online + await agent.poHomeChannel.sidenav.switchStatus('online'); + + // start a new chat for each test + newVisitor = { + name: faker.name.firstName(), + email: faker.internet.email(), + }; + poLiveChat = new OmnichannelLiveChat(page); + await page.goto('/livechat'); + await poLiveChat.btnOpenLiveChat('R').click(); + await poLiveChat.sendMessage(newVisitor, false); + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_user'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + }); + + test('expect on-hold chat to be closed automatically in 5 seconds', async () => { + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + await agent.poHomeChannel.content.sendMessage('this_is_a_test_message_from_agent'); + + expect(agent.poHomeChannel.content.btnOnHold).toBeVisible(); + await agent.poHomeChannel.content.btnOnHold.click(); + + // expect to see a confirmation modal + expect(agent.poHomeChannel.content.btnModalConfirm).toBeVisible(); + await agent.poHomeChannel.content.btnModalConfirm.click(); + + // expect to see a system message saying the chat was on-hold + expect(agent.poHomeChannel.content.lastSystemMessageBody).toHaveText(`Chat On Hold: The chat was manually placed On Hold by user1`); + expect(agent.poHomeChannel.content.inputMessage).not.toBeVisible(); + expect(agent.poHomeChannel.content.resumeOnHoldOmnichannelChatButton).toBeVisible(); + + // current url + const chatRoomUrl = agent.page.url(); + + // wait for the chat to be closed automatically for 5 seconds + await agent.page.waitForTimeout(7000); + + // expect to see a system message saying the chat was closed automatically in the closed chat room + await agent.page.goto(chatRoomUrl); + expect(await agent.poHomeChannel.content.lastSystemMessageBody.innerText()).toBe( + 'Conversation closed: Closed automatically because chat was On Hold for 5 seconds.', + ); + }); + + test.afterAll(async ({ api }) => { + // delete "user-1" from agents + let statusCode = (await api.delete('/livechat/users/agent/user1')).status(); + expect(statusCode).toBe(200); + + // reset auto close on-hold chats timeout + statusCode = (await api.post('/settings/Livechat_auto_close_on_hold_chats_timeout', { value: 3600 })).status(); + expect(statusCode).toBe(200); + + // reset setting which allows agents to manually place chats on-hold + statusCode = (await api.post('/settings/Livechat_allow_manual_on_hold', { value: false })).status(); + expect(statusCode).toBe(200); + }); +}); diff --git a/apps/meteor/tests/e2e/omnichannel-auto-transfer-unanswered-chat.spec.ts b/apps/meteor/tests/e2e/omnichannel-auto-transfer-unanswered-chat.spec.ts new file mode 100644 index 0000000000000..983407776bc62 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel-auto-transfer-unanswered-chat.spec.ts @@ -0,0 +1,87 @@ +import { faker } from '@faker-js/faker'; +import type { Browser, Page } from '@playwright/test'; + +import { test, expect } from './utils/test'; +import { OmnichannelLiveChat, HomeChannel } from './page-objects'; +import { IS_EE } from './config/constants'; + +const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { + const page = await browser.newPage({ storageState }); + const poHomeChannel = new HomeChannel(page); + await page.goto('/'); + await page.locator('.main-content').waitFor(); + + return { page, poHomeChannel }; +}; + +test.describe('omnichannel-auto-transfer-unanswered-chat', () => { + test.skip(!IS_EE, 'Enterprise Only'); + + let poLiveChat: OmnichannelLiveChat; + let newVisitor: { email: string; name: string }; + + let agent1: { page: Page; poHomeChannel: HomeChannel }; + let agent2: { page: Page; poHomeChannel: HomeChannel }; + + test.beforeAll(async ({ api, browser }) => { + // make "user-1" & "user-2" an agent + let statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status(); + expect(statusCode).toBe(200); + statusCode = (await api.post('/livechat/users/agent', { username: 'user2' })).status(); + expect(statusCode).toBe(200); + + // turn on auto selection routing + statusCode = (await api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' })).status(); + expect(statusCode).toBe(200); + + // make auto close on-hold chats timeout to be 5 seconds + statusCode = (await api.post('/settings/Livechat_auto_transfer_chat_timeout', { value: 5 })).status(); + expect(statusCode).toBe(200); + + agent1 = await createAuxContext(browser, 'user1-session.json'); + agent2 = await createAuxContext(browser, 'user2-session.json'); + }); + + test.beforeEach(async ({ page }) => { + // make "user-1" online + await agent1.poHomeChannel.sidenav.switchOmnichannelStatus('online'); + await agent2.poHomeChannel.sidenav.switchOmnichannelStatus('offline'); + + // start a new chat for each test + newVisitor = { + name: faker.name.firstName(), + email: faker.internet.email(), + }; + poLiveChat = new OmnichannelLiveChat(page); + await page.goto('/livechat'); + await poLiveChat.btnOpenLiveChat('R').click(); + await poLiveChat.sendMessage(newVisitor, false); + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_user'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + }); + + test('expect chat to be auto transferred to next agent within 5 seconds of no reply from first agent', async () => { + await agent1.poHomeChannel.sidenav.openChat(newVisitor.name); + + await agent2.poHomeChannel.sidenav.switchOmnichannelStatus('online'); + + // wait for the chat to be closed automatically for 5 seconds + await agent1.page.waitForTimeout(7000); + + await agent2.poHomeChannel.sidenav.openChat(newVisitor.name); + }); + + test.afterAll(async ({ api }) => { + // delete "user-1" from agents + let statusCode = (await api.delete('/livechat/users/agent/user1')).status(); + expect(statusCode).toBe(200); + + // delete "user-2" from agents + statusCode = (await api.delete('/livechat/users/agent/user2')).status(); + expect(statusCode).toBe(200); + + // reset auto close on-hold chats timeout + statusCode = (await api.post('/settings/Livechat_auto_transfer_chat_timeout', { value: 0 })).status(); + expect(statusCode).toBe(200); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 4072e9a6e6e06..05d4cf5dc0507 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -170,4 +170,16 @@ export class HomeContent { get takeOmnichannelChatButton(): Locator { return this.page.locator('role=button[name="Take it!"]'); } + + get lastSystemMessageBody(): Locator { + return this.page.locator('[data-qa-type="system-message-body"]').last(); + } + + get resumeOnHoldOmnichannelChatButton(): Locator { + return this.page.locator('button.rcx-button--primary >> text=Resume'); + } + + get btnOnHold(): Locator { + return this.page.locator('[data-qa-id="ToolBoxAction-pause-unfilled"]'); + } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index 8876f16a822f2..200582a5a756c 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -1,5 +1,7 @@ import type { Locator, Page } from '@playwright/test'; +import { expect } from '../../utils/test'; + export class HomeSidenav { private readonly page: Page; @@ -51,6 +53,31 @@ export class HomeSidenav { await this.page.locator(`[data-qa="sidebar-item-title"] >> text="${name}"`).first().click(); } + async switchOmnichannelStatus(status: 'offline' | 'online') { + // button has a id of "omnichannel-status-toggle" + const toggleButton = this.page.locator('#omnichannel-status-toggle'); + expect(toggleButton).toBeVisible(); + + enum StatusTitleMap { + offline = 'Turn on answer chats', + online = 'Turn off answer chats', + } + + const currentStatus: StatusTitleMap = (await toggleButton.getAttribute('data-tooltip')) as any; + if (status === 'offline') { + if (currentStatus === StatusTitleMap.online) { + await toggleButton.click(); + } + } else if (currentStatus === StatusTitleMap.offline) { + await toggleButton.click(); + } + + await this.page.waitForTimeout(500); + + const newStatus: StatusTitleMap = (await this.page.locator('#omnichannel-status-toggle').getAttribute('data-tooltip')) as any; + expect(newStatus).toBe(status === 'offline' ? StatusTitleMap.offline : StatusTitleMap.online); + } + // Note: this is a workaround for now since queued omnichannel chats are not searchable yet so we can't use openChat() :( async openQueuedOmnichannelChat(name: string): Promise { await this.page.locator('[data-qa="sidebar-item-title"]', { hasText: name }).first().click(); diff --git a/packages/livechat/src/components/helpers.js b/packages/livechat/src/components/helpers.js index 42a94fc162d39..0223813ae326f 100644 --- a/packages/livechat/src/components/helpers.js +++ b/packages/livechat/src/components/helpers.js @@ -144,7 +144,7 @@ export const normalizeTransferHistoryMessage = (transferData, sender, t) => { return; } - const { transferredBy, transferredTo, nextDepartment, scope } = transferData; + const { transferredBy, transferredTo, nextDepartment, scope, comment } = transferData; const from = transferredBy && (transferredBy.name || transferredBy.username); const transferTypes = { @@ -170,6 +170,8 @@ export const normalizeTransferHistoryMessage = (transferData, sender, t) => { } return t('from_returned_the_chat_to_the_queue', { from }); }, + autoTransferUnansweredChatsToAgent: () => t('the_chat_was_transferred_to_another_agent_due_to_unanswered', { duration: comment }), + autoTransferUnansweredChatsToQueue: () => t('the_chat_was_moved_back_to_queue_due_to_unanswered', { duration: comment }), }; return transferTypes[scope](); diff --git a/packages/livechat/src/i18n/en.json b/packages/livechat/src/i18n/en.json index 9155dd70dea91..791e2262351f8 100644 --- a/packages/livechat/src/i18n/en.json +++ b/packages/livechat/src/i18n/en.json @@ -78,7 +78,9 @@ "start_chat": "Start chat", "thanks_for_talking_with_us": "Thanks for talking with us", "the_chat_was_moved_back_to_queue": "The chat was moved back to queue", + "the_chat_was_moved_back_to_queue_due_to_unanswered": "The chat was moved back to queue since it was unanswered for {{duration}} seconds", "the_chat_was_transferred_to_another_agent": "The chat was transferred to another agent", + "the_chat_was_transferred_to_another_agent_due_to_unanswered": "The chat was transferred to another agent since it was unanswered for {{duration}} seconds", "the_controller_of_your_personal_data_is_company_na": "The controller of your personal data is [Company Name], with registered office at [Company Address]. To start the chat you agree that your personal data shall be processed and trasmitted in accordance with the General Data Protection Regulation (GDPR).", "transcript_success": "Transcript sent", "type_your_message_here": "Type your message here", diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 1e1a8d105e1e2..2d4b405dfee99 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -1,4 +1,5 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { FindCursor, UpdateResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -97,5 +98,11 @@ export interface ILivechatRoomsModel extends IBaseModel { setDepartmentByRoomId(roomId: any, departmentId: any): any; - findOpen(): any; + findOpen(): FindCursor; + + setAutoTransferOngoingById(roomId: string): Promise; + + unsetAutoTransferOngoingById(roomId: string): Promise; + + setAutoTransferredAtById(roomId: string): Promise; }