From e30af6171e9906cb57997400d61010a87134dc27 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Wed, 2 Sep 2020 14:53:06 -0300 Subject: [PATCH] Refactor: Omnichannel Realtime Monitoring (#18666) Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> --- app/livechat/client/route.js | 8 - app/livechat/client/views/admin.js | 1 - .../analytics/livechatRealTimeMonitoring.html | 164 --------- .../analytics/livechatRealTimeMonitoring.js | 343 ------------------ client/helpers/getDateRange.js | 9 + client/omnichannel/DepartmentAutoComplete.js | 24 ++ .../RealTimeMonitoringPage.js | 109 ++++++ .../charts/AgentStatusChart.js | 72 ++++ .../realTimeMonitoring/charts/Chart.js | 17 + .../charts/ChatDurationChart.js | 71 ++++ .../realTimeMonitoring/charts/ChatsChart.js | 73 ++++ .../charts/ChatsPerAgentChart.js | 64 ++++ .../charts/ChatsPerDepartmentChart.js | 64 ++++ .../charts/ResponseTimesChart.js | 79 ++++ .../charts/getMomentChartLabelsAndData.js | 14 + .../charts/getMomentCurrentLabel.js | 8 + .../charts/useUpdateChartData.js | 10 + .../counter/CounterContainer.js | 29 ++ .../realTimeMonitoring/counter/CounterItem.js | 20 + .../realTimeMonitoring/counter/CounterRow.js | 23 ++ .../counter/CounterRow.stories.js | 16 + .../overviews/AgentsOverview.js | 28 ++ .../overviews/ChatsOverview.js | 23 ++ .../overviews/ConversationOverview.js | 29 ++ .../overviews/ProductivityOverview.js | 27 ++ client/omnichannel/routes.js | 5 + client/omnichannel/sidebarItems.js | 2 +- packages/rocketchat-i18n/i18n/en.i18n.json | 1 + 28 files changed, 816 insertions(+), 517 deletions(-) delete mode 100644 app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.html delete mode 100644 app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.js create mode 100644 client/helpers/getDateRange.js create mode 100644 client/omnichannel/DepartmentAutoComplete.js create mode 100644 client/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js create mode 100644 client/omnichannel/realTimeMonitoring/charts/AgentStatusChart.js create mode 100644 client/omnichannel/realTimeMonitoring/charts/Chart.js create mode 100644 client/omnichannel/realTimeMonitoring/charts/ChatDurationChart.js create mode 100644 client/omnichannel/realTimeMonitoring/charts/ChatsChart.js create mode 100644 client/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js create mode 100644 client/omnichannel/realTimeMonitoring/charts/ChatsPerDepartmentChart.js create mode 100644 client/omnichannel/realTimeMonitoring/charts/ResponseTimesChart.js create mode 100644 client/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.js create mode 100644 client/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.js create mode 100644 client/omnichannel/realTimeMonitoring/charts/useUpdateChartData.js create mode 100644 client/omnichannel/realTimeMonitoring/counter/CounterContainer.js create mode 100644 client/omnichannel/realTimeMonitoring/counter/CounterItem.js create mode 100644 client/omnichannel/realTimeMonitoring/counter/CounterRow.js create mode 100644 client/omnichannel/realTimeMonitoring/counter/CounterRow.stories.js create mode 100644 client/omnichannel/realTimeMonitoring/overviews/AgentsOverview.js create mode 100644 client/omnichannel/realTimeMonitoring/overviews/ChatsOverview.js create mode 100644 client/omnichannel/realTimeMonitoring/overviews/ConversationOverview.js create mode 100644 client/omnichannel/realTimeMonitoring/overviews/ProductivityOverview.js diff --git a/app/livechat/client/route.js b/app/livechat/client/route.js index 98be3912c97..2e1b970d2b7 100644 --- a/app/livechat/client/route.js +++ b/app/livechat/client/route.js @@ -26,14 +26,6 @@ AccountBox.addRoute({ pageTemplate: 'livechatAnalytics', }, livechatManagerRoutes, load); -AccountBox.addRoute({ - name: 'livechat-real-time-monitoring', - path: '/real-time-monitoring', - sideNav: 'omnichannelFlex', - i18nPageTitle: 'Real_Time_Monitoring', - pageTemplate: 'livechatRealTimeMonitoring', -}, livechatManagerRoutes, load); - AccountBox.addRoute({ name: 'livechat-departments', path: '/departments', diff --git a/app/livechat/client/views/admin.js b/app/livechat/client/views/admin.js index 495de3e9d6d..0d27714086d 100644 --- a/app/livechat/client/views/admin.js +++ b/app/livechat/client/views/admin.js @@ -1,7 +1,6 @@ import './app/analytics/livechatAnalytics'; import './app/analytics/livechatAnalyticsCustomDaterange'; import './app/analytics/livechatAnalyticsDaterange'; -import './app/analytics/livechatRealTimeMonitoring'; import './app/livechatDashboard.html'; import './app/livechatDepartmentForm'; import './app/livechatDepartments'; diff --git a/app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.html b/app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.html deleted file mode 100644 index 04818769e35..00000000000 --- a/app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.html +++ /dev/null @@ -1,164 +0,0 @@ -<template name="livechatRealTimeMonitoring"> - {{#requiresPermission 'view-livechat-real-time-monitoring'}} - <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> - {{#if hasDepartments }} - <div class="form-group "> - {{> livechatAutocompleteUser - onClickTag=onClickTagDepartment - list=selectedDepartments - onSelect=onSelectDepartments - collection='CachedDepartmentList' - endpoint='livechat/department.autocomplete' - field='name' - sort='name' - placeholder="Select_a_department" - name="department" - icon="queue" - noMatchTemplate="userSearchEmpty" - templateItem="popupList_item_channel" - template="roomSearch" - noMatchTemplate="roomSearchEmpty" - modifier=departmentModifier - }} - </div> - {{/if}} - </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> - </div> - {{/if}} - <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"> - <canvas id="lc-chats-chart"></canvas> - </div> - </div> - </div> - <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"> - <canvas id="lc-agents-chart"></canvas> - </div> - </div> - </div> - <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"> - <canvas id="lc-chat-duration-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 timingOverview}} - <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="lc-monitoring-chart-container"> - <canvas id="lc-reaction-response-times-chart"></canvas> - </div> - </div> - </div> - {{/requiresPermission}} -</template> diff --git a/app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.js b/app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.js deleted file mode 100644 index 584e0f003b1..00000000000 --- a/app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.js +++ /dev/null @@ -1,343 +0,0 @@ -import { Template } from 'meteor/templating'; -import moment from 'moment'; -import { ReactiveVar } from 'meteor/reactive-var'; - -import { drawLineChart, drawDoughnutChart, updateChart } from '../../../lib/chartHandler'; -import { APIClient } from '../../../../../utils/client'; -import './livechatRealTimeMonitoring.html'; - -const chartContexts = {}; // stores context of current chart, used to clean when redrawing -let templateInstance; - -const initChart = { - 'lc-chats-chart'() { - return drawDoughnutChart( - document.getElementById('lc-chats-chart'), - 'Chats', - chartContexts['lc-chats-chart'], - ['Open', 'Queue', 'Closed'], [0, 0, 0]); - }, - - 'lc-agents-chart'() { - return drawDoughnutChart( - document.getElementById('lc-agents-chart'), - 'Agents', - chartContexts['lc-agents-chart'], - ['Available', 'Away', 'Busy', 'Offline'], [0, 0, 0, 0]); - }, - - 'lc-chats-per-agent-chart'() { - return drawLineChart( - document.getElementById('lc-chats-per-agent-chart'), - chartContexts['lc-chats-per-agent-chart'], - ['Open', 'Closed'], - [], [[], []], { legends: true, anim: true, smallTicks: true }); - }, - - 'lc-chats-per-dept-chart'() { - if (!document.getElementById('lc-chats-per-dept-chart')) { - return null; - } - - return drawLineChart( - document.getElementById('lc-chats-per-dept-chart'), - chartContexts['lc-chats-per-dept-chart'], - ['Open', 'Closed'], - [], [[], []], { legends: true, anim: true, smallTicks: true }); - }, - - 'lc-reaction-response-times-chart'() { - const timingLabels = []; - const initData = []; - const today = moment().startOf('day'); - for (let m = today; m.diff(moment(), 'hours') < 0; m.add(1, 'hours')) { - const hour = m.format('H'); - timingLabels.push(`${ moment(hour, ['H']).format('hA') }-${ moment((parseInt(hour) + 1) % 24, ['H']).format('hA') }`); - initData.push(0); - } - - return drawLineChart( - document.getElementById('lc-reaction-response-times-chart'), - chartContexts['lc-reaction-response-times-chart'], - ['Avg_reaction_time', 'Longest_reaction_time', 'Avg_response_time', 'Longest_response_time'], - timingLabels.slice(), - [initData.slice(), initData.slice(), initData.slice(), initData.slice()], { legends: true, anim: true, smallTicks: true }); - }, - - 'lc-chat-duration-chart'() { - const timingLabels = []; - const initData = []; - const today = moment().startOf('day'); - for (let m = today; m.diff(moment(), 'hours') < 0; m.add(1, 'hours')) { - const hour = m.format('H'); - timingLabels.push(`${ moment(hour, ['H']).format('hA') }-${ moment((parseInt(hour) + 1) % 24, ['H']).format('hA') }`); - initData.push(0); - } - - return drawLineChart( - document.getElementById('lc-chat-duration-chart'), - chartContexts['lc-chat-duration-chart'], - ['Avg_chat_duration', 'Longest_chat_duration'], - timingLabels.slice(), - [initData.slice(), initData.slice()], { legends: true, anim: true, smallTicks: true }); - }, -}; - -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 = async (chartId, label, data) => { - if (!chartContexts[chartId]) { - chartContexts[chartId] = await initChart[chartId](); - } - - await updateChart(chartContexts[chartId], label, data); -}; - -let timer; - -const getChartDepartment = (department) => department?._id; - -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 parseAdditionalParams = (options = {}, prefix = '') => `${ prefix }${ Object.keys(options).map((key) => `${ key }=${ options[key] }`).join('&') }`; - -const loadConversationOverview = async ({ start, end, ...options }) => { - const additionalParams = parseAdditionalParams(options, '&'); - const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/conversation-totalizers?start=${ start }&end=${ end }${ additionalParams }`); - return totalizers; -}; - -const updateConversationOverview = async (totalizers) => { - if (totalizers && Array.isArray(totalizers)) { - templateInstance.conversationsOverview.set(totalizers); - } -}; - -const loadAgentsOverview = async ({ start, end, ...options }) => { - const additionalParams = parseAdditionalParams(options, '&'); - const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/agents-productivity-totalizers?start=${ start }&end=${ end }${ additionalParams }`); - return totalizers; -}; - -const updateAgentsOverview = async (totalizers) => { - if (totalizers && Array.isArray(totalizers)) { - templateInstance.agentsOverview.set(totalizers); - } -}; -const loadChatsOverview = async ({ start, end, ...options }) => { - const additionalParams = parseAdditionalParams(options, '&'); - const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/chats-totalizers?start=${ start }&end=${ end }${ additionalParams }`); - return totalizers; -}; - -const updateChatsOverview = async (totalizers) => { - if (totalizers && Array.isArray(totalizers)) { - templateInstance.chatsOverview.set(totalizers); - } -}; - -const loadProductivityOverview = async ({ start, end, ...options }) => { - const additionalParams = parseAdditionalParams(options, '&'); - const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/productivity-totalizers?start=${ start }&end=${ end }${ additionalParams }`); - return totalizers; -}; - -const updateProductivityOverview = async (totalizers) => { - if (totalizers && Array.isArray(totalizers)) { - templateInstance.timingOverview.set(totalizers); - } -}; - -const loadChatsChartData = ({ start, end, ...options }) => { - const additionalParams = parseAdditionalParams(options, '&'); - return APIClient.v1.get(`livechat/analytics/dashboards/charts/chats?start=${ start }&end=${ end }${ additionalParams }`); -}; - -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 loadChatsPerAgentChartData = async ({ start, end, ...options }) => { - const additionalParams = parseAdditionalParams(options, '&'); - const result = await APIClient.v1.get(`livechat/analytics/dashboards/charts/chats-per-agent?start=${ start }&end=${ end }${ additionalParams }`); - delete result.success; - return result; -}; - -const updateChatsPerAgentChart = async (agents) => { - // this chart need to reset before new updates - chartContexts['lc-chats-per-agent-chart'] = await initChart['lc-chats-per-agent-chart'](); - - Object - .keys(agents) - .forEach((agent) => updateChartData('lc-chats-per-agent-chart', agent, [agents[agent].open, agents[agent].closed])); -}; - -const loadAgentsStatusChartData = ({ departmentId }) => { - const additionalParams = parseAdditionalParams({ departmentId }, '?'); - return APIClient.v1.get(`livechat/analytics/dashboards/charts/agents-status${ additionalParams }`); -}; - -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 loadChatsPerDepartmentChartData = async ({ start, end, ...options }) => { - const additionalParams = parseAdditionalParams(options, '&'); - const result = await APIClient.v1.get(`livechat/analytics/dashboards/charts/chats-per-department?start=${ start }&end=${ end }${ additionalParams }`); - delete result.success; - return result; -}; - -const updateDepartmentsChart = async (departments) => { - // this chart need to reset before new updates - chartContexts['lc-chats-per-dept-chart'] = await initChart['lc-chats-per-dept-chart'](); - - Object - .keys(departments) - .forEach((department) => updateChartData('lc-chats-per-dept-chart', department, [departments[department].open, departments[department].closed])); -}; - -const loadTimingsChartData = ({ start, end, ...options }) => { - const additionalParams = parseAdditionalParams(options, '&'); - return APIClient.v1.get(`livechat/analytics/dashboards/charts/timings?start=${ start }&end=${ end }${ additionalParams }`); -}; - -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({ - selected(value) { - return value === templateInstance.analyticsOptions.get().value || value === templateInstance.chartOptions.get().value ? 'selected' : false; - }, - conversationsOverview() { - return templateInstance.conversationsOverview.get(); - }, - timingOverview() { - return templateInstance.timingOverview.get(); - }, - agentsOverview() { - return templateInstance.agentsOverview.get(); - }, - chatsOverview() { - return templateInstance.chatsOverview.get(); - }, - isLoading() { - return Template.instance().isLoading.get(); - }, - departmentModifier() { - return (filter, text = '') => { - const f = filter.get(); - return `${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `<strong>${ part }</strong>`) }`; - }; - }, - onClickTagDepartment() { - return Template.instance().onClickTagDepartment; - }, - selectedDepartments() { - return Template.instance().selectedDepartments.get(); - }, - onSelectDepartments() { - return Template.instance().onSelectDepartments; - }, - hasDepartments() { - return Template.instance().hasDepartments.get(); - }, -}); - -Template.livechatRealTimeMonitoring.onCreated(async function() { - templateInstance = Template.instance(); - this.isLoading = new ReactiveVar(false); - this.conversationsOverview = new ReactiveVar(); - this.timingOverview = new ReactiveVar(); - this.chatsOverview = new ReactiveVar(); - this.agentsOverview = new ReactiveVar(); - this.conversationTotalizers = new ReactiveVar([]); - this.interval = new ReactiveVar(5); - this.selectedDepartments = new ReactiveVar([]); - this.hasDepartments = new ReactiveVar(false); - - this.onSelectDepartments = ({ item: department }) => { - department.text = department.name; - this.selectedDepartments.set([department]); - }; - - this.onClickTagDepartment = () => { - this.selectedDepartments.set([]); - }; - - const { departments } = await APIClient.v1.get('livechat/department?count=1'); - this.hasDepartments.set(departments?.length > 0); -}); - -Template.livechatRealTimeMonitoring.onRendered(async function() { - await initAllCharts(); - - this.updateDashboard = async () => { - const [department] = this.selectedDepartments.get(); - const departmentId = getChartDepartment(department); - const daterange = getDaterange(); - const filters = Object.assign( - { ...daterange }, - departmentId && { departmentId }, - ); - - updateConversationOverview(await loadConversationOverview(filters)); - updateProductivityOverview(await loadProductivityOverview(filters)); - updateChatsChart(await loadChatsChartData(filters)); - updateChatsPerAgentChart(await loadChatsPerAgentChartData(filters)); - updateAgentStatusChart(await loadAgentsStatusChartData(filters)); - updateDepartmentsChart(await loadChatsPerDepartmentChartData(filters)); - updateTimingsChart(await loadTimingsChartData(filters)); - updateAgentsOverview(await loadAgentsOverview(filters)); - updateChatsOverview(await loadChatsOverview(filters)); - }; - this.autorun(() => { - if (timer) { - clearInterval(timer); - } - timer = setInterval(() => this.updateDashboard(), getIntervalInMS()); - }); - this.isLoading.set(true); - await this.updateDashboard(); - this.isLoading.set(false); -}); - -Template.livechatRealTimeMonitoring.events({ - 'change .js-interval': (event, instance) => { - instance.interval.set(event.target.value); - }, -}); - -Template.livechatRealTimeMonitoring.onDestroyed(function() { - clearInterval(timer); -}); diff --git a/client/helpers/getDateRange.js b/client/helpers/getDateRange.js new file mode 100644 index 00000000000..84dd9ce73fc --- /dev/null +++ b/client/helpers/getDateRange.js @@ -0,0 +1,9 @@ +import moment from 'moment'; + +export 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`, + }; +}; diff --git a/client/omnichannel/DepartmentAutoComplete.js b/client/omnichannel/DepartmentAutoComplete.js new file mode 100644 index 00000000000..499e62b21a3 --- /dev/null +++ b/client/omnichannel/DepartmentAutoComplete.js @@ -0,0 +1,24 @@ +import React, { useMemo, useState } from 'react'; +import { AutoComplete, Option, Icon } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; + +import { useEndpointDataExperimental } from '../hooks/useEndpointDataExperimental'; + +const query = (term = '') => ({ selector: JSON.stringify({ term }) }); + +const DepartmentAutoComplete = React.memo((props) => { + const [filter, setFilter] = useState(''); + const { data } = useEndpointDataExperimental('livechat/department.autocomplete', useMemo(() => query(filter), [filter])); + const options = useMemo(() => (data && data.items.map((department) => ({ value: department._id, label: department.name }))) || [], [data]); + const onClickRemove = useMutableCallback(() => props.onChange('')); + return <AutoComplete + {...props} + filter={filter} + setFilter={setFilter} + renderSelected={({ value, label, ...props }) => <Option key={value} {...props} onClick={onClickRemove}>{label}<Icon name='cross' /></Option>} + renderItem={({ value, label, ...props }) => <Option key={value} {...props} >{label}</Option>} + options={ options } + />; +}); + +export default DepartmentAutoComplete; diff --git a/client/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js b/client/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js new file mode 100644 index 00000000000..7e991f74f07 --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js @@ -0,0 +1,109 @@ +import React, { useRef, useState, useMemo, useEffect } from 'react'; +import { Box, Select, Field, Margins } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; + +import Page from '../../components/basic/Page'; +import ChatsChart from './charts/ChatsChart'; +import ChatsPerAgentChart from './charts/ChatsPerAgentChart'; +import AgentStatusChart from './charts/AgentStatusChart'; +import ChatsPerDepartmentChart from './charts/ChatsPerDepartmentChart'; +import ChatDurationChart from './charts/ChatDurationChart'; +import ResponseTimesChart from './charts/ResponseTimesChart'; +import ConversationOverview from './overviews/ConversationOverview'; +import AgentsOverview from './overviews/AgentsOverview'; +import ChatsOverview from './overviews/ChatsOverview'; +import ProductivityOverview from './overviews/ProductivityOverview'; +import DepartmentAutoComplete from '../DepartmentAutoComplete'; +import { getDateRange } from '../../helpers/getDateRange'; +import { useTranslation } from '../../contexts/TranslationContext'; + +const dateRange = getDateRange(); + +const RealTimeMonitoringPage = () => { + const t = useTranslation(); + + const [reloadFrequency, setReloadFrequency] = useState(5); + const [department, setDepartment] = useState(''); + + const reloadRef = useRef({}); + + const departmentParams = useMemo(() => ({ + ...department && { departmentId: department }, + }), [department]); + + const allParams = useMemo(() => ({ + ...departmentParams, + ...dateRange, + }), [departmentParams]); + + const reloadCharts = useMutableCallback(() => { + Object.values(reloadRef.current).forEach((reload) => { + reload(); + }); + }); + + useEffect(() => { + const interval = setInterval(reloadCharts, reloadFrequency * 1000); + return () => { + clearInterval(interval); + }; + }, [reloadCharts, reloadFrequency]); + + const reloadOptions = useMemo(() => [ + [5, <>5 {t('seconds')}</>], + [10, <>10 {t('seconds')}</>], + [30, <>30 {t('seconds')}</>], + [60, <>1 {t('minute')}</>], + ], [t]); + + return <Page> + <Page.Header title={t('Real_Time_Monitoring')}> + </Page.Header> + <Page.ScrollableContentWithShadow> + <Margins block='x4'> + <Box flexDirection='row' display='flex' justifyContent='space-between' alignSelf='center' w='full'> + <Field mie='x4'> + <Field.Label>{t('Department')}</Field.Label> + <Field.Row> + <DepartmentAutoComplete placeholder={t('All')} value={department} onChange={setDepartment}/> + </Field.Row> + </Field> + <Field mis='x4'> + <Field.Label>{t('Update_every')}</Field.Label> + <Field.Row> + <Select options={reloadOptions} onChange={useMutableCallback((val) => setReloadFrequency(val))} value={reloadFrequency}/> + </Field.Row> + </Field> + </Box> + <Box display='flex' flexDirection='row' w='full' alignItems='stretch' flexShrink={1}> + <ConversationOverview flexGrow={1} flexShrink={1} width='50%' reloadRef={reloadRef} params={allParams}/> + </Box> + <Box display='flex' flexDirection='row' w='full' alignItems='stretch' flexShrink={1}> + <ChatsChart flexGrow={1} flexShrink={1} width='50%' mie='x2' reloadRef={reloadRef} params={allParams}/> + <ChatsPerAgentChart flexGrow={1} flexShrink={1} width='50%' mis='x2' reloadRef={reloadRef} params={allParams}/> + </Box> + <Box display='flex' flexDirection='row' w='full' alignItems='stretch' flexShrink={1}> + <ChatsOverview flexGrow={1} flexShrink={1} width='50%' reloadRef={reloadRef} params={allParams}/> + </Box> + <Box display='flex' flexDirection='row' w='full' alignItems='stretch' flexShrink={1}> + <AgentStatusChart flexGrow={1} flexShrink={1} width='50%' mie='x2' reloadRef={reloadRef} params={allParams}/> + <ChatsPerDepartmentChart flexGrow={1} flexShrink={1} width='50%' mis='x2' reloadRef={reloadRef} params={allParams}/> + </Box> + <Box display='flex' flexDirection='row' w='full' alignItems='stretch' flexShrink={1}> + <AgentsOverview flexGrow={1} flexShrink={1} reloadRef={reloadRef} params={allParams}/> + </Box> + <Box display='flex' w='full' flexShrink={1}> + <ChatDurationChart flexGrow={1} flexShrink={1} reloadRef={reloadRef} params={allParams}/> + </Box> + <Box display='flex' flexDirection='row' w='full' alignItems='stretch' flexShrink={1}> + <ProductivityOverview flexGrow={1} flexShrink={1} reloadRef={reloadRef} params={allParams}/> + </Box> + <Box display='flex' w='full' flexShrink={1}> + <ResponseTimesChart flexGrow={1} flexShrink={1} reloadRef={reloadRef} params={allParams}/> + </Box> + </Margins> + </Page.ScrollableContentWithShadow> + </Page>; +}; + +export default RealTimeMonitoringPage; diff --git a/client/omnichannel/realTimeMonitoring/charts/AgentStatusChart.js b/client/omnichannel/realTimeMonitoring/charts/AgentStatusChart.js new file mode 100644 index 00000000000..8a9d96b9620 --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/charts/AgentStatusChart.js @@ -0,0 +1,72 @@ +import React, { useRef, useEffect } from 'react'; + +import Chart from './Chart'; +import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import { drawDoughnutChart } from '../../../../app/livechat/client/lib/chartHandler'; +import { useUpdateChartData } from './useUpdateChartData'; + +const labels = ['Available', 'Away', 'Busy', 'Offline']; + +const initialData = { + available: 0, + away: 0, + busy: 0, + offline: 0, +}; + +const init = (canvas, context, t) => drawDoughnutChart( + canvas, + t('Agents'), + context, + labels, + Object.values(initialData), +); + +const AgentStatusChart = ({ params, reloadRef, ...props }) => { + const t = useTranslation(); + + const canvas = useRef(); + const context = useRef(); + + const updateChartData = useUpdateChartData({ + context, + canvas, + t, + init, + }); + + const { data, state, reload } = useEndpointDataExperimental( + 'livechat/analytics/dashboards/charts/agents-status', + params, + ); + + reloadRef.current.agentStatusChart = reload; + + const { + offline = 0, + available = 0, + away = 0, + busy = 0, + } = data ?? initialData; + + useEffect(() => { + const initChart = async () => { + context.current = await init(canvas.current, context.current, t); + }; + initChart(); + }, [t]); + + useEffect(() => { + if (state === ENDPOINT_STATES.DONE) { + updateChartData('Offline', [offline]); + updateChartData('Available', [available]); + updateChartData('Away', [away]); + updateChartData('Busy', [busy]); + } + }, [available, away, busy, offline, state, t, updateChartData]); + + return <Chart ref={canvas} {...props}/>; +}; + +export default AgentStatusChart; diff --git a/client/omnichannel/realTimeMonitoring/charts/Chart.js b/client/omnichannel/realTimeMonitoring/charts/Chart.js new file mode 100644 index 00000000000..19d66f066fc --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/charts/Chart.js @@ -0,0 +1,17 @@ +import React, { forwardRef } from 'react'; +import { Box } from '@rocket.chat/fuselage'; + +const style = { + minHeight: '250px', +}; +const Chart = forwardRef(function Chart(props, ref) { + return <Box + padding='x20' + height='x300' + {...props} + > + <canvas ref={ref} style={style}></canvas> + </Box>; +}); + +export default Chart; diff --git a/client/omnichannel/realTimeMonitoring/charts/ChatDurationChart.js b/client/omnichannel/realTimeMonitoring/charts/ChatDurationChart.js new file mode 100644 index 00000000000..38cfa059874 --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/charts/ChatDurationChart.js @@ -0,0 +1,71 @@ +import React, { useRef, useEffect } from 'react'; + +import Chart from './Chart'; +import { useUpdateChartData } from './useUpdateChartData'; +import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import { drawLineChart } from '../../../../app/livechat/client/lib/chartHandler'; +import { getMomentChartLabelsAndData } from './getMomentChartLabelsAndData'; +import { getMomentCurrentLabel } from './getMomentCurrentLabel'; + +const [labels, initialData] = getMomentChartLabelsAndData(); + +const init = (canvas, context, t) => drawLineChart( + canvas, + context, + [t('Avg_chat_duration'), t('Longest_chat_duration')], + labels, + [initialData, initialData], + { legends: true, anim: true, smallTicks: true }, +); + +const ChatDurationChart = ({ params, reloadRef, ...props }) => { + const t = useTranslation(); + + const canvas = useRef(); + const context = useRef(); + + const updateChartData = useUpdateChartData({ + context, + canvas, + t, + init, + }); + + const { data, state, reload } = useEndpointDataExperimental( + 'livechat/analytics/dashboards/charts/timings', + params, + ); + + reloadRef.current.chatDurationChart = reload; + + const { + chatDuration: { + avg, + longest, + }, + } = data ?? { + chatDuration: { + avg: 0, + longest: 0, + }, + }; + + useEffect(() => { + const initChart = async () => { + context.current = await init(canvas.current, context.current, t); + }; + initChart(); + }, [t]); + + useEffect(() => { + if (state === ENDPOINT_STATES.DONE) { + const label = getMomentCurrentLabel(); + updateChartData(label, [avg, longest]); + } + }, [avg, longest, state, t, updateChartData]); + + return <Chart ref={canvas} {...props}/>; +}; + +export default ChatDurationChart; diff --git a/client/omnichannel/realTimeMonitoring/charts/ChatsChart.js b/client/omnichannel/realTimeMonitoring/charts/ChatsChart.js new file mode 100644 index 00000000000..032a9439ba6 --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/charts/ChatsChart.js @@ -0,0 +1,73 @@ +import React, { useRef, useEffect } from 'react'; + +import Chart from './Chart'; +import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import { drawDoughnutChart } from '../../../../app/livechat/client/lib/chartHandler'; +import { useUpdateChartData } from './useUpdateChartData'; + +const labels = [ + 'Open', + 'Queued', + 'Closed', +]; + +const initialData = { + open: 0, + queued: 0, + closed: 0, +}; + +const init = (canvas, context, t) => drawDoughnutChart( + canvas, + t('Chats'), + context, + labels, + Object.values(initialData), +); + +const ChatsChart = ({ params, reloadRef, ...props }) => { + const t = useTranslation(); + + const canvas = useRef(); + const context = useRef(); + + const updateChartData = useUpdateChartData({ + context, + canvas, + t, + init, + }); + + const { data, state, reload } = useEndpointDataExperimental( + 'livechat/analytics/dashboards/charts/chats', + params, + ); + + reloadRef.current.chatsChart = reload; + + const { + open, + queued, + closed, + } = data ?? initialData; + + useEffect(() => { + const initChart = async () => { + context.current = await init(canvas.current, context.current, t); + }; + initChart(); + }, [t]); + + useEffect(() => { + if (state === ENDPOINT_STATES.DONE) { + updateChartData(t('Open'), [open]); + updateChartData(t('Closed'), [closed]); + updateChartData(t('Queued'), [queued]); + } + }, [closed, open, queued, state, t, updateChartData]); + + return <Chart ref={canvas} {...props}/>; +}; + +export default ChatsChart; diff --git a/client/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js b/client/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js new file mode 100644 index 00000000000..3b18b98e19e --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js @@ -0,0 +1,64 @@ +import React, { useRef, useEffect } from 'react'; + +import Chart from './Chart'; +import { useUpdateChartData } from './useUpdateChartData'; +import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import { drawLineChart } from '../../../../app/livechat/client/lib/chartHandler'; + +const initialData = { + agents: {}, +}; + +const init = (canvas, context, t) => drawLineChart( + canvas, + context, + [t('Open'), t('Closed')], + [], + [[], []], + { legends: true, anim: true, smallTicks: true }, +); + +const ChatsPerAgentChart = ({ params, reloadRef, ...props }) => { + const t = useTranslation(); + + const canvas = useRef(); + const context = useRef(); + + const updateChartData = useUpdateChartData({ + context, + canvas, + t, + init, + }); + + const { data, state, reload } = useEndpointDataExperimental( + 'livechat/analytics/dashboards/charts/chats-per-agent', + params, + ); + + reloadRef.current.chatsPerAgentChart = reload; + + const { + agents = {}, + } = data ?? initialData; + + useEffect(() => { + const initChart = async () => { + context.current = await init(canvas.current, context.current, t); + }; + initChart(); + }, [t]); + + useEffect(() => { + if (state === ENDPOINT_STATES.DONE) { + Object.entries(agents).forEach(([name, value]) => { + updateChartData(name, [value.open, value.closed]); + }); + } + }, [agents, state, t, updateChartData]); + + return <Chart ref={canvas} {...props}/>; +}; + +export default ChatsPerAgentChart; diff --git a/client/omnichannel/realTimeMonitoring/charts/ChatsPerDepartmentChart.js b/client/omnichannel/realTimeMonitoring/charts/ChatsPerDepartmentChart.js new file mode 100644 index 00000000000..493c7d9288a --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/charts/ChatsPerDepartmentChart.js @@ -0,0 +1,64 @@ +import React, { useRef, useEffect } from 'react'; + +import Chart from './Chart'; +import { useUpdateChartData } from './useUpdateChartData'; +import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import { drawLineChart } from '../../../../app/livechat/client/lib/chartHandler'; + +const initialData = { + departments: {}, +}; + +const init = (canvas, context, t) => drawLineChart( + canvas, + context, + [t('Open'), t('Closed')], + [], + [[], []], + { legends: true, anim: true, smallTicks: true }, +); + +const ChatsPerDepartmentChart = ({ params, reloadRef, ...props }) => { + const t = useTranslation(); + + const canvas = useRef(); + const context = useRef(); + + const updateChartData = useUpdateChartData({ + context, + canvas, + t, + init, + }); + + const { data, state, reload } = useEndpointDataExperimental( + 'livechat/analytics/dashboards/charts/chats-per-department', + params, + ); + + reloadRef.current.chatsPerDepartmentChart = reload; + + const { + departments = {}, + } = data ?? initialData; + + useEffect(() => { + const initChart = async () => { + context.current = await init(canvas.current, context.current, t); + }; + initChart(); + }, [t]); + + useEffect(() => { + if (state === ENDPOINT_STATES.DONE) { + Object.entries(departments).forEach(([name, value]) => { + updateChartData(name, [value.open, value.closed]); + }); + } + }, [departments, state, t, updateChartData]); + + return <Chart ref={canvas} {...props}/>; +}; + +export default ChatsPerDepartmentChart; diff --git a/client/omnichannel/realTimeMonitoring/charts/ResponseTimesChart.js b/client/omnichannel/realTimeMonitoring/charts/ResponseTimesChart.js new file mode 100644 index 00000000000..5f34715280c --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/charts/ResponseTimesChart.js @@ -0,0 +1,79 @@ +import React, { useRef, useEffect } from 'react'; + +import Chart from './Chart'; +import { useUpdateChartData } from './useUpdateChartData'; +import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import { drawLineChart } from '../../../../app/livechat/client/lib/chartHandler'; +import { getMomentChartLabelsAndData } from './getMomentChartLabelsAndData'; +import { getMomentCurrentLabel } from './getMomentCurrentLabel'; + +const [labels, initialData] = getMomentChartLabelsAndData(); + +const init = (canvas, context, t) => drawLineChart( + canvas, + context, + [t('Avg_reaction_time'), t('Longest_reaction_time'), t('Avg_response_time'), t('Longest_response_time')], + labels, + [initialData, initialData, initialData, initialData], + { legends: true, anim: true, smallTicks: true }, +); + +const ResponseTimesChart = ({ params, reloadRef, ...props }) => { + const t = useTranslation(); + + const canvas = useRef(); + const context = useRef(); + + const updateChartData = useUpdateChartData({ + context, + canvas, + t, + init, + }); + + const { data, state, reload } = useEndpointDataExperimental( + 'livechat/analytics/dashboards/charts/timings', + params, + ); + + reloadRef.current.responseTimesChart = reload; + + const { + reaction: { + avg: reactionAvg, + longest: reactionLongest, + }, + response: { + avg: responseAvg, + longest: responseLongest, + }, + } = data ?? { + reaction: { + avg: 0, + longest: 0, + }, + response: { + avg: 0, + longest: 0, + }, + }; + + useEffect(() => { + const initChart = async () => { + context.current = await init(canvas.current, context.current, t); + }; + initChart(); + }, [t]); + + useEffect(() => { + if (state === ENDPOINT_STATES.DONE) { + const label = getMomentCurrentLabel(); + updateChartData(label, [reactionAvg, reactionLongest, responseAvg, responseLongest]); + } + }, [reactionAvg, reactionLongest, responseAvg, responseLongest, state, t, updateChartData]); + + return <Chart ref={canvas} {...props}/>; +}; + +export default ResponseTimesChart; diff --git a/client/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.js b/client/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.js new file mode 100644 index 00000000000..64b1ace72cb --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.js @@ -0,0 +1,14 @@ +import moment from 'moment'; + +export const getMomentChartLabelsAndData = () => { + const timingLabels = []; + const initData = []; + const today = moment().startOf('day'); + for (let m = today; m.diff(moment(), 'hours') < 0; m.add(1, 'hours')) { + const hour = m.format('H'); + timingLabels.push(`${ moment(hour, ['H']).format('hA') }-${ moment((parseInt(hour) + 1) % 24, ['H']).format('hA') }`); + initData.push(0); + } + + return [timingLabels, initData]; +}; diff --git a/client/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.js b/client/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.js new file mode 100644 index 00000000000..964a21b4298 --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.js @@ -0,0 +1,8 @@ + +import moment from 'moment'; + +export const getMomentCurrentLabel = () => { + const hour = moment(new Date()).format('H'); + + return `${ moment(hour, ['H']).format('hA') }-${ moment((parseInt(hour) + 1) % 24, ['H']).format('hA') }`; +}; diff --git a/client/omnichannel/realTimeMonitoring/charts/useUpdateChartData.js b/client/omnichannel/realTimeMonitoring/charts/useUpdateChartData.js new file mode 100644 index 00000000000..7deaf9d1f65 --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/charts/useUpdateChartData.js @@ -0,0 +1,10 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; + +import { updateChart } from '../../../../app/livechat/client/lib/chartHandler'; + +export const useUpdateChartData = ({ context, canvas, init, t }) => useMutableCallback(async (label, data) => { + if (!context.current) { + context.current = await init(canvas.current, context.current, t); + } + await updateChart(context.current, label, data); +}); diff --git a/client/omnichannel/realTimeMonitoring/counter/CounterContainer.js b/client/omnichannel/realTimeMonitoring/counter/CounterContainer.js new file mode 100644 index 00000000000..e260e80585a --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/counter/CounterContainer.js @@ -0,0 +1,29 @@ +import React, { useEffect, useState } from 'react'; +import { Skeleton } from '@rocket.chat/fuselage'; + +import { ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import CounterRow from './CounterRow'; +import CounterItem from './CounterItem'; + +const CounterContainer = ({ data, state, initialData, ...props }) => { + const t = useTranslation(); + + const [displayData, setDisplayData] = useState(initialData); + + const { + totalizers, + } = data || { totalizers: initialData }; + + useEffect(() => { + if (state === ENDPOINT_STATES.DONE) { + setDisplayData(totalizers); + } + }, [state, t, totalizers]); + + return <CounterRow {...props}> + {displayData.map(({ title, value }, i) => <CounterItem key={i} title={title ? t(title) : <Skeleton width='x60' />} count={value}/>)} + </CounterRow>; +}; + +export default CounterContainer; diff --git a/client/omnichannel/realTimeMonitoring/counter/CounterItem.js b/client/omnichannel/realTimeMonitoring/counter/CounterItem.js new file mode 100644 index 00000000000..8cfc7016d92 --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/counter/CounterItem.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { Box } from '@rocket.chat/fuselage'; + +const CounterItem = ({ title = '', count = '-', ...props }) => <Box + display='flex' + flexDirection='column' + justifyContent='space-between' + alignItems='center' + flexGrow={1} + {...props} +> + <Box fontScale='s1' textTransform='uppercase' color='hint' textAlign='center' pi='x8'> + {title} + </Box> + <Box fontScale='h1'> + {count} + </Box> +</Box>; + +export default CounterItem; diff --git a/client/omnichannel/realTimeMonitoring/counter/CounterRow.js b/client/omnichannel/realTimeMonitoring/counter/CounterRow.js new file mode 100644 index 00000000000..526372e1580 --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/counter/CounterRow.js @@ -0,0 +1,23 @@ +import React, { Fragment } from 'react'; +import { Box, Divider } from '@rocket.chat/fuselage'; +import flattenChildren from 'react-keyed-flatten-children'; + +const CounterRow = ({ children, ...props }) => <Box + pb='x28' + pi='x20' + display='flex' + flexDirection='row' + justifyContent='space-around' + alignItems='center' + flexGrow={1} + {...props} +> + {children && flattenChildren(children).reduce((acc, child, i) => { + acc = children.length - 1 !== i + ? [...acc, <Fragment key={i}>{child}</Fragment>, <Divider key={(i + 1) * children.length} width='x2' m='none' alignSelf='stretch'/>] + : [...acc, child]; + return acc; + }, [])} +</Box>; + +export default CounterRow; diff --git a/client/omnichannel/realTimeMonitoring/counter/CounterRow.stories.js b/client/omnichannel/realTimeMonitoring/counter/CounterRow.stories.js new file mode 100644 index 00000000000..ca1bd8d550e --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/counter/CounterRow.stories.js @@ -0,0 +1,16 @@ +import React from 'react'; + +import CounterRow from './CounterRow'; +import CounterItem from './CounterItem'; + +export default { + title: 'omnichannel/RealtimeMonitoring/Counter', + component: CounterRow, +}; + +export const Default = () => <CounterRow> + <CounterItem title='total conversations' count={10}/> + <CounterItem title='open conversations' count={10}/> + <CounterItem title='total messages' count={10}/> + <CounterItem title='total visitors'/> +</CounterRow>; diff --git a/client/omnichannel/realTimeMonitoring/overviews/AgentsOverview.js b/client/omnichannel/realTimeMonitoring/overviews/AgentsOverview.js new file mode 100644 index 00000000000..29dc5457d21 --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/overviews/AgentsOverview.js @@ -0,0 +1,28 @@ +import React from 'react'; + +import { useEndpointDataExperimental } from '../../../hooks/useEndpointDataExperimental'; +import CounterContainer from '../counter/CounterContainer'; + +const overviewInitalValue = { + title: '', + value: '-', +}; + +const initialData = [ + overviewInitalValue, + overviewInitalValue, + overviewInitalValue, +]; + +const AgentsOverview = ({ params, reloadRef, ...props }) => { + const { data, state, reload } = useEndpointDataExperimental( + 'livechat/analytics/dashboards/agents-productivity-totalizers', + params, + ); + + reloadRef.current.agentsOverview = reload; + + return <CounterContainer state={state} data={data} initialData={initialData} {...props}/>; +}; + +export default AgentsOverview; diff --git a/client/omnichannel/realTimeMonitoring/overviews/ChatsOverview.js b/client/omnichannel/realTimeMonitoring/overviews/ChatsOverview.js new file mode 100644 index 00000000000..08f6cec7a37 --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/overviews/ChatsOverview.js @@ -0,0 +1,23 @@ +import React from 'react'; + +import { useEndpointDataExperimental } from '../../../hooks/useEndpointDataExperimental'; +import CounterContainer from '../counter/CounterContainer'; + +const initialData = [ + { title: '', value: 0 }, + { title: '', value: '0%' }, + { title: '', value: '00:00:00' }, +]; + +const ChatsOverview = ({ params, reloadRef, ...props }) => { + const { data, state, reload } = useEndpointDataExperimental( + 'livechat/analytics/dashboards/chats-totalizers', + params, + ); + + reloadRef.current.chatsOverview = reload; + + return <CounterContainer state={state} data={data} initialData={initialData} {...props}/>; +}; + +export default ChatsOverview; diff --git a/client/omnichannel/realTimeMonitoring/overviews/ConversationOverview.js b/client/omnichannel/realTimeMonitoring/overviews/ConversationOverview.js new file mode 100644 index 00000000000..6fe604dd9fe --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/overviews/ConversationOverview.js @@ -0,0 +1,29 @@ +import React from 'react'; + +import { useEndpointDataExperimental } from '../../../hooks/useEndpointDataExperimental'; +import CounterContainer from '../counter/CounterContainer'; + +const overviewInitalValue = { + title: '', + value: 0, +}; + +const initialData = [ + overviewInitalValue, + overviewInitalValue, + overviewInitalValue, + overviewInitalValue, +]; + +const ConversationOverview = ({ params, reloadRef, ...props }) => { + const { data, state, reload } = useEndpointDataExperimental( + 'livechat/analytics/dashboards/conversation-totalizers', + params, + ); + + reloadRef.current.conversationOverview = reload; + + return <CounterContainer state={state} data={data} initialData={initialData} {...props}/>; +}; + +export default ConversationOverview; diff --git a/client/omnichannel/realTimeMonitoring/overviews/ProductivityOverview.js b/client/omnichannel/realTimeMonitoring/overviews/ProductivityOverview.js new file mode 100644 index 00000000000..a86cadf5c9a --- /dev/null +++ b/client/omnichannel/realTimeMonitoring/overviews/ProductivityOverview.js @@ -0,0 +1,27 @@ +import React from 'react'; + +import { useEndpointDataExperimental } from '../../../hooks/useEndpointDataExperimental'; +import CounterContainer from '../counter/CounterContainer'; + +const defaultValue = { title: '', value: '00:00:00' }; + + +const initialData = [ + defaultValue, + defaultValue, + defaultValue, + defaultValue, +]; + +const ProductivityOverview = ({ params, reloadRef, ...props }) => { + const { data, state, reload } = useEndpointDataExperimental( + 'livechat/analytics/dashboards/productivity-totalizers', + params, + ); + + reloadRef.current.productivityOverview = reload; + + return <CounterContainer state={state} data={data} initialData={initialData} {...props}/>; +}; + +export default ProductivityOverview; diff --git a/client/omnichannel/routes.js b/client/omnichannel/routes.js index 9def7135dea..fe9cd83a93c 100644 --- a/client/omnichannel/routes.js +++ b/client/omnichannel/routes.js @@ -73,3 +73,8 @@ registerOmnichannelRoute('/current', { name: 'omnichannel-current-chats', lazyRouteComponent: () => import('./currentChats/CurrentChatsRoute'), }); + +registerOmnichannelRoute('/realtime-monitoring', { + name: 'omnichannel-realTime', + lazyRouteComponent: () => import('./realTimeMonitoring/RealTimeMonitoringPage'), +}); diff --git a/client/omnichannel/sidebarItems.js b/client/omnichannel/sidebarItems.js index 5aee7ef1d01..82586bc50da 100644 --- a/client/omnichannel/sidebarItems.js +++ b/client/omnichannel/sidebarItems.js @@ -15,7 +15,7 @@ export const { i18nLabel: 'Analytics', permissionGranted: () => hasPermission('view-livechat-analytics'), }, { - href: 'omnichannel/real-time-monitoring', + href: 'omnichannel-realTime', i18nLabel: 'Real_Time_Monitoring', permissionGranted: () => hasPermission('view-livechat-real-time-monitoring'), }, { diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 36e5dd92e21..d6e264059b4 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3682,6 +3682,7 @@ "Unread_Tray_Icon_Alert": "Unread Tray Icon Alert", "Unstar_Message": "Remove Star", "Update": "Update", + "Update_every": "Update every", "Update_LatestAvailableVersion": "Update Latest Available Version", "Update_EnableChecker": "Enable the Update Checker", "Update_to_version": "Update to __version__", -- GitLab