Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Show on-hold metrics on analytics pages and current chats #23498

Merged
merged 9 commits into from
Nov 1, 2021
4 changes: 2 additions & 2 deletions app/livechat/client/lib/chartHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}],
Expand Down
4 changes: 3 additions & 1 deletion app/livechat/imports/server/rest/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -51,6 +52,7 @@ API.v1.addRoute('livechat/rooms', { authRequired: true }, {
closedAt,
tags,
customFields,
onhold,
options: { offset, count, sort, fields },
})));
},
Expand Down
2 changes: 2 additions & 0 deletions app/livechat/server/api/lib/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export async function findRooms({
closedAt,
tags,
customFields,
onhold,
options: {
offset,
count,
Expand All @@ -25,6 +26,7 @@ export async function findRooms({
closedAt,
tags,
customFields,
onhold: ['t', 'true', '1'].includes(onhold),
options: {
sort: sort || { ts: -1 },
offset,
Expand Down
13 changes: 8 additions & 5 deletions app/livechat/server/lib/Analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -357,8 +362,6 @@ export const Analytics = {
title: 'Busiest_time',
value: `${ busiestHour.from }${ busiestHour.to ? `- ${ busiestHour.to }` : '' }`,
}];

return data;
},

/**
Expand Down
12 changes: 10 additions & 2 deletions app/livechat/server/lib/analytics/dashboards.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
};

Expand Down Expand Up @@ -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: [
Expand All @@ -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;
};

Expand Down
2 changes: 2 additions & 0 deletions app/models/server/models/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ export class LivechatRooms extends Base {
open: '$open',
servedBy: '$servedBy',
metrics: '$metrics',
onHold: '$onHold',
},
messagesCount: {
$sum: 1,
Expand All @@ -578,6 +579,7 @@ export class LivechatRooms extends Base {
servedBy: '$_id.servedBy',
metrics: '$_id.metrics',
msgs: '$messagesCount',
onHold: '$_id.onHold',
},
},
]);
Expand Down
76 changes: 74 additions & 2 deletions app/models/server/raw/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) },
};
Expand All @@ -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') {
Expand All @@ -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') {
Expand All @@ -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) },
},
};
Expand Down Expand Up @@ -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',
};
Expand All @@ -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 };
Expand Down Expand Up @@ -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: {
Expand Down
50 changes: 30 additions & 20 deletions client/views/omnichannel/currentChats/CurrentChatsRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const useQuery: useQueryType = (
departmentId?: string;
tags?: string[];
customFields?: string;
onhold?: boolean;
} = {
...(guest && { roomName: guest }),
sort: JSON.stringify({
Expand All @@ -71,8 +72,10 @@ const useQuery: useQueryType = (
}),
});
}

if (status !== 'all') {
query.open = status === 'opened';
query.open = status === 'opened' || status === 'onhold';
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
query.onhold = status === 'onhold';
}
if (servedBy && servedBy !== 'all') {
query.agents = [servedBy];
Expand Down Expand Up @@ -215,25 +218,32 @@ const CurrentChatsRoute: FC = () => {
);

const renderRow = useCallback(
({ _id, fname, servedBy, ts, lm, department, open }) => (
<Table.Row
key={_id}
tabIndex={0}
role='link'
onClick={(): void => onRowClick(_id)}
action
qa-user-id={_id}
>
<Table.Cell withTruncatedText>{fname}</Table.Cell>
<Table.Cell withTruncatedText>{department ? department.name : ''}</Table.Cell>
<Table.Cell withTruncatedText>{servedBy && servedBy.username}</Table.Cell>
<Table.Cell withTruncatedText>{moment(ts).format('L LTS')}</Table.Cell>
<Table.Cell withTruncatedText>{moment(lm).format('L LTS')}</Table.Cell>
<Table.Cell withTruncatedText>{open ? t('Open') : t('Closed')}</Table.Cell>
{canRemoveClosedChats && !open && <RemoveChatButton _id={_id} reload={reload} />}
</Table.Row>
),
[onRowClick, reload, t, canRemoveClosedChats],
({ _id, fname, servedBy, ts, lm, department, open, onHold }) => {
const getStatusText = (open: boolean, onHold: boolean): string => {
if (!open) return t('Closed');
return onHold ? t('On_Hold_Chats') : t('Open');
};

return (
<Table.Row
key={_id}
tabIndex={0}
role='link'
onClick={(): void => onRowClick(_id)}
action
qa-user-id={_id}
>
<Table.Cell withTruncatedText>{fname}</Table.Cell>
<Table.Cell withTruncatedText>{department ? department.name : ''}</Table.Cell>
<Table.Cell withTruncatedText>{servedBy && servedBy.username}</Table.Cell>
<Table.Cell withTruncatedText>{moment(ts).format('L LTS')}</Table.Cell>
<Table.Cell withTruncatedText>{moment(lm).format('L LTS')}</Table.Cell>
<Table.Cell withTruncatedText>{getStatusText(open, onHold)}</Table.Cell>
{canRemoveClosedChats && !open && <RemoveChatButton _id={_id} reload={reload} />}
</Table.Row>
);
},
[onRowClick, reload, canRemoveClosedChats, t],
);

if (!canViewCurrentChats) {
Expand Down
3 changes: 2 additions & 1 deletion client/views/omnichannel/currentChats/FilterByText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, ...props }) => {
['all', t('All')],
['closed', t('Closed')],
['opened', t('Open')],
['onhold', t('On_Hold_Chats')],
];
const customFieldsOptions: [string, string][] = useMemo(
() =>
Expand Down Expand Up @@ -110,7 +111,7 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, ...props }) => {
reload && reload();
dispatchToastMessage({ type: 'success', message: t('Chat_removed') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
dispatchToastMessage({ type: 'error', message: (error as Error).message });
}
setModal(null);
};
Expand Down
Loading