[IMPROVE] Livechat realtime dashboard (#15792)

* LIvechat conversations totalizers REST endpoint

* Add conversations metrics

* Change the query to ignore livechat system messages

* Add total of livechat abandoned chats

* Fix projection query

* Livechat Productivty metrics using REST

* Invert query logic on livechat analytics functions

* Convert chats chart data to REST

* Remove publication chart updates

* Change livechat agents metrics chart data to REST

* Change livechat agents status metrics chart data by REST

* Replace livechat:visitors publication by REST

* Change livechat chats by department chat data by REST

* Change livechat timings chart data by REST

* remove unnecessary analytics functions

* Fix time converter

* Add more productivity metrics to the livechat dashboard

* Re add livechat departments metric

* Fix test

* Split livechat agents metrics

* Improve metrics

* Move some metrics to another endpoints

* improve layout

* Fix wrong query filters

* fix percentage of abandoned rooms query

* fix livechat analytics query

* Fix dashboard style.
pull/16006/head^2
Marcos Spessatto Defendi 6 years ago committed by Renato Becker
parent f2eed8af99
commit c439d9e0d4
  1. 3
      app/livechat/client/collections/AgentUsers.js
  2. 3
      app/livechat/client/collections/LivechatDepartment.js
  3. 3
      app/livechat/client/collections/LivechatMonitoring.js
  4. 199
      app/livechat/client/lib/dataHandler.js
  5. 29
      app/livechat/client/stylesheets/livechat.less
  6. 105
      app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.html
  7. 322
      app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.js
  8. 219
      app/livechat/imports/server/rest/dashboards.js
  9. 1
      app/livechat/server/api.js
  10. 11
      app/livechat/server/hooks/markRoomResponded.js
  11. 33
      app/livechat/server/lib/Analytics.js
  12. 270
      app/livechat/server/lib/analytics/dashboards.js
  13. 60
      app/livechat/server/lib/analytics/departments.js
  14. 1
      app/livechat/server/publications/livechatAgents.js
  15. 1
      app/livechat/server/publications/livechatDepartments.js
  16. 1
      app/livechat/server/publications/livechatMonitoring.js
  17. 1
      app/livechat/server/publications/livechatVisitors.js
  18. 2
      app/livechat/server/publications/visitorInfo.js
  19. 75
      app/models/server/models/LivechatRooms.js
  20. 67
      app/models/server/raw/LivechatAgentActivity.js
  21. 483
      app/models/server/raw/LivechatDepartment.js
  22. 1012
      app/models/server/raw/LivechatRooms.js
  23. 9
      app/models/server/raw/LivechatVisitors.js
  24. 84
      app/models/server/raw/Users.js
  25. 3
      app/models/server/raw/index.js
  26. 1
      app/utils/client/index.js
  27. 24
      app/utils/lib/timeConverter.js
  28. 1
      app/utils/server/index.js
  29. 15
      packages/rocketchat-i18n/i18n/en.i18n.json
  30. 7
      packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  31. 314
      tests/end-to-end/api/livechat/dashboards.js

@ -1,3 +0,0 @@
import { Mongo } from 'meteor/mongo';
export const AgentUsers = new Mongo.Collection('agentUsers');

@ -1,3 +0,0 @@
import { Mongo } from 'meteor/mongo';
export const LivechatDepartment = new Mongo.Collection('rocketchat_livechat_department');

@ -1,3 +0,0 @@
import { Mongo } from 'meteor/mongo';
export const LivechatMonitoring = new Mongo.Collection('livechatMonitoring');

@ -1,199 +0,0 @@
/**
*
* @param {Object} dbCursor cursor to minimongo result
* @return {Object}
*/
const calculateResponseTimings = (dbCursor) => {
let art = 0;
let longest = 0;
let count = 0;
dbCursor.forEach(({ metrics }) => {
if (metrics && metrics.response && metrics.response.avg) {
art += metrics.response.avg;
count++;
}
if (metrics && metrics.response && metrics.response.ft) {
longest = Math.max(longest, metrics.response.ft);
}
});
const avgArt = count ? art / count : 0;
return {
avg: Math.round(avgArt * 100) / 100,
longest,
};
};
/**
*
* @param {Object} dbCursor cursor to minimongo result
* @return {Object}
*/
const calculateReactionTimings = (dbCursor) => {
let arnt = 0;
let longest = 0;
let count = 0;
dbCursor.forEach(({ metrics }) => {
if (metrics && metrics.reaction && metrics.reaction.ft) {
arnt += metrics.reaction.ft;
longest = Math.max(longest, metrics.reaction.ft);
count++;
}
});
const avgArnt = count ? arnt / count : 0;
return {
avg: Math.round(avgArnt * 100) / 100,
longest,
};
};
/**
*
* @param {Object} dbCursor cursor to minimongo result
* @return {Object}
*/
const calculateDurationData = (dbCursor) => {
let total = 0;
let longest = 0;
let count = 0;
dbCursor.forEach(({ metrics }) => {
if (metrics && metrics.chatDuration) {
total += metrics.chatDuration;
longest = Math.max(longest, metrics.chatDuration);
count++;
}
});
const avgCD = count ? total / count : 0;
return {
avg: Math.round(avgCD * 100) / 100,
longest,
};
};
/**
* return readable time format from seconds
* @param {Double} sec seconds
* @return {String} Readable string format
*/
const secondsToHHMMSS = (sec) => {
sec = parseFloat(sec);
let hours = Math.floor(sec / 3600);
let minutes = Math.floor((sec - (hours * 3600)) / 60);
let seconds = Math.round(sec - (hours * 3600) - (minutes * 60));
if (hours < 10) { hours = `0${ hours }`; }
if (minutes < 10) { minutes = `0${ minutes }`; }
if (seconds < 10) { seconds = `0${ seconds }`; }
if (hours > 0) {
return `${ hours }:${ minutes }:${ seconds }`;
}
if (minutes > 0) {
return `${ minutes }:${ seconds }`;
}
return sec;
};
/**
*
* @param {Object} dbCursor cursor to minimongo result
* @return {Object}
*/
export const getTimingsChartData = (dbCursor) => {
const data = {};
data.reaction = calculateReactionTimings(dbCursor);
data.response = calculateResponseTimings(dbCursor);
data.chatDuration = calculateDurationData(dbCursor);
return data;
};
/**
*
* @param {Object} dbCursor cursor to minimongo result
* @return {Object}
*/
export const getAgentStatusData = (dbCursor) => {
const data = {
offline: 0,
available: 0,
away: 0,
busy: 0,
};
dbCursor.forEach((doc) => {
if (doc.status === 'offline' || doc.status === 'away' || doc.status === 'busy') {
data[doc.status]++;
} else if (doc.status === 'online' && doc.statusLivechat === 'available') {
data[doc.statusLivechat]++;
} else {
data.offline++;
}
});
return data;
};
/**
*
* @param {Object} dbCursor cursor to minimongo result
* @return {Array(Object)}
*/
export const getConversationsOverviewData = (dbCursor) => {
let total = 0;
let totalMessages = 0;
dbCursor.forEach(function(doc) {
total++;
if (doc.msgs) {
totalMessages += doc.msgs;
}
});
return [{
title: 'Total_conversations',
value: total || 0,
}, {
title: 'Total_messages',
value: totalMessages || 0,
}];
};
/**
*
* @param {Object} dbCursor cursor to minimongo result
* @return {Array(Object)}
*/
export const getTimingsOverviewData = (dbCursor) => {
let total = 0;
let totalResponseTime = 0;
let totalReactionTime = 0;
dbCursor.forEach(function(doc) {
total++;
if (doc.metrics && doc.metrics.response) {
totalResponseTime += doc.metrics.response.avg;
totalReactionTime += doc.metrics.reaction.ft;
}
});
return [{
title: 'Avg_reaction_time',
value: total ? secondsToHHMMSS((totalReactionTime / total).toFixed(2)) : '-',
}, {
title: 'Avg_response_time',
value: total ? secondsToHHMMSS((totalResponseTime / total).toFixed(2)) : '-',
}];
};

@ -282,7 +282,7 @@
.lc-analytics-overview {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-top: 10px;
margin-bottom: 10px;
justify-content: center;
@ -290,8 +290,8 @@
.lc-analytics-ov-col {
min-height: 20px;
flex: 33.33333%; //1;
max-width: 33.33333%;
flex: 25%; //1;
max-width: 25%;
&.-full-width {
max-width: 100%;
@ -450,6 +450,8 @@
.lc-monitoring-flex {
display: flex;
flex-wrap: wrap;
margin-bottom: 2px !important;
margin-top: 2px !important;
// flex-direction: row;
// margin-top: 10px;
// margin-bottom: 10px;
@ -467,12 +469,29 @@
flex: 66.66666%;
max-width: 66.66666%;
padding: 2px 2px;
& .lc-analytics-ov-col {
min-height: 20px;
flex: 33.3%; //1;
max-width: 33.3%;
&.-full-width {
max-width: 100%;
}
&:not(:last-child) {
border-right: 1px solid #E9E9E9;
}
}
& .lc-analytics-overview {
margin: auto;
}
}
.lc-monitoring-line-chart-full {
flex: 66.66666%;
width: 66.66666%;
max-width: 100%;
width: 100%;
padding: 2px 2px;
}

@ -1,23 +1,36 @@
<template name="livechatRealTimeMonitoring">
{{#requiresPermission 'view-livechat-real-time-monitoring'}}
<div class="lc-analytics-overview">
{{#each conversationsOverview}}
<div class="lc-analytics-ov-col">
<div class="lc-analytics-ov-case">
<span class="title">{{_ title}}</span>
<span class="value">{{value}}</span>
</div>
<form class="form-inline">
<div class="form-group rc-select lc-analytics-header">
<select id="lc-analytics-options" class="rc-select__element js-interval">
<option class="rc-select__option" value="5">5 {{_ "seconds"}}</option>
<option class="rc-select__option" value="10">10 {{_ "seconds"}}</option>
<option class="rc-select__option" value="30">30 {{_ "seconds"}}</option>
<option class="rc-select__option" value="60">1 {{_ "minute"}}</option>
</select>
<i class="icon-angle-down"></i>
</div>
{{/each}}
{{#with totalVisitors}}
<div class="lc-analytics-ov-col">
<div class="lc-analytics-ov-case">
<span class="title">{{_ title}}</span>
<span class="value">{{value}}</span>
</form>
{{#if isLoading}}
{{> loading }}
{{else}}
<div class="lc-monitoring-flex">
<div class="section lc-monitoring-line-chart-full">
<div class="section-content border-component-color">
<div class="lc-analytics-overview">
{{#each conversationsOverview}}
<div class="lc-analytics-ov-col">
<div class="lc-analytics-ov-case">
<span class="title">{{_ title}}</span>
<span class="value">{{value}}</span>
</div>
</div>
{{/each}}
</div>
</div>
</div>
{{/with}}
</div>
{{/if}}
<div class="lc-monitoring-flex">
<div class="section lc-monitoring-doughnut-chart">
<div class="section-content border-component-color">
@ -26,13 +39,35 @@
</div>
</div>
</div>
<div class="section lc-monitoring-line-chart">
<div class="section lc-monitoring-line-chart-full">
<div class="section-content border-component-color">
<div class="lc-monitoring-chart-container">
<canvas id="lc-chats-per-agent-chart"></canvas>
</div>
</div>
</div>
</div>
<div class="lc-monitoring-flex">
<div class="section lc-monitoring-line-chart-full">
<div class="section-content border-component-color">
{{#if isLoading}}
{{> loading }}
{{else}}
<div class="lc-analytics-overview">
{{#each chatsOverview}}
<div class="lc-analytics-ov-col">
<div class="lc-analytics-ov-case">
<span class="title">{{_ title}}</span>
<span class="value">{{value}}</span>
</div>
</div>
{{/each}}
</div>
{{/if}}
</div>
</div>
</div>
<div class="lc-monitoring-flex">
<div class="section lc-monitoring-doughnut-chart">
<div class="section-content border-component-color">
<div class="lc-monitoring-chart-container">
@ -40,13 +75,35 @@
</div>
</div>
</div>
<div class="lc-chats-per-dept-chart-section section lc-monitoring-line-chart">
<div class="section lc-monitoring-line-chart-full">
<div class="section-content border-component-color">
<div class="lc-monitoring-chart-container">
<canvas id="lc-chats-per-dept-chart"></canvas>
</div>
</div>
</div>
</div>
<div class="lc-monitoring-flex">
<div class="section lc-monitoring-line-chart-full">
<div class="section-content border-component-color">
{{#if isLoading}}
{{> loading }}
{{else}}
<div class="lc-analytics-overview">
{{#each agentsOverview}}
<div class="lc-analytics-ov-col">
<div class="lc-analytics-ov-case">
<span class="title">{{_ title}}</span>
<span class="value">{{value}}</span>
</div>
</div>
{{/each}}
</div>
{{/if}}
</div>
</div>
</div>
<div class="lc-monitoring-flex">
<div class="section lc-monitoring-line-chart-full">
<div class="section-content border-component-color">
<div class="lc-monitoring-chart-container">
@ -54,8 +111,13 @@
</div>
</div>
</div>
</div>
<div class="lc-monitoring-flex">
<div class="section lc-monitoring-line-chart-full">
<div class="section-content border-component-color">
{{#if isLoading}}
{{> loading }}
{{else}}
<div class="lc-analytics-overview">
{{#each timingOverview}}
<div class="lc-analytics-ov-col">
@ -66,9 +128,14 @@
</div>
{{/each}}
</div>
<div class="lc-monitoring-chart-container">
<canvas id="lc-reaction-response-times-chart"></canvas>
</div>
{{/if}}
</div>
</div>
</div>
<div class="lc-monitoring-flex">
<div class="section lc-monitoring-line-chart-full">
<div class="lc-monitoring-chart-container">
<canvas id="lc-reaction-response-times-chart"></canvas>
</div>
</div>
</div>

@ -1,21 +1,14 @@
import { Mongo } from 'meteor/mongo';
import { Template } from 'meteor/templating';
import moment from 'moment';
import { ReactiveVar } from 'meteor/reactive-var';
import { drawLineChart, drawDoughnutChart, updateChart } from '../../../lib/chartHandler';
import { getTimingsChartData, getAgentStatusData, getConversationsOverviewData, getTimingsOverviewData } from '../../../lib/dataHandler';
import { LivechatMonitoring } from '../../../collections/LivechatMonitoring';
import { AgentUsers } from '../../../collections/AgentUsers';
import { LivechatDepartment } from '../../../collections/LivechatDepartment';
import { APIClient } from '../../../../../utils/client';
import './livechatRealTimeMonitoring.html';
let chartContexts = {}; // stores context of current chart, used to clean when redrawing
const chartContexts = {}; // stores context of current chart, used to clean when redrawing
let templateInstance;
const LivechatVisitors = new Mongo.Collection('livechatVisitors');
const initChart = {
'lc-chats-chart'() {
return drawDoughnutChart(
@ -90,127 +83,136 @@ const initChart = {
},
};
const initAllCharts = () => {
chartContexts['lc-chats-chart'] = initChart['lc-chats-chart']();
chartContexts['lc-agents-chart'] = initChart['lc-agents-chart']();
chartContexts['lc-chats-per-agent-chart'] = initChart['lc-chats-per-agent-chart']();
chartContexts['lc-chats-per-dept-chart'] = initChart['lc-chats-per-dept-chart']();
chartContexts['lc-reaction-response-times-chart'] = initChart['lc-reaction-response-times-chart']();
chartContexts['lc-chat-duration-chart'] = initChart['lc-chat-duration-chart']();
const initAllCharts = async () => {
chartContexts['lc-chats-chart'] = await initChart['lc-chats-chart']();
chartContexts['lc-agents-chart'] = await initChart['lc-agents-chart']();
chartContexts['lc-chats-per-agent-chart'] = await initChart['lc-chats-per-agent-chart']();
chartContexts['lc-chats-per-dept-chart'] = await initChart['lc-chats-per-dept-chart']();
chartContexts['lc-reaction-response-times-chart'] = await initChart['lc-reaction-response-times-chart']();
chartContexts['lc-chat-duration-chart'] = await initChart['lc-chat-duration-chart']();
};
const updateChartData = (chartId, label, data) => {
// update chart
const updateChartData = async (chartId, label, data) => {
if (!chartContexts[chartId]) {
chartContexts[chartId] = initChart[chartId]();
chartContexts[chartId] = await initChart[chartId]();
}
updateChart(chartContexts[chartId], label, data);
await updateChart(chartContexts[chartId], label, data);
};
const metricsUpdated = (ts) => {
const hour = moment(ts).format('H');
const label = `${ moment(hour, ['H']).format('hA') }-${ moment((parseInt(hour) + 1) % 24, ['H']).format('hA') }`;
let timer;
const query = {
ts: {
$gte: new Date(moment(ts).startOf('hour')),
$lt: new Date(moment(ts).add(1, 'hours').startOf('hour')),
},
const getDaterange = () => {
const today = moment(new Date());
return {
start: `${ moment(new Date(today.year(), today.month(), today.date(), 0, 0, 0)).utc().format('YYYY-MM-DDTHH:mm:ss') }Z`,
end: `${ moment(new Date(today.year(), today.month(), today.date(), 23, 59, 59)).utc().format('YYYY-MM-DDTHH:mm:ss') }Z`,
};
};
const data = getTimingsChartData(LivechatMonitoring.find(query));
const loadConversationOverview = async ({ start, end }) => {
const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/conversation-totalizers?start=${ start }&end=${ end }`);
return totalizers;
};
updateChartData('lc-reaction-response-times-chart', label, [data.reaction.avg, data.reaction.longest, data.response.avg, data.response.longest]);
updateChartData('lc-chat-duration-chart', label, [data.chatDuration.avg, data.chatDuration.longest]);
const updateConversationOverview = async (totalizers) => {
if (totalizers && Array.isArray(totalizers)) {
templateInstance.conversationsOverview.set(totalizers);
}
};
const updateDepartmentsChart = (departmentId) => {
if (departmentId) {
// update for dept
const label = LivechatDepartment.findOne({ _id: departmentId }).name;
const data = {
open: LivechatMonitoring.find({ departmentId, open: true }).count(),
closed: LivechatMonitoring.find({ departmentId, open: { $exists: false } }).count(),
};
updateChartData('lc-chats-per-dept-chart', label, [data.open, data.closed]);
} else {
// update for all
LivechatDepartment.find({ enabled: true }).forEach(function(dept) {
updateDepartmentsChart(dept._id);
});
const loadAgentsOverview = async ({ start, end }) => {
const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/agents-productivity-totalizers?start=${ start }&end=${ end }`);
return totalizers;
};
const updateAgentsOverview = async (totalizers) => {
if (totalizers && Array.isArray(totalizers)) {
templateInstance.agentsOverview.set(totalizers);
}
};
const loadChatsOverview = async ({ start, end }) => {
const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/chats-totalizers?start=${ start }&end=${ end }`);
return totalizers;
};
const updateAgentsChart = (agent) => {
if (agent) {
// update for the agent
const data = {
open: LivechatMonitoring.find({ 'servedBy.username': agent, open: true }).count(),
closed: LivechatMonitoring.find({ 'servedBy.username': agent, open: { $exists: false } }).count(),
};
updateChartData('lc-chats-per-agent-chart', agent, [data.open, data.closed]);
} else {
// update for all agents
AgentUsers.find().forEach(function(agent) {
if (agent.username) {
updateAgentsChart(agent.username);
}
});
const updateChatsOverview = async (totalizers) => {
if (totalizers && Array.isArray(totalizers)) {
templateInstance.chatsOverview.set(totalizers);
}
};
const updateAgentStatusChart = () => {
const statusData = getAgentStatusData(AgentUsers.find());
const loadProductivityOverview = async ({ start, end }) => {
const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/productivity-totalizers?start=${ start }&end=${ end }`);
return totalizers;
};
updateChartData('lc-agents-chart', 'Offline', [statusData.offline]);
updateChartData('lc-agents-chart', 'Available', [statusData.available]);
updateChartData('lc-agents-chart', 'Away', [statusData.away]);
updateChartData('lc-agents-chart', 'Busy', [statusData.busy]);
const updateProductivityOverview = async (totalizers) => {
if (totalizers && Array.isArray(totalizers)) {
templateInstance.timingOverview.set(totalizers);
}
};
const updateChatsChart = () => {
const chats = {
open: LivechatMonitoring.find({ 'metrics.chatDuration': { $exists: false }, servedBy: { $exists: true } }).count(),
closed: LivechatMonitoring.find({ 'metrics.chatDuration': { $exists: true }, servedBy: { $exists: true } }).count(),
queue: LivechatMonitoring.find({ servedBy: { $exists: false } }).count(),
};
const loadChatsChartData = ({ start, end }) => APIClient.v1.get(`livechat/analytics/dashboards/charts/chats?start=${ start }&end=${ end }`);
updateChartData('lc-chats-chart', 'Open', [chats.open]);
updateChartData('lc-chats-chart', 'Closed', [chats.closed]);
updateChartData('lc-chats-chart', 'Queue', [chats.queue]);
const updateChatsChart = async ({ open, closed, queued }) => {
await updateChartData('lc-chats-chart', 'Open', [open]);
await updateChartData('lc-chats-chart', 'Closed', [closed]);
await updateChartData('lc-chats-chart', 'Queue', [queued]);
};
const updateConversationsOverview = () => {
const data = getConversationsOverviewData(LivechatMonitoring.find());
const loadChatsPerAgentChartData = async ({ start, end }) => {
const result = await APIClient.v1.get(`livechat/analytics/dashboards/charts/chats-per-agent?start=${ start }&end=${ end }`);
delete result.success;
return result;
};
templateInstance.conversationsOverview.set(data);
const updateChatsPerAgentChart = (agents) => {
Object
.keys(agents)
.forEach((agent) => updateChartData('lc-chats-per-agent-chart', agent, [agents[agent].open, agents[agent].closed]));
};
const updateTimingsOverview = () => {
const data = getTimingsOverviewData(LivechatMonitoring.find());
const loadAgentsStatusChartData = () => APIClient.v1.get('livechat/analytics/dashboards/charts/agents-status');
templateInstance.timingOverview.set(data);
const updateAgentStatusChart = async (statusData) => {
if (!statusData) {
return;
}
await updateChartData('lc-agents-chart', 'Offline', [statusData.offline]);
await updateChartData('lc-agents-chart', 'Available', [statusData.available]);
await updateChartData('lc-agents-chart', 'Away', [statusData.away]);
await updateChartData('lc-agents-chart', 'Busy', [statusData.busy]);
};
const displayDepartmentChart = (val) => {
const elem = document.getElementsByClassName('lc-chats-per-dept-chart-section')[0];
elem.style.display = val ? 'block' : 'none';
const loadChatsPerDepartmentChartData = async ({ start, end }) => {
const result = await APIClient.v1.get(`livechat/analytics/dashboards/charts/chats-per-department?start=${ start }&end=${ end }`);
delete result.success;
return result;
};
const updateVisitorsCount = () => {
templateInstance.totalVisitors.set({
title: templateInstance.totalVisitors.get().title,
value: LivechatVisitors.find().count(),
});
const updateDepartmentsChart = (departments) => {
Object
.keys(departments)
.forEach((department) => updateChartData('lc-chats-per-dept-chart', department, [departments[department].open, departments[department].closed]));
};
const loadTimingsChartData = ({ start, end }) => APIClient.v1.get(`livechat/analytics/dashboards/charts/timings?start=${ start }&end=${ end }`);
const updateTimingsChart = async (timingsData) => {
const hour = moment(new Date()).format('H');
const label = `${ moment(hour, ['H']).format('hA') }-${ moment((parseInt(hour) + 1) % 24, ['H']).format('hA') }`;
await updateChartData('lc-reaction-response-times-chart', label, [timingsData.reaction.avg, timingsData.reaction.longest, timingsData.response.avg, timingsData.response.longest]);
await updateChartData('lc-chat-duration-chart', label, [timingsData.chatDuration.avg, timingsData.chatDuration.longest]);
};
const getIntervalInMS = () => templateInstance.interval.get() * 1000;
Template.livechatRealTimeMonitoring.helpers({
showDepartmentChart() {
return templateInstance.showDepartmentChart.get();
selected(value) {
return value === templateInstance.analyticsOptions.get().value || value === templateInstance.chartOptions.get().value ? 'selected' : false;
},
conversationsOverview() {
return templateInstance.conversationsOverview.get();
@ -218,106 +220,60 @@ Template.livechatRealTimeMonitoring.helpers({
timingOverview() {
return templateInstance.timingOverview.get();
},
totalVisitors() {
return templateInstance.totalVisitors.get();
agentsOverview() {
return templateInstance.agentsOverview.get();
},
chatsOverview() {
return templateInstance.chatsOverview.get();
},
isLoading() {
return Template.instance().isLoading.get();
},
});
Template.livechatRealTimeMonitoring.onCreated(function() {
templateInstance = Template.instance();
this.isLoading = new ReactiveVar(false);
this.conversationsOverview = new ReactiveVar();
this.timingOverview = new ReactiveVar();
this.totalVisitors = new ReactiveVar({
title: 'Total_visitors',
value: 0,
});
AgentUsers.find().observeChanges({
changed() {
updateAgentStatusChart();
},
added() {
updateAgentStatusChart();
},
});
LivechatVisitors.find().observeChanges({
added() {
updateVisitorsCount();
},
removed() {
updateVisitorsCount();
},
});
LivechatDepartment.find({ enabled: true }).observeChanges({
changed(id) {
displayDepartmentChart(true);
updateDepartmentsChart(id);
},
added(id) {
displayDepartmentChart(true);
updateDepartmentsChart(id);
},
});
const updateMonitoringDashboard = (id, fields) => {
const { ts } = LivechatMonitoring.findOne({ _id: id });
if (fields.metrics) {
// metrics changed
metricsUpdated(ts);
updateChatsChart();
updateAgentsChart();
updateTimingsOverview();
updateDepartmentsChart();
}
if (fields.servedBy) {
// agent data changed
updateAgentsChart(fields.servedBy.username);
updateChatsChart();
}
if (fields.open) {
updateAgentsChart();
updateChatsChart();
}
if (fields.departmentId) {
updateDepartmentsChart(fields.departmentId);
}
this.chatsOverview = new ReactiveVar();
this.agentsOverview = new ReactiveVar();
this.conversationTotalizers = new ReactiveVar([]);
this.interval = new ReactiveVar(5);
});
if (fields.msgs) {
updateConversationsOverview();
}
Template.livechatRealTimeMonitoring.onRendered(async function() {
await initAllCharts();
this.updateDashboard = async () => {
const daterange = getDaterange();
updateConversationOverview(await loadConversationOverview(daterange));
updateProductivityOverview(await loadProductivityOverview(daterange));
updateChatsChart(await loadChatsChartData(daterange));
updateChatsPerAgentChart(await loadChatsPerAgentChartData(daterange));
updateAgentStatusChart(await loadAgentsStatusChartData());
updateDepartmentsChart(await loadChatsPerDepartmentChartData(daterange));
updateTimingsChart(await loadTimingsChartData(daterange));
updateAgentsOverview(await loadAgentsOverview(daterange));
updateChatsOverview(await loadChatsOverview(daterange));
};
LivechatMonitoring.find().observeChanges({
changed(id, fields) {
updateMonitoringDashboard(id, fields);
},
added(id, fields) {
updateMonitoringDashboard(id, fields);
},
this.autorun(() => {
if (timer) {
clearInterval(timer);
}
timer = setInterval(() => this.updateDashboard(), getIntervalInMS());
});
this.isLoading.set(true);
await this.updateDashboard();
this.isLoading.set(false);
});
Template.livechatRealTimeMonitoring.onRendered(function() {
chartContexts = {}; // Clear chart contexts from previous loads, fixing bug when menu is reopened after changing to another.
initAllCharts();
displayDepartmentChart(false);
Template.livechatRealTimeMonitoring.events({
'change .js-interval': (event, instance) => {
instance.interval.set(event.target.value);
},
});
this.subscribe('livechat:departments');
this.subscribe('livechat:agents');
this.subscribe('livechat:monitoring', {
gte: moment().startOf('day').toISOString(),
lt: moment().startOf('day').add(1, 'days').toISOString(),
});
this.subscribe('livechat:visitors', {
gte: moment().startOf('day').toISOString(),
lt: moment().startOf('day').add(1, 'days').toISOString(),
});
Template.livechatRealTimeMonitoring.onDestroyed(function() {
clearInterval(timer);
});

@ -0,0 +1,219 @@
import { check } from 'meteor/check';
import { API } from '../../../../api';
import { hasPermission } from '../../../../authorization/server';
import {
findAllChatsStatus,
getProductivityMetrics,
getConversationsMetrics,
findAllChatMetricsByAgent,
findAllAgentsStatus,
findAllChatMetricsByDepartment,
findAllResponseTimeMetrics,
getAgentsProductivityMetrics,
getChatsMetrics,
} from '../../../server/lib/analytics/dashboards';
API.v1.addRoute('livechat/analytics/dashboards/conversation-totalizers', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-livechat-manager')) {
return API.v1.unauthorized();
}
let { start, end } = this.requestParams();
check(start, String);
check(end, String);
if (isNaN(Date.parse(start))) {
return API.v1.failure('The "start" query parameter must be a valid date.');
}
start = new Date(start);
if (isNaN(Date.parse(end))) {
return API.v1.failure('The "end" query parameter must be a valid date.');
}
end = new Date(end);
const totalizers = getConversationsMetrics({ start, end });
return API.v1.success(totalizers);
},
});
API.v1.addRoute('livechat/analytics/dashboards/agents-productivity-totalizers', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-livechat-manager')) {
return API.v1.unauthorized();
}
let { start, end } = this.requestParams();
check(start, String);
check(end, String);
if (isNaN(Date.parse(start))) {
return API.v1.failure('The "start" query parameter must be a valid date.');
}
start = new Date(start);
if (isNaN(Date.parse(end))) {
return API.v1.failure('The "end" query parameter must be a valid date.');
}
end = new Date(end);
const totalizers = getAgentsProductivityMetrics({ start, end });
return API.v1.success(totalizers);
},
});
API.v1.addRoute('livechat/analytics/dashboards/chats-totalizers', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-livechat-manager')) {
return API.v1.unauthorized();
}
let { start, end } = this.requestParams();
check(start, String);
check(end, String);
if (isNaN(Date.parse(start))) {
return API.v1.failure('The "start" query parameter must be a valid date.');
}
start = new Date(start);
if (isNaN(Date.parse(end))) {
return API.v1.failure('The "end" query parameter must be a valid date.');
}
end = new Date(end);
const totalizers = getChatsMetrics({ start, end });
return API.v1.success(totalizers);
},
});
API.v1.addRoute('livechat/analytics/dashboards/productivity-totalizers', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-livechat-manager')) {
return API.v1.unauthorized();
}
let { start, end } = this.requestParams();
check(start, String);
check(end, String);
if (isNaN(Date.parse(start))) {
return API.v1.failure('The "start" query parameter must be a valid date.');
}
start = new Date(start);
if (isNaN(Date.parse(end))) {
return API.v1.failure('The "end" query parameter must be a valid date.');
}
end = new Date(end);
const totalizers = getProductivityMetrics({ start, end });
return API.v1.success(totalizers);
},
});
API.v1.addRoute('livechat/analytics/dashboards/charts/chats', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-livechat-manager')) {
return API.v1.unauthorized();
}
let { start, end } = this.requestParams();
check(start, String);
check(end, String);
if (isNaN(Date.parse(start))) {
return API.v1.failure('The "start" query parameter must be a valid date.');
}
start = new Date(start);
if (isNaN(Date.parse(end))) {
return API.v1.failure('The "end" query parameter must be a valid date.');
}
end = new Date(end);
const result = findAllChatsStatus({ start, end });
return API.v1.success(result);
},
});
API.v1.addRoute('livechat/analytics/dashboards/charts/chats-per-agent', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-livechat-manager')) {
return API.v1.unauthorized();
}
let { start, end } = this.requestParams();
check(start, String);
check(end, String);
if (isNaN(Date.parse(start))) {
return API.v1.failure('The "start" query parameter must be a valid date.');
}
start = new Date(start);
if (isNaN(Date.parse(end))) {
return API.v1.failure('The "end" query parameter must be a valid date.');
}
end = new Date(end);
const result = findAllChatMetricsByAgent({ start, end });
return API.v1.success(result);
},
});
API.v1.addRoute('livechat/analytics/dashboards/charts/agents-status', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-livechat-manager')) {
return API.v1.unauthorized();
}
const result = findAllAgentsStatus({});
return API.v1.success(result);
},
});
API.v1.addRoute('livechat/analytics/dashboards/charts/chats-per-department', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-livechat-manager')) {
return API.v1.unauthorized();
}
let { start, end } = this.requestParams();
check(start, String);
check(end, String);
if (isNaN(Date.parse(start))) {
return API.v1.failure('The "start" query parameter must be a valid date.');
}
start = new Date(start);
if (isNaN(Date.parse(end))) {
return API.v1.failure('The "end" query parameter must be a valid date.');
}
end = new Date(end);
const result = findAllChatMetricsByDepartment({ start, end });
return API.v1.success(result);
},
});
API.v1.addRoute('livechat/analytics/dashboards/charts/timings', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-livechat-manager')) {
return API.v1.unauthorized();
}
let { start, end } = this.requestParams();
check(start, String);
check(end, String);
if (isNaN(Date.parse(start))) {
return API.v1.failure('The "start" query parameter must be a valid date.');
}
start = new Date(start);
if (isNaN(Date.parse(end))) {
return API.v1.failure('The "end" query parameter must be a valid date.');
}
end = new Date(end);
const result = findAllResponseTimeMetrics({ start, end });
return API.v1.success(result);
},
});

@ -11,5 +11,6 @@ import '../imports/server/rest/triggers.js';
import '../imports/server/rest/integrations.js';
import '../imports/server/rest/messages.js';
import '../imports/server/rest/visitors.js';
import '../imports/server/rest/dashboards.js';
import '../imports/server/rest/queue.js';
import '../imports/server/rest/officeHour.js';

@ -8,13 +8,16 @@ callbacks.add('afterSaveMessage', function(message, room) {
return message;
}
// check if room is yet awaiting for response
if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.waitingResponse)) {
// if the message has a token, it was sent by the visitor, so ignore it
if (message.token) {
return message;
}
if (room.responseBy) {
LivechatRooms.setAgentLastMessageTs(room._id);
}
// if the message has a token, it was sent by the visitor, so ignore it
if (message.token) {
// check if room is yet awaiting for response
if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.waitingResponse)) {
return message;
}

@ -1,31 +1,7 @@
import moment from 'moment';
import { LivechatRooms } from '../../../models';
/**
* return readable time format from seconds
* @param {Double} sec seconds
* @return {String} Readable string format
*/
const secondsToHHMMSS = (sec) => {
sec = parseFloat(sec);
let hours = Math.floor(sec / 3600);
let minutes = Math.floor((sec - (hours * 3600)) / 60);
let seconds = Math.round(sec - (hours * 3600) - (minutes * 60));
if (hours < 10) { hours = `0${ hours }`; }
if (minutes < 10) { minutes = `0${ minutes }`; }
if (seconds < 10) { seconds = `0${ seconds }`; }
if (hours > 0) {
return `${ hours }:${ minutes }:${ seconds }`;
}
if (minutes > 0) {
return `${ minutes }:${ seconds }`;
}
return sec;
};
import { secondsToHHMMSS } from '../../../utils/server';
export const Analytics = {
getAgentOverviewData(options) {
@ -285,8 +261,8 @@ export const Analytics = {
lt: moment(m).add(1, 'days'),
};
const result = LivechatRooms.getAnalyticsMetricsBetweenDate('l', date);
totalConversations += result.count();
const result = Promise.await(LivechatRooms.getAnalyticsBetweenDate(date).toArray());
totalConversations += result.length;
result.forEach(summarize(m));
}
@ -302,8 +278,7 @@ export const Analytics = {
gte: h,
lt: moment(h).add(1, 'hours'),
};
LivechatRooms.getAnalyticsMetricsBetweenDate('l', date).forEach(({
Promise.await(LivechatRooms.getAnalyticsBetweenDate(date).toArray()).forEach(({
msgs,
}) => {
const dayHour = h.format('H'); // @int : 0, 1, ... 23

@ -0,0 +1,270 @@
import moment from 'moment';
import { LivechatRooms, Users, LivechatVisitors, LivechatAgentActivity } from '../../../../models/server/raw';
import { Livechat } from '../Livechat';
import { secondsToHHMMSS } from '../../../../utils/server';
import {
findPercentageOfAbandonedRoomsAsync,
findAllAverageOfChatDurationTimeAsync,
findAllAverageWaitingTimeAsync,
findAllNumberOfAbandonedRoomsAsync,
findAllAverageServiceTimeAsync,
} from './departments';
const findAllChatsStatusAsync = async ({
start,
end,
departmentId = undefined,
}) => {
if (!start || !end) {
throw new Error('"start" and "end" must be provided');
}
return {
open: await LivechatRooms.countAllOpenChatsBetweenDate({ start, end, departmentId }),
closed: await LivechatRooms.countAllClosedChatsBetweenDate({ start, end, departmentId }),
queued: await LivechatRooms.countAllQueuedChatsBetweenDate({ start, end, departmentId }),
};
};
const getProductivityMetricsAsync = async ({
start,
end,
departmentId = undefined,
}) => {
if (!start || !end) {
throw new Error('"start" and "end" must be provided');
}
const totalizers = Livechat.Analytics.getAnalyticsOverviewData({
daterange: {
from: start,
to: end,
},
analyticsOptions: {
name: 'Productivity',
},
});
const averageWaitingTime = await findAllAverageWaitingTimeAsync({
start,
end,
departmentId,
});
const totalOfWaitingTime = averageWaitingTime.departments.length;
const sumOfWaitingTime = averageWaitingTime.departments.reduce((acc, serviceTime) => {
acc += serviceTime.averageWaitingTimeInSeconds;
return acc;
}, 0);
const totalOfAvarageWaitingTime = totalOfWaitingTime === 0 ? 0 : sumOfWaitingTime / totalOfWaitingTime;
return {
totalizers: [
...totalizers,
{ title: 'Avg_of_waiting_time', value: secondsToHHMMSS(totalOfAvarageWaitingTime) },
],
};
};
const getAgentsProductivityMetricsAsync = async ({
start,
end,
departmentId = undefined,
}) => {
if (!start || !end) {
throw new Error('"start" and "end" must be provided');
}
const averageOfAvailableServiceTime = (await LivechatAgentActivity.findAllAverageAvailableServiceTime({
date: parseInt(moment(start).format('YYYYMMDD')),
departmentId,
}))[0];
const averageOfServiceTime = await findAllAverageServiceTimeAsync({
start,
end,
departmentId,
});
const totalizers = Livechat.Analytics.getAnalyticsOverviewData({
daterange: {
from: start,
to: end,
},
analyticsOptions: {
name: 'Conversations',
},
});
const totalOfServiceTime = averageOfServiceTime.departments.length;
const sumOfServiceTime = averageOfServiceTime.departments.reduce((acc, serviceTime) => {
acc += serviceTime.averageServiceTimeInSeconds;
return acc;
}, 0);
const totalOfAverageAvailableServiceTime = averageOfAvailableServiceTime ? averageOfAvailableServiceTime.averageAvailableServiceTimeInSeconds : 0;
const totalOfAverageServiceTime = totalOfServiceTime === 0 ? 0 : sumOfServiceTime / totalOfServiceTime;
return {
totalizers: [
...totalizers.filter((metric) => metric.title === 'Busiest_time'),
{ title: 'Avg_of_available_service_time', value: secondsToHHMMSS(totalOfAverageAvailableServiceTime) },
{ title: 'Avg_of_service_time', value: secondsToHHMMSS(totalOfAverageServiceTime) },
],
};
};
const getChatsMetricsAsync = async ({
start,
end,
departmentId = undefined,
}) => {
if (!start || !end) {
throw new Error('"start" and "end" must be provided');
}
const abandonedRooms = await findAllNumberOfAbandonedRoomsAsync({
start,
end,
departmentId,
});
const averageOfAbandonedRooms = await findPercentageOfAbandonedRoomsAsync({
start,
end,
departmentId,
});
const averageOfChatDurationTime = await findAllAverageOfChatDurationTimeAsync({
start,
end,
departmentId,
});
const totalOfAbandonedRooms = averageOfAbandonedRooms.departments.length;
const totalOfChatDurationTime = averageOfChatDurationTime.departments.length;
const sumOfPercentageOfAbandonedRooms = averageOfAbandonedRooms.departments.reduce((acc, abandonedRoom) => {
acc += abandonedRoom.percentageOfAbandonedChats;
return acc;
}, 0);
const sumOfChatDurationTime = averageOfChatDurationTime.departments.reduce((acc, chatDurationTime) => {
acc += chatDurationTime.averageChatDurationTimeInSeconds;
return acc;
}, 0);
const totalAbandonedRooms = abandonedRooms.departments.reduce((acc, item) => {
acc += item.abandonedRooms;
return acc;
}, 0);
const totalOfAverageAbandonedRooms = totalOfAbandonedRooms === 0 ? 0 : sumOfPercentageOfAbandonedRooms / totalOfAbandonedRooms;
const totalOfAverageChatDurationTime = totalOfChatDurationTime === 0 ? 0 : sumOfChatDurationTime / totalOfChatDurationTime;
return {
totalizers: [
{ title: 'Total_abandoned_chats', value: totalAbandonedRooms },
{ title: 'Avg_of_abandoned_chats', value: `${ totalOfAverageAbandonedRooms }%` },
{ title: 'Avg_of_chat_duration_time', value: secondsToHHMMSS(totalOfAverageChatDurationTime) },
],
};
};
const getConversationsMetricsAsync = async ({
start,
end,
}) => {
if (!start || !end) {
throw new Error('"start" and "end" must be provided');
}
const totalizers = Livechat.Analytics.getAnalyticsOverviewData({
daterange: {
from: start,
to: end,
},
analyticsOptions: {
name: 'Conversations',
},
});
const metrics = ['Total_conversations', 'Open_conversations', 'Total_messages'];
const visitorsCount = await LivechatVisitors.getVisitorsBetweenDate({ start, end }).count();
return {
totalizers: [
...totalizers.filter((metric) => metrics.includes(metric.title)),
{ title: 'Total_visitors', value: visitorsCount },
],
};
};
const findAllChatMetricsByAgentAsync = async ({
start,
end,
departmentId = undefined,
}) => {
if (!start || !end) {
throw new Error('"start" and "end" must be provided');
}
const open = await LivechatRooms.countAllOpenChatsByAgentBetweenDate({ start, end, departmentId });
const closed = await LivechatRooms.countAllClosedChatsByAgentBetweenDate({ start, end, departmentId });
const result = {};
(open || []).forEach((agent) => {
result[agent._id] = { open: agent.chats, closed: 0 };
});
(closed || []).forEach((agent) => {
result[agent._id] = { open: result[agent._id] ? result[agent._id].open : 0, closed: agent.chats };
});
return result;
};
const findAllAgentsStatusAsync = async ({ departmentId = undefined }) => (await Users.countAllAgentsStatus({ departmentId }))[0];
const findAllChatMetricsByDepartmentAsync = async ({
start,
end,
departmentId = undefined,
}) => {
if (!start || !end) {
throw new Error('"start" and "end" must be provided');
}
const open = await LivechatRooms.countAllOpenChatsByDepartmentBetweenDate({ start, end, departmentId });
const closed = await LivechatRooms.countAllClosedChatsByDepartmentBetweenDate({ start, end, departmentId });
const result = {};
(open || []).forEach((department) => {
result[department.name] = { open: department.chats, closed: 0 };
});
(closed || []).forEach((department) => {
result[department.name] = { open: result[department.name] ? result[department.name].open : 0, closed: department.chats };
});
return result;
};
const findAllResponseTimeMetricsAsync = async ({
start,
end,
departmentId = undefined,
}) => {
if (!start || !end) {
throw new Error('"start" and "end" must be provided');
}
const responseTimes = (await LivechatRooms.calculateResponseTimingsBetweenDates({ start, end, departmentId }))[0];
const reactionTimes = (await LivechatRooms.calculateReactionTimingsBetweenDates({ start, end, departmentId }))[0];
const durationTimings = (await LivechatRooms.calculateDurationTimingsBetweenDates({ start, end, departmentId }))[0];
return {
response: {
avg: responseTimes ? responseTimes.avg : 0,
longest: responseTimes ? responseTimes.longest : 0,
},
reaction: {
avg: reactionTimes ? reactionTimes.avg : 0,
longest: reactionTimes ? reactionTimes.longest : 0,
},
chatDuration: {
avg: durationTimings ? durationTimings.avg : 0,
longest: durationTimings ? durationTimings.longest : 0,
},
};
};
export const findAllChatsStatus = ({ start, end, departmentId = undefined }) => Promise.await(findAllChatsStatusAsync({ start, end, departmentId }));
export const getProductivityMetrics = ({ start, end, departmentId = undefined }) => Promise.await(getProductivityMetricsAsync({ start, end, departmentId }));
export const getAgentsProductivityMetrics = ({ start, end, departmentId = undefined }) => Promise.await(getAgentsProductivityMetricsAsync({ start, end, departmentId }));
export const getConversationsMetrics = ({ start, end, departmentId = undefined }) => Promise.await(getConversationsMetricsAsync({ start, end, departmentId }));
export const findAllChatMetricsByAgent = ({ start, end, departmentId = undefined }) => Promise.await(findAllChatMetricsByAgentAsync({ start, end, departmentId }));
export const findAllChatMetricsByDepartment = ({ start, end, departmentId = undefined }) => Promise.await(findAllChatMetricsByDepartmentAsync({ start, end, departmentId }));
export const findAllResponseTimeMetrics = ({ start, end, departmentId = undefined }) => Promise.await(findAllResponseTimeMetricsAsync({ start, end, departmentId }));
export const getChatsMetrics = ({ start, end, departmentId = undefined }) => Promise.await(getChatsMetricsAsync({ start, end, departmentId }));
export const findAllAgentsStatus = ({ departmentId = undefined }) => Promise.await(findAllAgentsStatusAsync({ departmentId }));

@ -1,6 +1,6 @@
import { LivechatDepartment } from '../../../../models/server/raw';
import { LivechatRooms } from '../../../../models/server/raw';
const findAllRoomsAsync = async ({
export const findAllRoomsAsync = async ({
start,
end,
answered,
@ -11,12 +11,12 @@ const findAllRoomsAsync = async ({
throw new Error('"start" and "end" must be provided');
}
return {
departments: await LivechatDepartment.findAllRooms({ start, answered, end, departmentId, options }),
total: (await LivechatDepartment.findAllRooms({ start, answered, end, departmentId })).length,
departments: await LivechatRooms.findAllRooms({ start, answered, end, departmentId, options }),
total: (await LivechatRooms.findAllRooms({ start, answered, end, departmentId })).length,
};
};
const findAllAverageServiceTimeAsync = async ({
export const findAllAverageOfChatDurationTimeAsync = async ({
start,
end,
departmentId,
@ -26,12 +26,12 @@ const findAllAverageServiceTimeAsync = async ({
throw new Error('"start" and "end" must be provided');
}
return {
departments: await LivechatDepartment.findAllAverageServiceTime({ start, end, departmentId, options }),
total: (await LivechatDepartment.findAllAverageServiceTime({ start, end, departmentId })).length,
departments: await LivechatRooms.findAllAverageOfChatDurationTime({ start, end, departmentId, options }),
total: (await LivechatRooms.findAllAverageOfChatDurationTime({ start, end, departmentId })).length,
};
};
const findAllServiceTimeAsync = async ({
export const findAllAverageServiceTimeAsync = async ({
start,
end,
departmentId,
@ -41,12 +41,12 @@ const findAllServiceTimeAsync = async ({
throw new Error('"start" and "end" must be provided');
}
return {
departments: await LivechatDepartment.findAllServiceTime({ start, end, departmentId, options }),
total: (await LivechatDepartment.findAllServiceTime({ start, end, departmentId })).length,
departments: await LivechatRooms.findAllAverageOfServiceTime({ start, end, departmentId, options }),
total: (await LivechatRooms.findAllAverageOfServiceTime({ start, end, departmentId })).length,
};
};
const findAllAverageWaitingTimeAsync = async ({
export const findAllServiceTimeAsync = async ({
start,
end,
departmentId,
@ -56,12 +56,12 @@ const findAllAverageWaitingTimeAsync = async ({
throw new Error('"start" and "end" must be provided');
}
return {
departments: await LivechatDepartment.findAllAverageWaitingTime({ start, end, departmentId, options }),
total: (await LivechatDepartment.findAllAverageWaitingTime({ start, end, departmentId })).length,
departments: await LivechatRooms.findAllServiceTime({ start, end, departmentId, options }),
total: (await LivechatRooms.findAllServiceTime({ start, end, departmentId })).length,
};
};
const findAllNumberOfTransferredRoomsAsync = async ({
export const findAllAverageWaitingTimeAsync = async ({
start,
end,
departmentId,
@ -71,12 +71,12 @@ const findAllNumberOfTransferredRoomsAsync = async ({
throw new Error('"start" and "end" must be provided');
}
return {
departments: await LivechatDepartment.findAllNumberOfTransferredRooms({ start, end, departmentId, options }),
total: (await LivechatDepartment.findAllNumberOfTransferredRooms({ start, end, departmentId })).length,
departments: await LivechatRooms.findAllAverageWaitingTime({ start, end, departmentId, options }),
total: (await LivechatRooms.findAllAverageWaitingTime({ start, end, departmentId })).length,
};
};
const findAllNumberOfAbandonedRoomsAsync = async ({
export const findAllNumberOfTransferredRoomsAsync = async ({
start,
end,
departmentId,
@ -86,12 +86,12 @@ const findAllNumberOfAbandonedRoomsAsync = async ({
throw new Error('"start" and "end" must be provided');
}
return {
departments: await LivechatDepartment.findAllNumberOfAbandonedRooms({ start, end, departmentId, options }),
total: (await LivechatDepartment.findAllNumberOfAbandonedRooms({ start, end, departmentId })).length,
departments: await LivechatRooms.findAllNumberOfTransferredRooms({ start, end, departmentId, options }),
total: (await LivechatRooms.findAllNumberOfTransferredRooms({ start, end, departmentId })).length,
};
};
const findPercentageOfAbandonedRoomsAsync = async ({
export const findAllNumberOfAbandonedRoomsAsync = async ({
start,
end,
departmentId,
@ -101,11 +101,27 @@ const findPercentageOfAbandonedRoomsAsync = async ({
throw new Error('"start" and "end" must be provided');
}
return {
departments: await LivechatDepartment.findPercentageOfAbandonedRooms({ start, end, departmentId, options }),
total: (await LivechatDepartment.findPercentageOfAbandonedRooms({ start, end, departmentId })).length,
departments: await LivechatRooms.findAllNumberOfAbandonedRooms({ start, end, departmentId, options }),
total: (await LivechatRooms.findAllNumberOfAbandonedRooms({ start, end, departmentId })).length,
};
};
export const findPercentageOfAbandonedRoomsAsync = async ({
start,
end,
departmentId,
options = {},
}) => {
if (!start || !end) {
throw new Error('"start" and "end" must be provided');
}
return {
departments: await LivechatRooms.findPercentageOfAbandonedRooms({ start, end, departmentId, options }),
total: (await LivechatRooms.findPercentageOfAbandonedRooms({ start, end, departmentId })).length,
};
};
export const findAllAverageOfChatDurationTime = ({ start, end, departmentId, options }) => Promise.await(findAllAverageOfChatDurationTimeAsync({ start, end, departmentId, options }));
export const findAllAverageServiceTime = ({ start, end, departmentId, options }) => Promise.await(findAllAverageServiceTimeAsync({ start, end, departmentId, options }));
export const findAllRooms = ({ start, end, answered, departmentId, options }) => Promise.await(findAllRoomsAsync({ start, end, answered, departmentId, options }));
export const findAllServiceTime = ({ start, end, departmentId, options }) => Promise.await(findAllServiceTimeAsync({ start, end, departmentId, options }));

@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor';
import { hasPermission, getUsersInRole } from '../../../authorization';
Meteor.publish('livechat:agents', function() {
console.warn('The publication "livechat:agents" is deprecated and will be removed after version v3.0.0');
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' }));
}

@ -4,6 +4,7 @@ import { hasPermission } from '../../../authorization';
import { LivechatDepartment } from '../../../models';
Meteor.publish('livechat:departments', function(_id, limit = 50) {
console.warn('The publication "livechat:departments" is deprecated and will be removed after version v3.0.0');
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:departments' }));
}

@ -5,6 +5,7 @@ import { hasPermission } from '../../../authorization';
import { LivechatRooms } from '../../../models';
Meteor.publish('livechat:monitoring', function(date) {
console.warn('The publication "livechat:monitoring" is deprecated and will be removed after version v3.0.0');
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:monitoring' }));
}

@ -5,6 +5,7 @@ import { hasPermission } from '../../../authorization';
import { LivechatVisitors } from '../../../models';
Meteor.publish('livechat:visitors', function(date) {
console.warn('The publication "livechat:visitors" is deprecated and will be removed after version v3.0.0');
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitors' }));
}

@ -3,8 +3,8 @@ import { Meteor } from 'meteor/meteor';
import { hasPermission } from '../../../authorization';
import { LivechatRooms, LivechatVisitors } from '../../../models';
console.warn('The publication "livechat:visitorInfo" is deprecated and will be removed after version v3.0.0');
Meteor.publish('livechat:visitorInfo', function({ rid: roomId }) {
console.warn('The publication "livechat:visitorInfo" is deprecated and will be removed after version v3.0.0');
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorInfo' }));
}

@ -253,6 +253,7 @@ export class LivechatRooms extends Base {
responseBy: {
_id: response.user._id,
username: response.user.username,
lastMessageTs: new Date(),
},
},
$unset: {
@ -261,6 +262,17 @@ export class LivechatRooms extends Base {
});
}
setAgentLastMessageTs(roomId) {
return this.update({
_id: roomId,
t: 'l',
}, {
$set: {
'responseBy.lastMessageTs': new Date(),
},
});
}
saveAnalyticsDataByRoomId(room, message, analyticsData) {
const update = {
$set: {},
@ -324,6 +336,69 @@ export class LivechatRooms extends Base {
return this.find(query, { fields: { ts: 1, departmentId: 1, open: 1, servedBy: 1, metrics: 1, msgs: 1 } });
}
getAnalyticsBetweenDate(date) {
return this.model.rawCollection().aggregate([
{
$match: {
t: 'l',
ts: {
$gte: new Date(date.gte), // ISO Date, ts >= date.gte
$lt: new Date(date.lt), // ISODate, ts < date.lt
},
},
},
{
$lookup: {
from: 'rocketchat_message',
localField: '_id',
foreignField: 'rid',
as: 'messages',
},
},
{
$unwind: {
path: '$messages',
preserveNullAndEmptyArrays: true,
},
},
{
$group: {
_id: {
_id: '$_id',
ts: '$ts',
departmentId: '$departmentId',
open: '$open',
servedBy: '$servedBy',
metrics: '$metrics',
msgs: '$msgs',
},
messages: {
$sum: {
$cond: [{
$and: [
{ $ifNull: ['$messages.t', false] },
],
}, 1, 0],
},
},
},
},
{
$project: {
_id: '$_id._id',
ts: '$_id.ts',
departmentId: '$_id.departmentId',
open: '$_id.open',
servedBy: '$_id.servedBy',
metrics: '$_id.metrics',
msgs: { $subtract: ['$_id.msgs', '$messages'] },
},
},
]);
}
closeByRoomId(roomId, closeInfo) {
return this.update({
_id: roomId,

@ -0,0 +1,67 @@
import { BaseRaw } from './BaseRaw';
export class LivechatAgentActivityRaw extends BaseRaw {
findAllAverageAvailableServiceTime({ date, departmentId }) {
const match = { $match: { date } };
const lookup = {
$lookup: {
from: 'rocketchat_livechat_department_agents',
localField: 'agentId',
foreignField: 'agentId',
as: 'departments',
},
};
const unwind = {
$unwind: {
path: '$departments',
preserveNullAndEmptyArrays: true,
},
};
const departmentsMatch = {
$match: {
'departments.departmentId': departmentId,
},
};
const sumAvailableTimeWithCurrentTime = {
$sum: [
{ $divide: [{ $subtract: [new Date(), '$lastStartedAt'] }, 1000] },
'$availableTime',
],
};
const group = {
$group: {
_id: null,
allAvailableTimeInSeconds: {
$sum: {
$cond: [{ $ifNull: ['$lastStoppedAt', false] },
'$availableTime',
sumAvailableTimeWithCurrentTime],
},
},
rooms: { $sum: 1 },
},
};
const project = {
$project: {
averageAvailableServiceTimeInSeconds: {
$trunc: {
$cond: [
{ $eq: ['$rooms', 0] },
0,
{ $divide: ['$allAvailableTimeInSeconds', '$rooms'] },
],
},
},
},
};
const params = [match];
if (departmentId) {
params.push(lookup);
params.push(unwind);
params.push(departmentsMatch);
}
params.push(group);
params.push(project);
return this.col.aggregate(params).toArray();
}
}

@ -1,489 +1,6 @@
import { BaseRaw } from './BaseRaw';
import { getValue } from '../../../settings/server/raw';
export class LivechatDepartmentRaw extends BaseRaw {
findAllRooms({ start, end, answered, departmentId, options = {} }) {
const roomsFilter = [
{ $gte: ['$$room.ts', new Date(start)] },
{ $lte: ['$$room.ts', new Date(end)] },
];
if (answered !== undefined) {
roomsFilter.push({ [answered ? '$ne' : '$eq']: ['$$room.waitingResponse', true] });
}
const lookup = {
$lookup: {
from: 'rocketchat_room',
localField: '_id',
foreignField: 'departmentId',
as: 'rooms',
},
};
const project = {
$project: {
name: 1,
description: 1,
enabled: 1,
rooms: {
$size: {
$filter: {
input: '$rooms',
as: 'room',
cond: {
$and: roomsFilter,
},
},
},
},
},
};
const match = {
$match: {
_id: departmentId,
},
};
const params = [lookup, project];
if (departmentId) {
params.unshift(match);
}
if (options.offset) {
params.push({ $skip: options.offset });
}
if (options.count) {
params.push({ $limit: options.count });
}
if (options.sort) {
params.push({ $sort: { name: 1 } });
}
return this.col.aggregate(params).toArray();
}
findAllAverageServiceTime({ start, end, departmentId, options = {} }) {
const roomsFilter = [
{ $gte: ['$$room.ts', new Date(start)] },
{ $lte: ['$$room.ts', new Date(end)] },
];
const lookup = {
$lookup: {
from: 'rocketchat_room',
localField: '_id',
foreignField: 'departmentId',
as: 'rooms',
},
};
const projects = [
{
$project: {
department: '$$ROOT',
rooms: {
$filter: {
input: '$rooms',
as: 'room',
cond: {
$and: roomsFilter,
},
},
},
},
},
{
$project: {
department: '$department',
chats: { $size: '$rooms' },
chatsDuration: { $sum: '$rooms.metrics.chatDuration' },
},
},
{
$project: {
name: '$department.name',
description: '$department.description',
enabled: '$department.enabled',
averageServiceTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$chats', 0] }, 0, { $divide: ['$chatsDuration', '$chats'] }] } },
},
}];
const match = {
$match: {
_id: departmentId,
},
};
const params = [lookup, ...projects];
if (departmentId) {
params.unshift(match);
}
if (options.offset) {
params.push({ $skip: options.offset });
}
if (options.count) {
params.push({ $limit: options.count });
}
if (options.sort) {
params.push({ $sort: { name: 1 } });
}
return this.col.aggregate(params).toArray();
}
findAllServiceTime({ start, end, departmentId, options = {} }) {
const roomsFilter = [
{ $gte: ['$$room.ts', new Date(start)] },
{ $lte: ['$$room.ts', new Date(end)] },
];
const lookup = {
$lookup: {
from: 'rocketchat_room',
localField: '_id',
foreignField: 'departmentId',
as: 'rooms',
},
};
const projects = [
{
$project: {
department: '$$ROOT',
rooms: {
$filter: {
input: '$rooms',
as: 'room',
cond: {
$and: roomsFilter,
},
},
},
},
},
{
$project: {
name: '$department.name',
description: '$department.description',
enabled: '$department.enabled',
chats: { $size: '$rooms' },
chatsDuration: { $ceil: { $sum: '$rooms.metrics.chatDuration' } },
},
}];
const match = {
$match: {
_id: departmentId,
},
};
const params = [lookup, ...projects];
if (departmentId) {
params.unshift(match);
}
if (options.offset) {
params.push({ $skip: options.offset });
}
if (options.count) {
params.push({ $limit: options.count });
}
if (options.sort) {
params.push({ $sort: { name: 1 } });
}
return this.col.aggregate(params).toArray();
}
findAllAverageWaitingTime({ start, end, departmentId, options = {} }) {
const roomsFilter = [
{ $gte: ['$$room.ts', new Date(start)] },
{ $lte: ['$$room.ts', new Date(end)] },
{ $ne: ['$$room.waitingResponse', true] },
];
const lookup = {
$lookup: {
from: 'rocketchat_room',
localField: '_id',
foreignField: 'departmentId',
as: 'rooms',
},
};
const projects = [{
$project: {
department: '$$ROOT',
rooms: {
$filter: {
input: '$rooms',
as: 'room',
cond: {
$and: roomsFilter,
},
},
},
},
},
{
$project: {
department: '$department',
chats: { $size: '$rooms' },
chatsFirstResponses: { $sum: '$rooms.metrics.response.ft' },
},
},
{
$project: {
name: '$department.name',
description: '$department.description',
enabled: '$department.enabled',
averageWaitingTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$chats', 0] }, 0, { $divide: ['$chatsFirstResponses', '$chats'] }] } },
},
}];
const match = {
$match: {
_id: departmentId,
},
};
const params = [lookup, ...projects];
if (departmentId) {
params.unshift(match);
}
if (options.offset) {
params.push({ $skip: options.offset });
}
if (options.count) {
params.push({ $limit: options.count });
}
if (options.sort) {
params.push({ $sort: { name: 1 } });
}
return this.col.aggregate(params).toArray();
}
findAllNumberOfTransferredRooms({ start, end, departmentId, options = {} }) {
const messageFilter = [
{ $gte: ['$$message.ts', new Date(start)] },
{ $lte: ['$$message.ts', new Date(end)] },
{ $eq: ['$$message.t', 'livechat_transfer_history'] },
];
const roomsLookup = {
$lookup: {
from: 'rocketchat_room',
localField: '_id',
foreignField: 'departmentId',
as: 'rooms',
},
};
const messagesLookup = {
$lookup: {
from: 'rocketchat_message',
localField: 'rooms._id',
foreignField: 'rid',
as: 'messages',
},
};
const projectRooms = {
$project: {
department: '$$ROOT',
rooms: 1,
},
};
const projectMessages = {
$project: {
department: '$department',
messages: {
$filter: {
input: '$messages',
as: 'message',
cond: {
$and: messageFilter,
},
},
},
},
};
const projectTransfersSize = {
$project: {
department: '$department',
transfers: { $size: { $ifNull: ['$messages', []] } },
},
};
const group = {
$group: {
_id: {
departmentId: '$department._id',
name: '$department.name',
description: '$department.description',
enabled: '$department.enabled',
},
numberOfTransferredRooms: { $sum: '$transfers' },
},
};
const presentationProject = {
$project: {
_id: '$_id.departmentId',
name: '$_id.name',
description: '$_id.description',
enabled: '$_id.enabled',
numberOfTransferredRooms: 1,
},
};
const unwind = {
$unwind: {
path: '$rooms',
preserveNullAndEmptyArrays: true,
},
};
const match = {
$match: {
_id: departmentId,
},
};
const params = [roomsLookup, projectRooms, unwind, messagesLookup, projectMessages, projectTransfersSize, group, presentationProject];
if (departmentId) {
params.unshift(match);
}
if (options.offset) {
params.push({ $skip: options.offset });
}
if (options.count) {
params.push({ $limit: options.count });
}
if (options.sort) {
params.push({ $sort: { name: 1 } });
}
return this.col.aggregate(params).toArray();
}
async findAllNumberOfAbandonedRooms({ start, end, departmentId, options = {} }) {
const roomsFilter = [
{ $gte: ['$$room.ts', new Date(start)] },
{ $lte: ['$$room.ts', new Date(end)] },
{ $gte: ['$$room.metrics.visitorInactivity', await getValue('Livechat_visitor_inactivity_timeout')] },
];
const lookup = {
$lookup: {
from: 'rocketchat_room',
localField: '_id',
foreignField: 'departmentId',
as: 'rooms',
},
};
const projects = [{
$project: {
department: '$$ROOT',
rooms: {
$filter: {
input: '$rooms',
as: 'room',
cond: {
$and: roomsFilter,
},
},
},
},
},
{
$project: {
name: '$department.name',
description: '$department.description',
enabled: '$department.enabled',
abandonedRooms: { $size: '$rooms' },
},
}];
const match = {
$match: {
_id: departmentId,
},
};
const params = [lookup, ...projects];
if (departmentId) {
params.unshift(match);
}
if (options.offset) {
params.push({ $skip: options.offset });
}
if (options.count) {
params.push({ $limit: options.count });
}
if (options.sort) {
params.push({ $sort: { name: 1 } });
}
return this.col.aggregate(params).toArray();
}
findPercentageOfAbandonedRooms({ start, end, departmentId, options = {} }) {
const roomsFilter = [
{ $gte: ['$$room.ts', new Date(start)] },
{ $lte: ['$$room.ts', new Date(end)] },
];
const lookup = {
$lookup: {
from: 'rocketchat_room',
localField: '_id',
foreignField: 'departmentId',
as: 'rooms',
},
};
const projectRooms = {
$project: {
department: '$$ROOT',
rooms: {
$filter: {
input: '$rooms',
as: 'room',
cond: {
$and: roomsFilter,
},
},
},
},
};
const unwind = {
$unwind: {
path: '$rooms',
preserveNullAndEmptyArrays: true,
},
};
const group = {
$group: {
_id: {
departmentId: '$department._id',
name: '$department.name',
description: '$department.description',
enabled: '$department.enabled',
},
abandonedChats: {
$sum: {
$cond: [{
$and: [
{ $ifNull: ['$rooms.metrics.visitorInactivity', false] },
{ $gte: ['$rooms.metrics.visitorInactivity', 1] },
],
}, 1, 0],
},
},
chats: { $sum: 1 },
},
};
const presentationProject = {
$project: {
_id: '$_id.departmentId',
name: '$_id.name',
description: '$_id.description',
enabled: '$_id.enabled',
percentageOfAbandonedChats: {
$floor: {
$cond: [
{ $eq: ['$chats', 0] },
0,
{ $divide: [{ $multiply: ['$abandonedChats', 100] }, '$chats'] },
],
},
},
},
};
const match = {
$match: {
_id: departmentId,
},
};
const params = [lookup, projectRooms, unwind, group, presentationProject];
if (departmentId) {
params.unshift(match);
}
if (options.offset) {
params.push({ $skip: options.offset });
}
if (options.count) {
params.push({ $limit: options.count });
}
if (options.sort) {
params.push({ $sort: { name: 1 } });
}
return this.col.aggregate(params).toArray();
}
}

File diff suppressed because it is too large Load Diff

@ -1,5 +1,14 @@
import { BaseRaw } from './BaseRaw';
export class LivechatVisitorsRaw extends BaseRaw {
getVisitorsBetweenDate({ start, end }) {
const query = {
_updatedAt: {
$gte: new Date(start),
$lt: new Date(end),
},
};
return this.find(query, { fields: { _id: 1 } });
}
}

@ -264,4 +264,88 @@ export class UsersRaw extends BaseRaw {
}
return this.col.aggregate(params).toArray();
}
countAllAgentsStatus({ departmentId = undefined }) {
const match = {
$match: {
roles: { $in: ['livechat-agent'] },
},
};
const group = {
$group: {
_id: null,
offline: {
$sum: {
$cond: [{
$or: [{
$and: [
{ $eq: ['$status', 'offline'] },
{ $eq: ['$statusLivechat', 'available'] },
],
},
{ $eq: ['$statusLivechat', 'not-available'] },
],
}, 1, 0],
},
},
away: {
$sum: {
$cond: [{
$and: [
{ $eq: ['$status', 'away'] },
{ $eq: ['$statusLivechat', 'available'] },
],
}, 1, 0],
},
},
busy: {
$sum: {
$cond: [{
$and: [
{ $eq: ['$status', 'busy'] },
{ $eq: ['$statusLivechat', 'available'] },
],
}, 1, 0],
},
},
available: {
$sum: {
$cond: [{
$and: [
{ $eq: ['$status', 'online'] },
{ $eq: ['$statusLivechat', 'available'] },
],
}, 1, 0],
},
},
},
};
const lookup = {
$lookup: {
from: 'rocketchat_livechat_department_agents',
localField: '_id',
foreignField: 'agentId',
as: 'departments',
},
};
const unwind = {
$unwind: {
path: '$departments',
preserveNullAndEmptyArrays: true,
},
};
const departmentsMatch = {
$match: {
'departments.departmentId': departmentId,
},
};
const params = [match];
if (departmentId) {
params.push(lookup);
params.push(unwind);
params.push(departmentsMatch);
}
params.push(group);
return this.col.aggregate(params).toArray();
}
}

@ -28,6 +28,8 @@ import LivechatExternalMessagesModel from '../models/LivechatExternalMessages';
import { LivechatExternalMessageRaw } from './LivechatExternalMessages';
import LivechatVisitorsModel from '../models/LivechatVisitors';
import { LivechatVisitorsRaw } from './LivechatVisitors';
import LivechatAgentActivityModel from '../models/LivechatAgentActivity';
import { LivechatAgentActivityRaw } from './LivechatAgentActivity';
export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection());
export const Roles = new RolesRaw(RolesModel.model.rawCollection());
@ -44,3 +46,4 @@ export const LivechatRooms = new LivechatRoomsRaw(LivechatRoomsModel.model.rawCo
export const Messages = new MessagesRaw(MessagesModel.model.rawCollection());
export const LivechatExternalMessage = new LivechatExternalMessageRaw(LivechatExternalMessagesModel.model.rawCollection());
export const LivechatVisitors = new LivechatVisitorsRaw(LivechatVisitorsModel.model.rawCollection());
export const LivechatAgentActivity = new LivechatAgentActivityRaw(LivechatAgentActivityModel.model.rawCollection());

@ -20,3 +20,4 @@ export { templateVarHandler } from '../lib/templateVarHandler';
export { APIClient } from './lib/RestApiClient';
export { canDeleteMessage } from './lib/canDeleteMessage';
export { mime } from '../lib/mimeTypes';
export { secondsToHHMMSS } from '../lib/timeConverter';

@ -0,0 +1,24 @@
/**
* return readable time format from seconds
* @param {Double} sec seconds
* @return {String} Readable string format
*/
export const secondsToHHMMSS = (sec) => {
sec = parseFloat(sec);
let hours = Math.floor(sec / 3600);
let minutes = Math.floor((sec - (hours * 3600)) / 60);
let seconds = Math.round(sec - (hours * 3600) - (minutes * 60));
if (hours < 10) { hours = `0${ hours }`; }
if (minutes < 10) { minutes = `0${ minutes }`; }
if (seconds < 10) { seconds = `0${ seconds }`; }
if (hours > 0) {
return `${ hours }:${ minutes }:${ seconds }`;
}
if (minutes > 0) {
return `00:${ minutes }:${ seconds }`;
}
return `00:00:${ seconds }`;
};

@ -17,3 +17,4 @@ export { getValidRoomName } from '../lib/getValidRoomName';
export { placeholders } from '../lib/placeholders';
export { templateVarHandler } from '../lib/templateVarHandler';
export { mime } from '../lib/mimeTypes';
export { secondsToHHMMSS } from '../lib/timeConverter';

@ -450,10 +450,15 @@
"Avatar_changed_successfully": "Avatar changed successfully",
"Avatar_URL": "Avatar URL",
"Avatar_url_invalid_or_error": "The url provided is invalid or not accessible. Please try again, but with a different url.",
"Avg_chat_duration": "Avg Chat Duration",
"Avg_first_response_time": "Avg First Response Time",
"Avg_response_time": "Avg Response Time",
"Avg_reaction_time": "Avg Reaction Time",
"Avg_chat_duration": "Average of Chat Duration",
"Avg_first_response_time": "Average of First Response Time",
"Avg_of_abandoned_chats": "Average of Abandoned Chats",
"Avg_of_available_service_time": "Average of Service Available Time",
"Avg_of_chat_duration_time": "Average of Chat Duration Time",
"Avg_of_service_time": "Average of Service Time",
"Avg_of_waiting_time": "Average of Waiting Time",
"Avg_response_time": "Average of Response Time",
"Avg_reaction_time": "Average of Reaction Time",
"away": "away",
"Away": "Away",
"away_female": "away",
@ -2238,6 +2243,7 @@
"Min_length_is": "Min length is %s",
"Minimum": "Minimum",
"Minimum_balance": "Minimum balance",
"minute": "minute",
"minutes": "minutes",
"Mobile": "Mobile",
"Mobile_Notifications_Default_Alert": "Mobile Notifications Default Alert",
@ -3153,6 +3159,7 @@
"Tokens_Required_Input_Placeholder": "Tokens asset names",
"Topic": "Topic",
"Total": "Total",
"Total_abandoned_chats": "Total Abandoned Chats",
"Total_conversations": "Total Conversations",
"Total_Discussions": "Total Discussions",
"Total_messages": "Total Messages",

@ -425,6 +425,11 @@
"Avatar_url_invalid_or_error": "A URL fornecida é inválida ou não acessível. Por favor tente novamente, mas com uma url diferente.",
"Avg_chat_duration": "Duração média do Chat",
"Avg_first_response_time": "Tempo médio de resposta inicial",
"Avg_of_abandoned_chats": "Média de abandono das conversas",
"Avg_of_available_service_time": "Tempo médio de serviço disponível",
"Avg_of_chat_duration_time": "Tempo médio de duração das conversas",
"Avg_of_service_time": "Tempo médio de serviço",
"Avg_of_waiting_time": "Tempo médio de espera",
"Avg_response_time": "Tempo médio de resposta",
"Avg_reaction_time": "Tempo médio de Reação",
"away": "ausente",
@ -2079,6 +2084,7 @@
"meteor_status_try_now_offline": "Conectar novamente",
"Min_length_is": "Tamanho mínimo é %s",
"Minimum_balance": "Saldo mínimo",
"minute": "minuto",
"minutes": "minutos",
"Mobile": "Móvel",
"Mobile_Notifications_Default_Alert": "Alertas Padrão de Notificações Móveis",
@ -2930,6 +2936,7 @@
"Tokens_Required_Input_Placeholder": "Tokens asset names",
"Topic": "Tópico",
"Total": "Total",
"Total_abandoned_chats": "Total de conversas abandonadas",
"Total_conversations": "Total de conversas",
"Total_Discussions": "Total de discussões",
"Total_messages": "Quantidade de Mensagens",

@ -0,0 +1,314 @@
import { getCredentials, api, request, credentials } from '../../../data/api-data.js';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';
describe('LIVECHAT - dashboards', function() {
this.retries(0);
before((done) => getCredentials(done));
before((done) => {
updateSetting('Livechat_enabled', true).then(done);
});
describe('livechat/analytics/dashboards/conversation-totalizers', () => {
const expectedMetrics = ['Total_conversations', 'Open_conversations', 'Total_messages', 'Busiest_time', 'Total_abandoned_chats', 'Total_visitors'];
it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
updatePermission('view-livechat-manager', []).then(() => {
request.get(api('livechat/analytics/dashboards/conversation-totalizers'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('unauthorized');
})
.end(done);
});
});
it('should return an array of conversation totalizers', (done) => {
updatePermission('view-livechat-manager', ['admin'])
.then(() => {
request.get(api('livechat/analytics/dashboards/conversation-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.totalizers).to.be.an('array');
res.body.totalizers.forEach((prop) => expect(expectedMetrics.includes(prop.title)).to.be.true);
})
.end(done);
});
});
});
describe('livechat/analytics/dashboards/productivity-totalizers', () => {
const expectedMetrics = [
'Avg_response_time',
'Avg_first_response_time',
'Avg_reaction_time',
'Avg_of_waiting_time',
];
it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
updatePermission('view-livechat-manager', []).then(() => {
request.get(api('livechat/analytics/dashboards/productivity-totalizers'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('unauthorized');
})
.end(done);
});
});
it('should return an array of productivity totalizers', (done) => {
updatePermission('view-livechat-manager', ['admin'])
.then(() => {
request.get(api('livechat/analytics/dashboards/productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.totalizers).to.be.an('array');
res.body.totalizers.forEach((prop) => expect(expectedMetrics.includes(prop.title)).to.be.true);
})
.end(done);
});
});
});
describe('livechat/analytics/dashboards/chats-totalizers', () => {
const expectedMetrics = [
'Total_abandoned_chats',
'Avg_of_abandoned_chats',
'Avg_of_chat_duration_time',
];
it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
updatePermission('view-livechat-manager', []).then(() => {
request.get(api('livechat/analytics/dashboards/chats-totalizers'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('unauthorized');
})
.end(done);
});
});
it('should return an array of chats totalizers', (done) => {
updatePermission('view-livechat-manager', ['admin'])
.then(() => {
request.get(api('livechat/analytics/dashboards/chats-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.totalizers).to.be.an('array');
res.body.totalizers.forEach((prop) => expect(expectedMetrics.includes(prop.title)).to.be.true);
})
.end(done);
});
});
});
describe('livechat/analytics/dashboards/agents-productivity-totalizers', () => {
const expectedMetrics = [
'Busiest_time',
'Avg_of_available_service_time',
'Avg_of_service_time',
];
it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
updatePermission('view-livechat-manager', []).then(() => {
request.get(api('livechat/analytics/dashboards/agents-productivity-totalizers'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('unauthorized');
})
.end(done);
});
});
it('should return an array of agents productivity totalizers', (done) => {
updatePermission('view-livechat-manager', ['admin'])
.then(() => {
request.get(api('livechat/analytics/dashboards/agents-productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.totalizers).to.be.an('array');
res.body.totalizers.forEach((prop) => expect(expectedMetrics.includes(prop.title)).to.be.true);
})
.end(done);
});
});
});
describe('livechat/analytics/dashboards/charts/chats', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
updatePermission('view-livechat-manager', []).then(() => {
request.get(api('livechat/analytics/dashboards/charts/chats'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('unauthorized');
})
.end(done);
});
});
it('should return an array of productivity totalizers', (done) => {
updatePermission('view-livechat-manager', ['admin'])
.then(() => {
request.get(api('livechat/analytics/dashboards/charts/chats?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('open');
expect(res.body).to.have.property('closed');
expect(res.body).to.have.property('queued');
})
.end(done);
});
});
});
describe('livechat/analytics/dashboards/charts/chats-per-agent', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
updatePermission('view-livechat-manager', []).then(() => {
request.get(api('livechat/analytics/dashboards/charts/chats-per-agent'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('unauthorized');
})
.end(done);
});
});
it('should return an object with open and closed chats by agent', (done) => {
updatePermission('view-livechat-manager', ['admin'])
.then(() => {
request.get(api('livechat/analytics/dashboards/charts/chats-per-agent?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
});
});
describe('livechat/analytics/dashboards/charts/agents-status', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
updatePermission('view-livechat-manager', []).then(() => {
request.get(api('livechat/analytics/dashboards/charts/agents-status'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('unauthorized');
})
.end(done);
});
});
it('should return an object with agents status metrics', (done) => {
updatePermission('view-livechat-manager', ['admin'])
.then(() => {
request.get(api('livechat/analytics/dashboards/charts/agents-status'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offline');
expect(res.body).to.have.property('away');
expect(res.body).to.have.property('busy');
expect(res.body).to.have.property('available');
})
.end(done);
});
});
});
describe('livechat/analytics/dashboards/charts/chats-per-department', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
updatePermission('view-livechat-manager', []).then(() => {
request.get(api('livechat/analytics/dashboards/charts/chats-per-department'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('unauthorized');
})
.end(done);
});
});
it('should return an object with open and closed chats by department', (done) => {
updatePermission('view-livechat-manager', ['admin'])
.then(() => {
request.get(api('livechat/analytics/dashboards/charts/chats-per-department?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
});
});
describe('livechat/analytics/dashboards/charts/timings', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
updatePermission('view-livechat-manager', []).then(() => {
request.get(api('livechat/analytics/dashboards/charts/timings'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('unauthorized');
})
.end(done);
});
});
it('should return an object with open and closed chats by department', (done) => {
updatePermission('view-livechat-manager', ['admin'])
.then(() => {
request.get(api('livechat/analytics/dashboards/charts/timings?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('response');
expect(res.body).to.have.property('reaction');
expect(res.body).to.have.property('chatDuration');
expect(res.body.response).to.have.property('avg');
expect(res.body.response).to.have.property('longest');
expect(res.body.reaction).to.have.property('avg');
expect(res.body.reaction).to.have.property('longest');
expect(res.body.chatDuration).to.have.property('avg');
expect(res.body.chatDuration).to.have.property('longest');
})
.end(done);
});
});
});
});
Loading…
Cancel
Save