diff --git a/app/livechat/client/lib/chartHandler.js b/app/livechat/client/lib/chartHandler.js index 7af6388409b1d..f99571f0e66d0 100644 --- a/app/livechat/client/lib/chartHandler.js +++ b/app/livechat/client/lib/chartHandler.js @@ -194,9 +194,9 @@ export const drawDoughnutChart = async (chart, title, chartContext, dataLabels, data: dataPoints, // data points corresponding to data labels, x-axis points backgroundColor: [ '#2de0a5', - '#ffd21f', - '#f5455c', '#cbced1', + '#f5455c', + '#ffd21f', ], borderWidth: 0, }], diff --git a/app/livechat/imports/server/rest/rooms.js b/app/livechat/imports/server/rest/rooms.js index d2f4c6e20d63f..9680b8baffce1 100644 --- a/app/livechat/imports/server/rest/rooms.js +++ b/app/livechat/imports/server/rest/rooms.js @@ -21,12 +21,13 @@ API.v1.addRoute('livechat/rooms', { authRequired: true }, { get() { const { offset, count } = this.getPaginationItems(); const { sort, fields } = this.parseJsonQuery(); - const { agents, departmentId, open, tags, roomName } = this.requestParams(); + const { agents, departmentId, open, tags, roomName, onhold } = this.requestParams(); let { createdAt, customFields, closedAt } = this.requestParams(); check(agents, Match.Maybe([String])); check(roomName, Match.Maybe(String)); check(departmentId, Match.Maybe(String)); check(open, Match.Maybe(String)); + check(onhold, Match.Maybe(String)); check(tags, Match.Maybe([String])); const hasAdminAccess = hasPermission(this.userId, 'view-livechat-rooms'); @@ -51,6 +52,7 @@ API.v1.addRoute('livechat/rooms', { authRequired: true }, { closedAt, tags, customFields, + onhold, options: { offset, count, sort, fields }, }))); }, diff --git a/app/livechat/server/api/lib/rooms.js b/app/livechat/server/api/lib/rooms.js index 72d84803de153..957911737f0aa 100644 --- a/app/livechat/server/api/lib/rooms.js +++ b/app/livechat/server/api/lib/rooms.js @@ -9,6 +9,7 @@ export async function findRooms({ closedAt, tags, customFields, + onhold, options: { offset, count, @@ -25,6 +26,7 @@ export async function findRooms({ closedAt, tags, customFields, + onhold: ['t', 'true', '1'].includes(onhold), options: { sort: sort || { ts: -1 }, offset, diff --git a/app/livechat/server/lib/Analytics.js b/app/livechat/server/lib/Analytics.js index c4251fbd1bce1..b4aa71af346f6 100644 --- a/app/livechat/server/lib/Analytics.js +++ b/app/livechat/server/lib/Analytics.js @@ -2,6 +2,7 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import moment from 'moment'; import { LivechatRooms } from '../../../models'; +import { LivechatRooms as LivechatRoomsRaw } from '../../../models/server/raw'; import { secondsToHHMMSS } from '../../../utils/server'; import { getTimezone } from '../../../utils/server/lib/getTimezone'; import { Logger } from '../../../logger'; @@ -288,8 +289,8 @@ export const Analytics = { const totalMessagesInHour = new Map(); // total messages in hour 0, 1, ... 23 of weekday const days = to.diff(from, 'days') + 1; // total days - const summarize = (m) => ({ metrics, msgs }) => { - if (metrics && !metrics.chatDuration) { + const summarize = (m) => ({ metrics, msgs, onHold = false }) => { + if (metrics && !metrics.chatDuration && !onHold) { openConversations++; } totalMessages += msgs; @@ -337,13 +338,17 @@ export const Analytics = { to: utcBusiestHour >= 0 ? moment.utc().set({ hour: utcBusiestHour }).tz(timezone).format('hA') : '-', from: utcBusiestHour >= 0 ? moment.utc().set({ hour: utcBusiestHour }).subtract(1, 'hour').tz(timezone).format('hA') : '', }; + const onHoldConversations = Promise.await(LivechatRoomsRaw.getOnHoldConversationsBetweenDate(from, to, departmentId)); - const data = [{ + return [{ title: 'Total_conversations', value: totalConversations, }, { title: 'Open_conversations', value: openConversations, + }, { + title: 'On_Hold_conversations', + value: onHoldConversations, }, { title: 'Total_messages', value: totalMessages, @@ -357,8 +362,6 @@ export const Analytics = { title: 'Busiest_time', value: `${ busiestHour.from }${ busiestHour.to ? `- ${ busiestHour.to }` : '' }`, }]; - - return data; }, /** diff --git a/app/livechat/server/lib/analytics/dashboards.js b/app/livechat/server/lib/analytics/dashboards.js index 18bdca6300332..70dc1c7925ff8 100644 --- a/app/livechat/server/lib/analytics/dashboards.js +++ b/app/livechat/server/lib/analytics/dashboards.js @@ -25,6 +25,7 @@ const findAllChatsStatusAsync = async ({ open: await LivechatRooms.countAllOpenChatsBetweenDate({ start, end, departmentId }), closed: await LivechatRooms.countAllClosedChatsBetweenDate({ start, end, departmentId }), queued: await LivechatRooms.countAllQueuedChatsBetweenDate({ start, end, departmentId }), + onhold: await LivechatRooms.getOnHoldConversationsBetweenDate(start, end, departmentId), }; }; @@ -193,7 +194,7 @@ const getConversationsMetricsAsync = async ({ utcOffset: user.utcOffset, language: user.language || settings.get('Language') || 'en', }); - const metrics = ['Total_conversations', 'Open_conversations', 'Total_messages']; + const metrics = ['Total_conversations', 'Open_conversations', 'On_Hold_conversations', 'Total_messages']; const visitorsCount = await LivechatVisitors.getVisitorsBetweenDate({ start, end, department: departmentId }).count(); return { totalizers: [ @@ -213,13 +214,20 @@ const findAllChatMetricsByAgentAsync = async ({ } const open = await LivechatRooms.countAllOpenChatsByAgentBetweenDate({ start, end, departmentId }); const closed = await LivechatRooms.countAllClosedChatsByAgentBetweenDate({ start, end, departmentId }); + const onhold = await LivechatRooms.countAllOnHoldChatsByAgentBetweenDate({ start, end, departmentId }); const result = {}; (open || []).forEach((agent) => { - result[agent._id] = { open: agent.chats, closed: 0 }; + result[agent._id] = { open: agent.chats, closed: 0, onhold: 0 }; }); (closed || []).forEach((agent) => { result[agent._id] = { open: result[agent._id] ? result[agent._id].open : 0, closed: agent.chats }; }); + (onhold || []).forEach((agent) => { + result[agent._id] = { + ...result[agent._id], + onhold: agent.chats, + }; + }); return result; }; diff --git a/app/models/server/models/LivechatRooms.js b/app/models/server/models/LivechatRooms.js index 1d71be35aad0b..dc73c211a2c13 100644 --- a/app/models/server/models/LivechatRooms.js +++ b/app/models/server/models/LivechatRooms.js @@ -563,6 +563,7 @@ export class LivechatRooms extends Base { open: '$open', servedBy: '$servedBy', metrics: '$metrics', + onHold: '$onHold', }, messagesCount: { $sum: 1, @@ -578,6 +579,7 @@ export class LivechatRooms extends Base { servedBy: '$_id.servedBy', metrics: '$_id.metrics', msgs: '$messagesCount', + onHold: '$_id.onHold', }, }, ]); diff --git a/app/models/server/raw/LivechatRooms.js b/app/models/server/raw/LivechatRooms.js index 0c07e12add2be..c73a15e3ad5c2 100644 --- a/app/models/server/raw/LivechatRooms.js +++ b/app/models/server/raw/LivechatRooms.js @@ -479,6 +479,16 @@ export class LivechatRoomsRaw extends BaseRaw { 'metrics.chatDuration': { $exists: false, }, + $or: [{ + onHold: { + $exists: false, + }, + }, { + onHold: { + $exists: true, + $eq: false, + }, + }], servedBy: { $exists: true }, ts: { $gte: new Date(start), $lte: new Date(end) }, }; @@ -494,7 +504,6 @@ export class LivechatRoomsRaw extends BaseRaw { 'metrics.chatDuration': { $exists: true, }, - servedBy: { $exists: true }, ts: { $gte: new Date(start), $lte: new Date(end) }, }; if (departmentId && departmentId !== 'undefined') { @@ -507,6 +516,7 @@ export class LivechatRoomsRaw extends BaseRaw { const query = { t: 'l', servedBy: { $exists: false }, + open: true, ts: { $gte: new Date(start), $lte: new Date(end) }, }; if (departmentId && departmentId !== 'undefined') { @@ -521,6 +531,41 @@ export class LivechatRoomsRaw extends BaseRaw { t: 'l', 'servedBy.username': { $exists: true }, open: true, + $or: [{ + onHold: { + $exists: false, + }, + }, { + onHold: { + $exists: true, + $eq: false, + }, + }], + ts: { $gte: new Date(start), $lte: new Date(end) }, + }, + }; + const group = { + $group: { + _id: '$servedBy.username', + chats: { $sum: 1 }, + }, + }; + if (departmentId && departmentId !== 'undefined') { + match.$match.departmentId = departmentId; + } + return this.col.aggregate([match, group]).toArray(); + } + + countAllOnHoldChatsByAgentBetweenDate({ start, end, departmentId }) { + const match = { + $match: { + t: 'l', + 'servedBy.username': { $exists: true }, + open: true, + onHold: { + $exists: true, + $eq: true, + }, ts: { $gte: new Date(start), $lte: new Date(end) }, }, }; @@ -896,7 +941,7 @@ export class LivechatRoomsRaw extends BaseRaw { return this.col.aggregate(params); } - findRoomsWithCriteria({ agents, roomName, departmentId, open, served, createdAt, closedAt, tags, customFields, visitorId, roomIds, options = {} }) { + findRoomsWithCriteria({ agents, roomName, departmentId, open, served, createdAt, closedAt, tags, customFields, visitorId, roomIds, onhold, options = {} }) { const query = { t: 'l', }; @@ -911,6 +956,7 @@ export class LivechatRoomsRaw extends BaseRaw { } if (open !== undefined) { query.open = { $exists: open }; + query.onHold = { $ne: true }; } if (served !== undefined) { query.servedBy = { $exists: served }; @@ -947,9 +993,35 @@ export class LivechatRoomsRaw extends BaseRaw { query._id = { $in: roomIds }; } + if (onhold) { + query.onHold = { + $exists: true, + $eq: onhold, + }; + } + return this.find(query, { sort: options.sort || { name: 1 }, skip: options.offset, limit: options.count }); } + getOnHoldConversationsBetweenDate(from, to, departmentId) { + const query = { + onHold: { + $exists: true, + $eq: true, + }, + ts: { + $gte: new Date(from), // ISO Date, ts >= date.gte + $lt: new Date(to), // ISODate, ts < date.lt + }, + }; + + if (departmentId && departmentId !== 'undefined') { + query.departmentId = departmentId; + } + + return this.find(query).count(); + } + findAllServiceTimeByAgent({ start, end, onlyCount = false, options = {} }) { const match = { $match: { diff --git a/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx b/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx index 7e766e4c00f4f..4bf9e123be84b 100644 --- a/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx +++ b/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx @@ -47,6 +47,7 @@ const useQuery: useQueryType = ( departmentId?: string; tags?: string[]; customFields?: string; + onhold?: boolean; } = { ...(guest && { roomName: guest }), sort: JSON.stringify({ @@ -71,8 +72,10 @@ const useQuery: useQueryType = ( }), }); } + if (status !== 'all') { - query.open = status === 'opened'; + query.open = status === 'opened' || status === 'onhold'; + query.onhold = status === 'onhold'; } if (servedBy && servedBy !== 'all') { query.agents = [servedBy]; @@ -215,25 +218,32 @@ const CurrentChatsRoute: FC = () => { ); const renderRow = useCallback( - ({ _id, fname, servedBy, ts, lm, department, open }) => ( -