diff --git a/apps/meteor/app/livechat/imports/server/rest/dashboards.ts b/apps/meteor/app/livechat/imports/server/rest/dashboards.ts index 8951854bff8..8dfdefc7e91 100644 --- a/apps/meteor/app/livechat/imports/server/rest/dashboards.ts +++ b/apps/meteor/app/livechat/imports/server/rest/dashboards.ts @@ -3,15 +3,15 @@ import { isGETDashboardTotalizerParams, isGETDashboardsAgentStatusParams } from import { API } from '../../../../api/server'; import { - findAllChatsStatusAsync, - getProductivityMetricsAsync, - getConversationsMetricsAsync, - findAllChatMetricsByAgentAsync, - findAllAgentsStatusAsync, - findAllChatMetricsByDepartmentAsync, - findAllResponseTimeMetricsAsync, - getAgentsProductivityMetricsAsync, - getChatsMetricsAsync, + getProductivityMetricsAsyncCached, + getConversationsMetricsAsyncCached, + getAgentsProductivityMetricsAsyncCached, + getChatsMetricsAsyncCached, + findAllChatsStatusAsyncCached, + findAllChatMetricsByAgentAsyncCached, + findAllAgentsStatusAsyncCached, + findAllChatMetricsByDepartmentAsyncCached, + findAllResponseTimeMetricsAsyncCached, } from '../../../server/lib/analytics/dashboards'; API.v1.addRoute( @@ -41,7 +41,7 @@ API.v1.addRoute( return API.v1.failure('User not found'); } - const totalizers = await getConversationsMetricsAsync({ start: startDate, end: endDate, departmentId, user }); + const totalizers = await getConversationsMetricsAsyncCached({ start: startDate, end: endDate, departmentId, user }); return API.v1.success(totalizers); }, }, @@ -70,7 +70,7 @@ API.v1.addRoute( return API.v1.failure('User not found'); } - const totalizers = await getAgentsProductivityMetricsAsync({ start: startDate, end: endDate, departmentId, user }); + const totalizers = await getAgentsProductivityMetricsAsyncCached({ start: startDate, end: endDate, departmentId, user }); return API.v1.success(totalizers); }, }, @@ -94,7 +94,7 @@ API.v1.addRoute( } const endDate = new Date(end); - const totalizers = await getChatsMetricsAsync({ start: startDate, end: endDate, departmentId }); + const totalizers = await getChatsMetricsAsyncCached({ start: startDate, end: endDate, departmentId }); return API.v1.success(totalizers); }, }, @@ -123,7 +123,7 @@ API.v1.addRoute( return API.v1.failure('User not found'); } - const totalizers = await getProductivityMetricsAsync({ start: startDate, end: endDate, departmentId, user }); + const totalizers = await getProductivityMetricsAsyncCached({ start: startDate, end: endDate, departmentId, user }); return API.v1.success(totalizers); }, @@ -148,7 +148,7 @@ API.v1.addRoute( } const endDate = new Date(end); - const result = await findAllChatsStatusAsync({ start: startDate, end: endDate, departmentId }); + const result = await findAllChatsStatusAsyncCached({ start: startDate, end: endDate, departmentId }); return API.v1.success(result); }, @@ -172,7 +172,7 @@ API.v1.addRoute( return API.v1.failure('The "end" query parameter must be a valid date.'); } const endDate = new Date(end); - const result = (await findAllChatMetricsByAgentAsync({ start: startDate, end: endDate, departmentId })) as { + const result = (await findAllChatMetricsByAgentAsyncCached({ start: startDate, end: endDate, departmentId })) as { [k: string]: { open: number; closed: number; onhold: number }; }; @@ -188,7 +188,7 @@ API.v1.addRoute( async get() { const { departmentId } = this.queryParams; - const result = await findAllAgentsStatusAsync({ departmentId }); + const result = await findAllAgentsStatusAsyncCached({ departmentId }); return API.v1.success(result); }, @@ -213,7 +213,7 @@ API.v1.addRoute( } const endDate = new Date(end); - const result = (await findAllChatMetricsByDepartmentAsync({ start: startDate, end: endDate, departmentId })) as { + const result = (await findAllChatMetricsByDepartmentAsyncCached({ start: startDate, end: endDate, departmentId })) as { [k: string]: { open: number; closed: number }; }; @@ -240,7 +240,7 @@ API.v1.addRoute( } const endDate = new Date(end); - const result = await findAllResponseTimeMetricsAsync({ start: startDate, end: endDate, departmentId }); + const result = await findAllResponseTimeMetricsAsyncCached({ start: startDate, end: endDate, departmentId }); return API.v1.success(result); }, diff --git a/apps/meteor/app/livechat/server/api/v1/statistics.ts b/apps/meteor/app/livechat/server/api/v1/statistics.ts index 078f366bb48..2cf30474a7d 100644 --- a/apps/meteor/app/livechat/server/api/v1/statistics.ts +++ b/apps/meteor/app/livechat/server/api/v1/statistics.ts @@ -3,7 +3,7 @@ import { isLivechatAnalyticsAgentOverviewProps, isLivechatAnalyticsOverviewProps import { API } from '../../../../api/server'; import { settings } from '../../../../settings/server'; -import { Livechat } from '../../lib/Livechat'; +import { getAgentOverviewDataCached, getAnalyticsOverviewDataCached } from '../../lib/AnalyticsTyped'; API.v1.addRoute( 'livechat/analytics/agent-overview', @@ -22,7 +22,7 @@ API.v1.addRoute( const user = await Users.findOneById(this.userId, { projection: { _id: 1, utcOffset: 1 } }); return API.v1.success( - await Livechat.Analytics.getAgentOverviewData({ + await getAgentOverviewDataCached({ departmentId, utcOffset: user?.utcOffset || 0, daterange: { from, to }, @@ -52,7 +52,7 @@ API.v1.addRoute( const language = user?.language || settings.get('Language') || 'en'; return API.v1.success( - await Livechat.Analytics.getAnalyticsOverviewData({ + await getAnalyticsOverviewDataCached({ departmentId, utcOffset: user?.utcOffset || 0, daterange: { from, to }, diff --git a/apps/meteor/app/livechat/server/lib/Analytics.js b/apps/meteor/app/livechat/server/lib/Analytics.js index 28bed221afb..bff3dd1d25d 100644 --- a/apps/meteor/app/livechat/server/lib/Analytics.js +++ b/apps/meteor/app/livechat/server/lib/Analytics.js @@ -36,866 +36,866 @@ async function* hourIterator(day) { } } -export const Analytics = { - async getAgentOverviewData(options) { - const { departmentId, utcOffset, daterange: { from: fDate, to: tDate } = {}, chartOptions: { name } = {} } = options; - const timezone = getTimezone({ utcOffset }); - const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); - const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - - if (!(moment(from).isValid() && moment(to).isValid())) { - logger.error('livechat:getAgentOverviewData => Invalid dates'); - return; - } - - if (!this.AgentOverviewData[name]) { - logger.error(`Method RocketChat.Livechat.Analytics.AgentOverviewData.${name} does NOT exist`); - return; - } +const OverviewData = { + /** + * + * @param {Map} map + * + * @return {String} + */ + getKeyHavingMaxValue(map, def) { + let maxValue = 0; + let maxKey = def; // default + + map.forEach((value, key) => { + if (value > maxValue) { + maxValue = value; + maxKey = key; + } + }); - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - return this.AgentOverviewData[name](from, to, departmentId, extraQuery); + return maxKey; }, - async getAnalyticsChartData(options) { - const { - utcOffset, - departmentId, - daterange: { from: fDate, to: tDate } = {}, - chartOptions: { name: chartLabel }, - chartOptions: { name } = {}, - } = options; + /** + * + * @param {Date} from + * @param {Date} to + * + * @returns {Array[Object]} + */ + async Conversations(from, to, departmentId, timezone, t = (v) => v, extraQuery) { + // TODO: most calls to db here can be done in one single call instead of one per day/hour + let totalConversations = 0; // Total conversations + let openConversations = 0; // open conversations + let totalMessages = 0; // total msgs + const totalMessagesOnWeekday = new Map(); // total messages on weekdays i.e Monday, Tuesday... + 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, onHold = false }) => { + if (metrics && !metrics.chatDuration && !onHold) { + openConversations++; + } + totalMessages += msgs; - // Check if function exists, prevent server error in case property altered - if (!this.ChartData[name]) { - logger.error(`Method RocketChat.Livechat.Analytics.ChartData.${name} does NOT exist`); - return; - } + const weekday = m.format('dddd'); // @string: Monday, Tuesday ... + totalMessagesOnWeekday.set(weekday, totalMessagesOnWeekday.has(weekday) ? totalMessagesOnWeekday.get(weekday) + msgs : msgs); + }; - const timezone = getTimezone({ utcOffset }); - const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); - const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - const isSameDay = from.diff(to, 'days') === 0; + const m = moment.tz(from, timezone).startOf('day').utc(); + // eslint-disable-next-line no-unused-vars + for await (const _ of Array(days).fill(0)) { + const clonedDate = m.clone(); + const date = { + gte: clonedDate, + lt: m.add(1, 'days'), + }; + // eslint-disable-next-line no-await-in-loop + const result = await LivechatRooms.getAnalyticsBetweenDate(date, { departmentId }, extraQuery).toArray(); + totalConversations += result.length; - if (!(moment(from).isValid() && moment(to).isValid())) { - logger.error('livechat:getAnalyticsChartData => Invalid dates'); - return; + result.forEach(summarize(clonedDate)); } - const data = { - chartLabel, - dataLabels: [], - dataPoints: [], - }; - - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - if (isSameDay) { - // data for single day - const m = moment(from); - for await (const currentHour of Array.from({ length: HOURS_IN_DAY }, (_, i) => i)) { - const hour = m.add(currentHour ? 1 : 0, 'hour').format('H'); - const label = { - from: moment.utc().set({ hour }).tz(timezone).format('hA'), - to: moment.utc().set({ hour }).add(1, 'hour').tz(timezone).format('hA'), - }; - data.dataLabels.push(`${label.from}-${label.to}`); + const busiestDay = this.getKeyHavingMaxValue(totalMessagesOnWeekday, '-'); // returns key with max value - const date = { - gte: m, - lt: moment(m).add(1, 'hours'), - }; - - data.dataPoints.push(await this.ChartData[name](date, departmentId, extraQuery)); + // TODO: this code assumes the busiest day is the same every week, which may not be true + // This means that for periods larger than 1 week, the busiest hour won't be the "busiest hour" + // on the period, but the busiest hour on the busiest day. (sorry for busiest excess) + // iterate through all busiestDay in given date-range and find busiest hour + for await (const m of weekIterator(from, to, timezone)) { + if (m < from) { + continue; } - } else { - for await (const m of dayIterator(from, to)) { - data.dataLabels.push(m.format('M/D')); + for await (const h of hourIterator(m)) { const date = { - gte: m, - lt: moment(m).add(1, 'days'), + gte: h.clone(), + lt: h.add(1, 'hours'), }; - - data.dataPoints.push(await this.ChartData[name](date, departmentId, extraQuery)); + (await LivechatRooms.getAnalyticsBetweenDate(date, { departmentId }, extraQuery).toArray()).forEach(({ msgs }) => { + const dayHour = h.format('H'); // @int : 0, 1, ... 23 + totalMessagesInHour.set(dayHour, totalMessagesInHour.has(dayHour) ? totalMessagesInHour.get(dayHour) + msgs : msgs); + }); } } - return data; + const utcBusiestHour = this.getKeyHavingMaxValue(totalMessagesInHour, -1); + const busiestHour = { + 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 = await LivechatRooms.getOnHoldConversationsBetweenDate(from, to, departmentId, extraQuery); + + return [ + { + title: 'Total_conversations', + value: totalConversations, + }, + { + title: 'Open_conversations', + value: openConversations, + }, + { + title: 'On_Hold_conversations', + value: onHoldConversations, + }, + { + title: 'Total_messages', + value: totalMessages, + }, + { + title: 'Busiest_day', + value: t(busiestDay), + }, + { + title: 'Conversations_per_day', + value: (totalConversations / days).toFixed(2), + }, + { + title: 'Busiest_time', + value: `${busiestHour.from}${busiestHour.to ? `- ${busiestHour.to}` : ''}`, + }, + ]; }, - async getAnalyticsOverviewData(options) { - const { departmentId, utcOffset = 0, language, daterange: { from: fDate, to: tDate } = {}, analyticsOptions: { name } = {} } = options; - const timezone = getTimezone({ utcOffset }); - const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); - const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); + /** + * + * @param {Date} from + * @param {Date} to + * + * @returns {Array[Object]} + */ + async Productivity(from, to, departmentId, extraQuery) { + let avgResponseTime = 0; + let firstResponseTime = 0; + let avgReactionTime = 0; + let count = 0; + + const date = { + gte: from, + lt: to.add(1, 'days'), + }; - if (!(moment(from).isValid() && moment(to).isValid())) { - logger.error('livechat:getAnalyticsOverviewData => Invalid dates'); - return; - } + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { + if (metrics && metrics.response && metrics.reaction) { + avgResponseTime += metrics.response.avg; + firstResponseTime += metrics.response.ft; + avgReactionTime += metrics.reaction.ft; + count++; + } + }); - if (!this.OverviewData[name]) { - logger.error(`Method RocketChat.Livechat.Analytics.OverviewData.${name} does NOT exist`); - return; + if (count) { + avgResponseTime /= count; + firstResponseTime /= count; + avgReactionTime /= count; } - const t = (s) => i18n.t(s, { lng: language }); + const data = [ + { + title: 'Avg_response_time', + value: secondsToHHMMSS(avgResponseTime.toFixed(2)), + }, + { + title: 'Avg_first_response_time', + value: secondsToHHMMSS(firstResponseTime.toFixed(2)), + }, + { + title: 'Avg_reaction_time', + value: secondsToHHMMSS(avgReactionTime.toFixed(2)), + }, + ]; - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - return this.OverviewData[name](from, to, departmentId, timezone, t, extraQuery); + return data; }, +}; - ChartData: { - /** - * - * @param {Object} date {gte: {Date}, lt: {Date}} - * - * @returns {Integer} - */ - Total_conversations(date, departmentId, extraQuery) { - return LivechatRooms.getTotalConversationsBetweenDate('l', date, { departmentId }, extraQuery); - }, - - async Avg_chat_duration(date, departmentId, extraQuery) { - let total = 0; - let count = 0; - - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.chatDuration) { - total += metrics.chatDuration; - count++; - } - }); +const ChartData = { + /** + * + * @param {Object} date {gte: {Date}, lt: {Date}} + * + * @returns {Integer} + */ + Total_conversations(date, departmentId, extraQuery) { + return LivechatRooms.getTotalConversationsBetweenDate('l', date, { departmentId }, extraQuery); + }, - const avgCD = count ? total / count : 0; - return Math.round(avgCD * 100) / 100; - }, - - async Total_messages(date, departmentId, extraQuery) { - let total = 0; - - // we don't want to count visitor messages - const extraFilter = { $lte: ['$token', null] }; - const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages( - 'l', - date, - { departmentId }, - extraFilter, - extraQuery, - ).toArray(); - allConversations.map(({ msgs }) => { - if (msgs) { - total += msgs; - } - return null; - }); + async Avg_chat_duration(date, departmentId, extraQuery) { + let total = 0; + let count = 0; - return total; - }, - - /** - * - * @param {Object} date {gte: {Date}, lt: {Date}} - * - * @returns {Double} - */ - async Avg_first_response_time(date, departmentId, extraQuery) { - let frt = 0; - let count = 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.response && metrics.response.ft) { - frt += metrics.response.ft; - count++; - } - }); + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { + if (metrics && metrics.chatDuration) { + total += metrics.chatDuration; + count++; + } + }); - const avgFrt = count ? frt / count : 0; - return Math.round(avgFrt * 100) / 100; - }, - - /** - * - * @param {Object} date {gte: {Date}, lt: {Date}} - * - * @returns {Double} - */ - async Best_first_response_time(date, departmentId, extraQuery) { - let maxFrt; - - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.response && metrics.response.ft) { - maxFrt = maxFrt ? Math.min(maxFrt, metrics.response.ft) : metrics.response.ft; - } - }); + const avgCD = count ? total / count : 0; + return Math.round(avgCD * 100) / 100; + }, - if (!maxFrt) { - maxFrt = 0; + async Total_messages(date, departmentId, extraQuery) { + let total = 0; + + // we don't want to count visitor messages + const extraFilter = { $lte: ['$token', null] }; + const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages( + 'l', + date, + { departmentId }, + extraFilter, + extraQuery, + ).toArray(); + allConversations.map(({ msgs }) => { + if (msgs) { + total += msgs; } + return null; + }); - return Math.round(maxFrt * 100) / 100; - }, - - /** - * - * @param {Object} date {gte: {Date}, lt: {Date}} - * - * @returns {Double} - */ - async Avg_response_time(date, departmentId, extraQuery) { - let art = 0; - let count = 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.response && metrics.response.avg) { - art += metrics.response.avg; - count++; - } - }); + return total; + }, - const avgArt = count ? art / count : 0; - - return Math.round(avgArt * 100) / 100; - }, - - /** - * - * @param {Object} date {gte: {Date}, lt: {Date}} - * - * @returns {Double} - */ - async Avg_reaction_time(date, departmentId, extraQuery) { - let arnt = 0; - let count = 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.reaction && metrics.reaction.ft) { - arnt += metrics.reaction.ft; - count++; - } - }); + /** + * + * @param {Object} date {gte: {Date}, lt: {Date}} + * + * @returns {Double} + */ + async Avg_first_response_time(date, departmentId, extraQuery) { + let frt = 0; + let count = 0; + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { + if (metrics && metrics.response && metrics.response.ft) { + frt += metrics.response.ft; + count++; + } + }); - const avgArnt = count ? arnt / count : 0; + const avgFrt = count ? frt / count : 0; + return Math.round(avgFrt * 100) / 100; + }, + + /** + * + * @param {Object} date {gte: {Date}, lt: {Date}} + * + * @returns {Double} + */ + async Best_first_response_time(date, departmentId, extraQuery) { + let maxFrt; + + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { + if (metrics && metrics.response && metrics.response.ft) { + maxFrt = maxFrt ? Math.min(maxFrt, metrics.response.ft) : metrics.response.ft; + } + }); - return Math.round(avgArnt * 100) / 100; - }, + if (!maxFrt) { + maxFrt = 0; + } + + return Math.round(maxFrt * 100) / 100; }, - OverviewData: { - /** - * - * @param {Map} map - * - * @return {String} - */ - getKeyHavingMaxValue(map, def) { - let maxValue = 0; - let maxKey = def; // default - - map.forEach((value, key) => { - if (value > maxValue) { - maxValue = value; - maxKey = key; - } - }); + /** + * + * @param {Object} date {gte: {Date}, lt: {Date}} + * + * @returns {Double} + */ + async Avg_response_time(date, departmentId, extraQuery) { + let art = 0; + let count = 0; + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { + if (metrics && metrics.response && metrics.response.avg) { + art += metrics.response.avg; + count++; + } + }); - return maxKey; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array[Object]} - */ - async Conversations(from, to, departmentId, timezone, t = (v) => v, extraQuery) { - // TODO: most calls to db here can be done in one single call instead of one per day/hour - let totalConversations = 0; // Total conversations - let openConversations = 0; // open conversations - let totalMessages = 0; // total msgs - const totalMessagesOnWeekday = new Map(); // total messages on weekdays i.e Monday, Tuesday... - 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, onHold = false }) => { - if (metrics && !metrics.chatDuration && !onHold) { - openConversations++; - } - totalMessages += msgs; - - const weekday = m.format('dddd'); // @string: Monday, Tuesday ... - totalMessagesOnWeekday.set(weekday, totalMessagesOnWeekday.has(weekday) ? totalMessagesOnWeekday.get(weekday) + msgs : msgs); - }; + const avgArt = count ? art / count : 0; - const m = moment.tz(from, timezone).startOf('day').utc(); - // eslint-disable-next-line no-unused-vars - for await (const _ of Array(days).fill(0)) { - const clonedDate = m.clone(); - const date = { - gte: clonedDate, - lt: m.add(1, 'days'), - }; - // eslint-disable-next-line no-await-in-loop - const result = await LivechatRooms.getAnalyticsBetweenDate(date, { departmentId }, extraQuery).toArray(); - totalConversations += result.length; + return Math.round(avgArt * 100) / 100; + }, - result.forEach(summarize(clonedDate)); + /** + * + * @param {Object} date {gte: {Date}, lt: {Date}} + * + * @returns {Double} + */ + async Avg_reaction_time(date, departmentId, extraQuery) { + let arnt = 0; + let count = 0; + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { + if (metrics && metrics.reaction && metrics.reaction.ft) { + arnt += metrics.reaction.ft; + count++; } + }); - const busiestDay = this.getKeyHavingMaxValue(totalMessagesOnWeekday, '-'); // returns key with max value + const avgArnt = count ? arnt / count : 0; - // TODO: this code assumes the busiest day is the same every week, which may not be true - // This means that for periods larger than 1 week, the busiest hour won't be the "busiest hour" - // on the period, but the busiest hour on the busiest day. (sorry for busiest excess) - // iterate through all busiestDay in given date-range and find busiest hour - for await (const m of weekIterator(from, to, timezone)) { - if (m < from) { - continue; - } + return Math.round(avgArnt * 100) / 100; + }, +}; - for await (const h of hourIterator(m)) { - const date = { - gte: h.clone(), - lt: h.add(1, 'hours'), - }; - (await LivechatRooms.getAnalyticsBetweenDate(date, { departmentId }, extraQuery).toArray()).forEach(({ msgs }) => { - const dayHour = h.format('H'); // @int : 0, 1, ... 23 - totalMessagesInHour.set(dayHour, totalMessagesInHour.has(dayHour) ? totalMessagesInHour.get(dayHour) + msgs : msgs); - }); - } +const AgentOverviewData = { + /** + * do operation equivalent to map[key] += value + * + */ + updateMap(map, key, value) { + map.set(key, map.has(key) ? map.get(key) + value : value); + }, + + /** + * Sort array of objects by value property of object + * @param {Array(Object)} data + * @param {Boolean} [inv=false] reverse sort + */ + sortByValue(data, inv = false) { + data.sort((a, b) => { + // sort array + if (parseFloat(a.value) > parseFloat(b.value)) { + return inv ? -1 : 1; // if inv, reverse sort } + if (parseFloat(a.value) < parseFloat(b.value)) { + return inv ? 1 : -1; + } + return 0; + }); + }, - const utcBusiestHour = this.getKeyHavingMaxValue(totalMessagesInHour, -1); - const busiestHour = { - 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 = await LivechatRooms.getOnHoldConversationsBetweenDate(from, to, departmentId, extraQuery); + /** + * + * @param {Date} from + * @param {Date} to + * + * @returns {Array(Object), Array(Object)} + */ + async Total_conversations(from, to, departmentId, extraQuery) { + let total = 0; + const agentConversations = new Map(); // stores total conversations for each agent + const date = { + gte: from, + lt: to.add(1, 'days'), + }; - return [ - { - title: 'Total_conversations', - value: totalConversations, - }, - { - title: 'Open_conversations', - value: openConversations, - }, - { - title: 'On_Hold_conversations', - value: onHoldConversations, - }, + const data = { + head: [ { - title: 'Total_messages', - value: totalMessages, + name: 'Agent', }, { - title: 'Busiest_day', - value: t(busiestDay), + name: '%_of_conversations', }, + ], + data: [], + }; + + const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages( + 'l', + date, + { + departmentId, + }, + {}, + extraQuery, + ).toArray(); + allConversations.map((room) => { + if (room.servedBy) { + this.updateMap(agentConversations, room.servedBy.username, 1); + total++; + } + return null; + }); + + agentConversations.forEach((value, key) => { + // calculate percentage + const percentage = ((value / total) * 100).toFixed(2); + + data.data.push({ + name: key, + value: percentage, + }); + }); + + this.sortByValue(data.data, true); // reverse sort array + + data.data.forEach((value) => { + value.value = `${value.value}%`; + }); + + return data; + }, + + /** + * + * @param {Date} from + * @param {Date} to + * + * @returns {Array(Object), Array(Object)} + */ + async Avg_chat_duration(from, to, departmentId, extraQuery) { + const agentChatDurations = new Map(); // stores total conversations for each agent + const date = { + gte: from, + lt: to.add(1, 'days'), + }; + + const data = { + head: [ { - title: 'Conversations_per_day', - value: (totalConversations / days).toFixed(2), + name: 'Agent', }, { - title: 'Busiest_time', - value: `${busiestHour.from}${busiestHour.to ? `- ${busiestHour.to}` : ''}`, + name: 'Avg_chat_duration', }, - ]; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array[Object]} - */ - async Productivity(from, to, departmentId, extraQuery) { - let avgResponseTime = 0; - let firstResponseTime = 0; - let avgReactionTime = 0; - let count = 0; - - const date = { - gte: from, - lt: to.add(1, 'days'), - }; + ], + data: [], + }; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.response && metrics.reaction) { - avgResponseTime += metrics.response.avg; - firstResponseTime += metrics.response.ft; - avgReactionTime += metrics.reaction.ft; - count++; + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { + if (servedBy && metrics && metrics.chatDuration) { + if (agentChatDurations.has(servedBy.username)) { + agentChatDurations.set(servedBy.username, { + chatDuration: agentChatDurations.get(servedBy.username).chatDuration + metrics.chatDuration, + total: agentChatDurations.get(servedBy.username).total + 1, + }); + } else { + agentChatDurations.set(servedBy.username, { + chatDuration: metrics.chatDuration, + total: 1, + }); } + } + }); + + agentChatDurations.forEach((obj, key) => { + // calculate percentage + const avg = (obj.chatDuration / obj.total).toFixed(2); + + data.data.push({ + name: key, + value: avg, }); + }); - if (count) { - avgResponseTime /= count; - firstResponseTime /= count; - avgReactionTime /= count; - } + this.sortByValue(data.data, true); // reverse sort array - const data = [ - { - title: 'Avg_response_time', - value: secondsToHHMMSS(avgResponseTime.toFixed(2)), - }, + data.data.forEach((obj) => { + obj.value = secondsToHHMMSS(obj.value); + }); + + return data; + }, + + /** + * + * @param {Date} from + * @param {Date} to + * + * @returns {Array(Object), Array(Object)} + */ + async Total_messages(from, to, departmentId, extraQuery) { + const agentMessages = new Map(); // stores total conversations for each agent + const date = { + gte: from, + lt: to.add(1, 'days'), + }; + + const data = { + head: [ { - title: 'Avg_first_response_time', - value: secondsToHHMMSS(firstResponseTime.toFixed(2)), + name: 'Agent', }, { - title: 'Avg_reaction_time', - value: secondsToHHMMSS(avgReactionTime.toFixed(2)), + name: 'Total_messages', }, - ]; - - return data; - }, - }, + ], + data: [], + }; - AgentOverviewData: { - /** - * do operation equivalent to map[key] += value - * - */ - updateMap(map, key, value) { - map.set(key, map.has(key) ? map.get(key) + value : value); - }, - - /** - * Sort array of objects by value property of object - * @param {Array(Object)} data - * @param {Boolean} [inv=false] reverse sort - */ - sortByValue(data, inv = false) { - data.sort((a, b) => { - // sort array - if (parseFloat(a.value) > parseFloat(b.value)) { - return inv ? -1 : 1; // if inv, reverse sort - } - if (parseFloat(a.value) < parseFloat(b.value)) { - return inv ? 1 : -1; - } - return 0; + // we don't want to count visitor messages + const extraFilter = { $lte: ['$token', null] }; + const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages( + 'l', + date, + { departmentId }, + extraFilter, + extraQuery, + ).toArray(); + allConversations.map(({ servedBy, msgs }) => { + if (servedBy) { + this.updateMap(agentMessages, servedBy.username, msgs); + } + return null; + }); + + agentMessages.forEach((value, key) => { + // calculate percentage + data.data.push({ + name: key, + value, }); - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Total_conversations(from, to, departmentId, extraQuery) { - let total = 0; - const agentConversations = new Map(); // stores total conversations for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; + }); - const data = { - head: [ - { - name: 'Agent', - }, - { - name: '%_of_conversations', - }, - ], - data: [], - }; + this.sortByValue(data.data, true); // reverse sort array + + return data; + }, + + /** + * + * @param {Date} from + * @param {Date} to + * + * @returns {Array(Object), Array(Object)} + */ + async Avg_first_response_time(from, to, departmentId, extraQuery) { + const agentAvgRespTime = new Map(); // stores avg response time for each agent + const date = { + gte: from, + lt: to.add(1, 'days'), + }; - const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages( - 'l', - date, + const data = { + head: [ + { + name: 'Agent', + }, { - departmentId, + name: 'Avg_first_response_time', }, - {}, - extraQuery, - ).toArray(); - allConversations.map((room) => { - if (room.servedBy) { - this.updateMap(agentConversations, room.servedBy.username, 1); - total++; + ], + data: [], + }; + + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { + if (servedBy && metrics && metrics.response && metrics.response.ft) { + if (agentAvgRespTime.has(servedBy.username)) { + agentAvgRespTime.set(servedBy.username, { + frt: agentAvgRespTime.get(servedBy.username).frt + metrics.response.ft, + total: agentAvgRespTime.get(servedBy.username).total + 1, + }); + } else { + agentAvgRespTime.set(servedBy.username, { + frt: metrics.response.ft, + total: 1, + }); } - return null; - }); + } + }); - agentConversations.forEach((value, key) => { - // calculate percentage - const percentage = ((value / total) * 100).toFixed(2); + agentAvgRespTime.forEach((obj, key) => { + // calculate avg + const avg = obj.frt / obj.total; - data.data.push({ - name: key, - value: percentage, - }); + data.data.push({ + name: key, + value: avg.toFixed(2), }); + }); - this.sortByValue(data.data, true); // reverse sort array + this.sortByValue(data.data, false); // sort array - data.data.forEach((value) => { - value.value = `${value.value}%`; - }); + data.data.forEach((obj) => { + obj.value = secondsToHHMMSS(obj.value); + }); - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Avg_chat_duration(from, to, departmentId, extraQuery) { - const agentChatDurations = new Map(); // stores total conversations for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; + return data; + }, - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Avg_chat_duration', - }, - ], - data: [], - }; + /** + * + * @param {Date} from + * @param {Date} to + * + * @returns {Array(Object), Array(Object)} + */ + async Best_first_response_time(from, to, departmentId, extraQuery) { + const agentFirstRespTime = new Map(); // stores avg response time for each agent + const date = { + gte: from, + lt: to.add(1, 'days'), + }; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.chatDuration) { - if (agentChatDurations.has(servedBy.username)) { - agentChatDurations.set(servedBy.username, { - chatDuration: agentChatDurations.get(servedBy.username).chatDuration + metrics.chatDuration, - total: agentChatDurations.get(servedBy.username).total + 1, - }); - } else { - agentChatDurations.set(servedBy.username, { - chatDuration: metrics.chatDuration, - total: 1, - }); - } - } - }); + const data = { + head: [ + { + name: 'Agent', + }, + { + name: 'Best_first_response_time', + }, + ], + data: [], + }; - agentChatDurations.forEach((obj, key) => { - // calculate percentage - const avg = (obj.chatDuration / obj.total).toFixed(2); + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { + if (servedBy && metrics && metrics.response && metrics.response.ft) { + if (agentFirstRespTime.has(servedBy.username)) { + agentFirstRespTime.set(servedBy.username, Math.min(agentFirstRespTime.get(servedBy.username), metrics.response.ft)); + } else { + agentFirstRespTime.set(servedBy.username, metrics.response.ft); + } + } + }); - data.data.push({ - name: key, - value: avg, - }); + agentFirstRespTime.forEach((value, key) => { + // calculate avg + data.data.push({ + name: key, + value: value.toFixed(2), }); + }); - this.sortByValue(data.data, true); // reverse sort array + this.sortByValue(data.data, false); // sort array - data.data.forEach((obj) => { - obj.value = secondsToHHMMSS(obj.value); - }); + data.data.forEach((obj) => { + obj.value = secondsToHHMMSS(obj.value); + }); - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Total_messages(from, to, departmentId, extraQuery) { - const agentMessages = new Map(); // stores total conversations for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; + return data; + }, - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Total_messages', - }, - ], - data: [], - }; + /** + * + * @param {Date} from + * @param {Date} to + * + * @returns {Array(Object), Array(Object)} + */ + async Avg_response_time(from, to, departmentId, extraQuery) { + const agentAvgRespTime = new Map(); // stores avg response time for each agent + const date = { + gte: from, + lt: to.add(1, 'days'), + }; + + const data = { + head: [ + { + name: 'Agent', + }, + { + name: 'Avg_response_time', + }, + ], + data: [], + }; - // we don't want to count visitor messages - const extraFilter = { $lte: ['$token', null] }; - const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages( - 'l', - date, - { departmentId }, - extraFilter, - extraQuery, - ).toArray(); - allConversations.map(({ servedBy, msgs }) => { - if (servedBy) { - this.updateMap(agentMessages, servedBy.username, msgs); + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { + if (servedBy && metrics && metrics.response && metrics.response.avg) { + if (agentAvgRespTime.has(servedBy.username)) { + agentAvgRespTime.set(servedBy.username, { + avg: agentAvgRespTime.get(servedBy.username).avg + metrics.response.avg, + total: agentAvgRespTime.get(servedBy.username).total + 1, + }); + } else { + agentAvgRespTime.set(servedBy.username, { + avg: metrics.response.avg, + total: 1, + }); } - return null; - }); + } + }); - agentMessages.forEach((value, key) => { - // calculate percentage - data.data.push({ - name: key, - value, - }); + agentAvgRespTime.forEach((obj, key) => { + // calculate avg + const avg = obj.avg / obj.total; + + data.data.push({ + name: key, + value: avg.toFixed(2), }); + }); - this.sortByValue(data.data, true); // reverse sort array + this.sortByValue(data.data, false); // sort array - return data; - }, + data.data.forEach((obj) => { + obj.value = secondsToHHMMSS(obj.value); + }); - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Avg_first_response_time(from, to, departmentId, extraQuery) { - const agentAvgRespTime = new Map(); // stores avg response time for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; + return data; + }, - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Avg_first_response_time', - }, - ], - data: [], - }; + /** + * + * @param {Date} from + * @param {Date} to + * + * @returns {Array(Object), Array(Object)} + */ + async Avg_reaction_time(from, to, departmentId, extraQuery) { + const agentAvgReactionTime = new Map(); // stores avg reaction time for each agent + const date = { + gte: from, + lt: to.add(1, 'days'), + }; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.response && metrics.response.ft) { - if (agentAvgRespTime.has(servedBy.username)) { - agentAvgRespTime.set(servedBy.username, { - frt: agentAvgRespTime.get(servedBy.username).frt + metrics.response.ft, - total: agentAvgRespTime.get(servedBy.username).total + 1, - }); - } else { - agentAvgRespTime.set(servedBy.username, { - frt: metrics.response.ft, - total: 1, - }); - } + const data = { + head: [ + { + name: 'Agent', + }, + { + name: 'Avg_reaction_time', + }, + ], + data: [], + }; + + await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { + if (servedBy && metrics && metrics.reaction && metrics.reaction.ft) { + if (agentAvgReactionTime.has(servedBy.username)) { + agentAvgReactionTime.set(servedBy.username, { + frt: agentAvgReactionTime.get(servedBy.username).frt + metrics.reaction.ft, + total: agentAvgReactionTime.get(servedBy.username).total + 1, + }); + } else { + agentAvgReactionTime.set(servedBy.username, { + frt: metrics.reaction.ft, + total: 1, + }); } - }); + } + }); - agentAvgRespTime.forEach((obj, key) => { - // calculate avg - const avg = obj.frt / obj.total; + agentAvgReactionTime.forEach((obj, key) => { + // calculate avg + const avg = obj.frt / obj.total; - data.data.push({ - name: key, - value: avg.toFixed(2), - }); + data.data.push({ + name: key, + value: avg.toFixed(2), }); + }); - this.sortByValue(data.data, false); // sort array + this.sortByValue(data.data, false); // sort array - data.data.forEach((obj) => { - obj.value = secondsToHHMMSS(obj.value); - }); + data.data.forEach((obj) => { + obj.value = secondsToHHMMSS(obj.value); + }); - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Best_first_response_time(from, to, departmentId, extraQuery) { - const agentFirstRespTime = new Map(); // stores avg response time for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; + return data; + }, +}; - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Best_first_response_time', - }, - ], - data: [], - }; +export const Analytics = { + async getAgentOverviewData(options) { + const { departmentId, utcOffset, daterange: { from: fDate, to: tDate } = {}, chartOptions: { name } = {} } = options; + const timezone = getTimezone({ utcOffset }); + const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); + const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.response && metrics.response.ft) { - if (agentFirstRespTime.has(servedBy.username)) { - agentFirstRespTime.set(servedBy.username, Math.min(agentFirstRespTime.get(servedBy.username), metrics.response.ft)); - } else { - agentFirstRespTime.set(servedBy.username, metrics.response.ft); - } - } - }); + if (!(moment(from).isValid() && moment(to).isValid())) { + logger.error('livechat:getAgentOverviewData => Invalid dates'); + return; + } - agentFirstRespTime.forEach((value, key) => { - // calculate avg - data.data.push({ - name: key, - value: value.toFixed(2), - }); - }); + if (!AgentOverviewData[name]) { + logger.error(`Method RocketChat.Livechat.Analytics.AgentOverviewData.${name} does NOT exist`); + return; + } - this.sortByValue(data.data, false); // sort array + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + return AgentOverviewData[name](from, to, departmentId, extraQuery); + }, - data.data.forEach((obj) => { - obj.value = secondsToHHMMSS(obj.value); - }); + async getAnalyticsChartData(options) { + const { + utcOffset, + departmentId, + daterange: { from: fDate, to: tDate } = {}, + chartOptions: { name: chartLabel }, + chartOptions: { name } = {}, + } = options; - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Avg_response_time(from, to, departmentId, extraQuery) { - const agentAvgRespTime = new Map(); // stores avg response time for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; + // Check if function exists, prevent server error in case property altered + if (!ChartData[name]) { + logger.error(`Method RocketChat.Livechat.Analytics.ChartData.${name} does NOT exist`); + return; + } - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Avg_response_time', - }, - ], - data: [], - }; + const timezone = getTimezone({ utcOffset }); + const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); + const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); + const isSameDay = from.diff(to, 'days') === 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.response && metrics.response.avg) { - if (agentAvgRespTime.has(servedBy.username)) { - agentAvgRespTime.set(servedBy.username, { - avg: agentAvgRespTime.get(servedBy.username).avg + metrics.response.avg, - total: agentAvgRespTime.get(servedBy.username).total + 1, - }); - } else { - agentAvgRespTime.set(servedBy.username, { - avg: metrics.response.avg, - total: 1, - }); - } - } - }); + if (!(moment(from).isValid() && moment(to).isValid())) { + logger.error('livechat:getAnalyticsChartData => Invalid dates'); + return; + } - agentAvgRespTime.forEach((obj, key) => { - // calculate avg - const avg = obj.avg / obj.total; + const data = { + chartLabel, + dataLabels: [], + dataPoints: [], + }; - data.data.push({ - name: key, - value: avg.toFixed(2), - }); - }); + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + if (isSameDay) { + // data for single day + const m = moment(from); + for await (const currentHour of Array.from({ length: HOURS_IN_DAY }, (_, i) => i)) { + const hour = m.add(currentHour ? 1 : 0, 'hour').format('H'); + const label = { + from: moment.utc().set({ hour }).tz(timezone).format('hA'), + to: moment.utc().set({ hour }).add(1, 'hour').tz(timezone).format('hA'), + }; + data.dataLabels.push(`${label.from}-${label.to}`); - this.sortByValue(data.data, false); // sort array + const date = { + gte: m, + lt: moment(m).add(1, 'hours'), + }; - data.data.forEach((obj) => { - obj.value = secondsToHHMMSS(obj.value); - }); + data.dataPoints.push(await ChartData[name](date, departmentId, extraQuery)); + } + } else { + for await (const m of dayIterator(from, to)) { + data.dataLabels.push(m.format('M/D')); - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Avg_reaction_time(from, to, departmentId, extraQuery) { - const agentAvgReactionTime = new Map(); // stores avg reaction time for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; + const date = { + gte: m, + lt: moment(m).add(1, 'days'), + }; - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Avg_reaction_time', - }, - ], - data: [], - }; + data.dataPoints.push(await ChartData[name](date, departmentId, extraQuery)); + } + } - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.reaction && metrics.reaction.ft) { - if (agentAvgReactionTime.has(servedBy.username)) { - agentAvgReactionTime.set(servedBy.username, { - frt: agentAvgReactionTime.get(servedBy.username).frt + metrics.reaction.ft, - total: agentAvgReactionTime.get(servedBy.username).total + 1, - }); - } else { - agentAvgReactionTime.set(servedBy.username, { - frt: metrics.reaction.ft, - total: 1, - }); - } - } - }); + return data; + }, - agentAvgReactionTime.forEach((obj, key) => { - // calculate avg - const avg = obj.frt / obj.total; + async getAnalyticsOverviewData(options) { + const { departmentId, utcOffset = 0, language, daterange: { from: fDate, to: tDate } = {}, analyticsOptions: { name } = {} } = options; + const timezone = getTimezone({ utcOffset }); + const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); + const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - data.data.push({ - name: key, - value: avg.toFixed(2), - }); - }); + if (!(moment(from).isValid() && moment(to).isValid())) { + logger.error('livechat:getAnalyticsOverviewData => Invalid dates'); + return; + } - this.sortByValue(data.data, false); // sort array + if (!OverviewData[name]) { + logger.error(`Method RocketChat.Livechat.Analytics.OverviewData.${name} does NOT exist`); + return; + } - data.data.forEach((obj) => { - obj.value = secondsToHHMMSS(obj.value); - }); + const t = (s) => i18n.t(s, { lng: language }); - return data; - }, + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + return OverviewData[name](from, to, departmentId, timezone, t, extraQuery); }, }; diff --git a/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts b/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts new file mode 100644 index 00000000000..20717d6e02d --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts @@ -0,0 +1,12 @@ +import mem from 'mem'; + +import { Analytics } from './Analytics'; + +export const getAgentOverviewDataCached = mem(Analytics.getAgentOverviewData, { maxAge: 60000, cacheKey: JSON.stringify }); +// Agent overview data on realtime is cached for 5 seconds +// while the data on the overview page is cached for 1 minute +export const getAnalyticsOverviewDataCached = mem(Analytics.getAnalyticsOverviewData, { maxAge: 60000, cacheKey: JSON.stringify }); +export const getAnalyticsOverviewDataCachedForRealtime = mem(Analytics.getAnalyticsOverviewData, { + maxAge: 5000, + cacheKey: JSON.stringify, +}); diff --git a/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts b/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts index cb7ecbf079f..6788d01c444 100644 --- a/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts +++ b/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts @@ -1,9 +1,11 @@ import type { IUser } from '@rocket.chat/core-typings'; import { LivechatRooms, Users, LivechatVisitors, LivechatAgentActivity } from '@rocket.chat/models'; +import mem from 'mem'; import moment from 'moment'; import { secondsToHHMMSS } from '../../../../../lib/utils/secondsToHHMMSS'; import { settings } from '../../../../settings/server'; +import { getAnalyticsOverviewDataCachedForRealtime } from '../AnalyticsTyped'; import { Livechat } from '../Livechat'; import { findPercentageOfAbandonedRoomsAsync, @@ -13,15 +15,7 @@ import { findAllAverageServiceTimeAsync, } from './departments'; -export const findAllChatsStatusAsync = async ({ - start, - end, - departmentId = undefined, -}: { - start: Date; - end: Date; - departmentId?: string; -}) => { +const findAllChatsStatusAsync = async ({ start, end, departmentId = undefined }: { start: Date; end: Date; departmentId?: string }) => { if (!start || !end) { throw new Error('"start" and "end" must be provided'); } @@ -33,7 +27,7 @@ export const findAllChatsStatusAsync = async ({ }; }; -export const getProductivityMetricsAsync = async ({ +const getProductivityMetricsAsync = async ({ start, end, departmentId = undefined, @@ -78,7 +72,7 @@ export const getProductivityMetricsAsync = async ({ }; }; -export const getAgentsProductivityMetricsAsync = async ({ +const getAgentsProductivityMetricsAsync = async ({ start, end, departmentId = undefined, @@ -148,7 +142,7 @@ export const getAgentsProductivityMetricsAsync = async ({ }; }; -export const getChatsMetricsAsync = async ({ start, end, departmentId = undefined }: { start: Date; end: Date; departmentId?: string }) => { +const getChatsMetricsAsync = async ({ start, end, departmentId = undefined }: { start: Date; end: Date; departmentId?: string }) => { if (!start || !end) { throw new Error('"start" and "end" must be provided'); } @@ -223,7 +217,7 @@ export const getChatsMetricsAsync = async ({ start, end, departmentId = undefine }; }; -export const getConversationsMetricsAsync = async ({ +const getConversationsMetricsAsync = async ({ start, end, departmentId, @@ -237,7 +231,7 @@ export const getConversationsMetricsAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } - const totalizers = await Livechat.Analytics.getAnalyticsOverviewData({ + const totalizers = await getAnalyticsOverviewDataCachedForRealtime({ daterange: { from: start, to: end, @@ -263,7 +257,7 @@ export const getConversationsMetricsAsync = async ({ }; }; -export const findAllChatMetricsByAgentAsync = async ({ +const findAllChatMetricsByAgentAsync = async ({ start, end, departmentId = undefined, @@ -311,10 +305,10 @@ export const findAllChatMetricsByAgentAsync = async ({ return result; }; -export const findAllAgentsStatusAsync = async ({ departmentId = undefined }: { departmentId?: string }) => +const findAllAgentsStatusAsync = async ({ departmentId = undefined }: { departmentId?: string }) => (await Users.countAllAgentsStatus({ departmentId }))[0]; -export const findAllChatMetricsByDepartmentAsync = async ({ +const findAllChatMetricsByDepartmentAsync = async ({ start, end, departmentId = undefined, @@ -349,7 +343,7 @@ export const findAllChatMetricsByDepartmentAsync = async ({ return result; }; -export const findAllResponseTimeMetricsAsync = async ({ +const findAllResponseTimeMetricsAsync = async ({ start, end, departmentId = undefined, @@ -380,3 +374,16 @@ export const findAllResponseTimeMetricsAsync = async ({ }, }; }; + +export const getConversationsMetricsAsyncCached = mem(getConversationsMetricsAsync, { maxAge: 5000, cacheKey: JSON.stringify }); +export const getAgentsProductivityMetricsAsyncCached = mem(getAgentsProductivityMetricsAsync, { maxAge: 5000, cacheKey: JSON.stringify }); +export const getChatsMetricsAsyncCached = mem(getChatsMetricsAsync, { maxAge: 5000, cacheKey: JSON.stringify }); +export const getProductivityMetricsAsyncCached = mem(getProductivityMetricsAsync, { maxAge: 5000, cacheKey: JSON.stringify }); +export const findAllChatsStatusAsyncCached = mem(findAllChatsStatusAsync, { maxAge: 5000, cacheKey: JSON.stringify }); +export const findAllChatMetricsByAgentAsyncCached = mem(findAllChatMetricsByAgentAsync, { maxAge: 5000, cacheKey: JSON.stringify }); +export const findAllAgentsStatusAsyncCached = mem(findAllAgentsStatusAsync, { maxAge: 5000, cacheKey: JSON.stringify }); +export const findAllChatMetricsByDepartmentAsyncCached = mem(findAllChatMetricsByDepartmentAsync, { + maxAge: 5000, + cacheKey: JSON.stringify, +}); +export const findAllResponseTimeMetricsAsyncCached = mem(findAllResponseTimeMetricsAsync, { maxAge: 5000, cacheKey: JSON.stringify });