Skip to content

Commit

Permalink
[NEW] Show on-hold metrics on analytics pages and current chats (#23498)
Browse files Browse the repository at this point in the history
* Show on hold metrics on analytics pages

* Add onhold chats to graphs

* make colors match cause ocd

* change way of calculating open rooms to account for onhold chats

* remove onhold chats from open chats

* fix colors

* fix summarize to remove onhold conversations
  • Loading branch information
KevLehman authored Nov 1, 2021
1 parent cbd2a0b commit 9c42d40
Show file tree
Hide file tree
Showing 14 changed files with 146 additions and 38 deletions.
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';
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

0 comments on commit 9c42d40

Please sign in to comment.