Refactor: Omnichannel Analytics (#18766)

pull/18932/head^2
gabriellsh 5 years ago committed by GitHub
parent af8794184f
commit b3d132fe5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      app/livechat/client/route.js
  2. 3
      app/livechat/client/views/admin.js
  3. 122
      app/livechat/client/views/app/analytics/livechatAnalytics.html
  4. 264
      app/livechat/client/views/app/analytics/livechatAnalytics.js
  5. 15
      app/livechat/client/views/app/analytics/livechatAnalyticsCustomDaterange.html
  6. 42
      app/livechat/client/views/app/analytics/livechatAnalyticsCustomDaterange.js
  7. 48
      app/livechat/client/views/app/analytics/livechatAnalyticsDaterange.html
  8. 57
      app/livechat/client/views/app/analytics/livechatAnalyticsDaterange.js
  9. 44
      client/omnichannel/analytics/AgentOverview.js
  10. 82
      client/omnichannel/analytics/AnalyticsPage.js
  11. 10
      client/omnichannel/analytics/AnalyticsPage.stories.js
  12. 122
      client/omnichannel/analytics/DateRangePicker.js
  13. 10
      client/omnichannel/analytics/DateRangePicker.stories.js
  14. 51
      client/omnichannel/analytics/InterchangeableChart.js
  15. 55
      client/omnichannel/analytics/Overview.js
  16. 4
      client/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js
  17. 5
      client/omnichannel/routes.js
  18. 2
      client/omnichannel/sidebarItems.js

@ -18,14 +18,6 @@ AccountBox.addRoute({
pageTemplate: 'livechatDashboard',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-analytics',
path: '/analytics',
sideNav: 'omnichannelFlex',
i18nPageTitle: 'Analytics',
pageTemplate: 'livechatAnalytics',
}, livechatManagerRoutes, load);
AccountBox.addRoute({
name: 'livechat-departments',
path: '/departments',

@ -1,6 +1,3 @@
import './app/analytics/livechatAnalytics';
import './app/analytics/livechatAnalyticsCustomDaterange';
import './app/analytics/livechatAnalyticsDaterange';
import './app/livechatDashboard.html';
import './app/livechatDepartmentForm';
import './app/livechatDepartments';

@ -1,122 +0,0 @@
<template name="livechatAnalytics">
{{#requiresPermission 'view-livechat-analytics'}}
<form class="form-inline">
<div class="form-group rc-select">
<select id="lc-analytics-options" class="required input-monitor rc-select__element">
{{#each analyticsAllOptions}}
<option class="rc-select__option" value="{{value}}" selected="{{selected value}}" dir="auto">{{_ name}}</option>
{{/each}}
</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}}
<div class="form-group lc-analytics-header">
{{#if showLeftNavButton}}
<button class="lc-daterange-prev">
<i class="icon-left-circled"></i>
</button>
{{/if}}
<button class="js-button lc-date-picker-btn">
{{#with daterange}}
<span class="lc-datarange-from">{{from}}</span>
<span class="fade">{{_ "to" }}</span>
<span class="lc-datarange-to">{{to}}</span>
{{/with}}
</button>
{{#if showRightNavButton}}
<button class="lc-daterange-next">
<i class="icon-right-circled"></i>
</button>
{{/if}}
</div>
</form>
<div class="section">
<div class="section-content border-component-color">
<div class="lc-analytics-overview">
{{#each analyticsOverviewData}}
<div class="lc-analytics-ov-col">
{{#each this}}
<div class="lc-analytics-ov-case">
<span class="value">{{value}}</span>
<span class="title">{{_ title}}</span>
</div>
{{/each}}
</div>
{{/each}}
</div>
</div>
</div>
<div class="lc-analytics-table">
<div class="lc-analytics-flex-container">
<div class="lc-analytics-chart-col">
<div class="section lc-chart-section">
<div class="section-content border-component-color lc-chart-section-content">
<div class="rc-select lc-chart-options">
<select id="lc-analytics-chart-options" class="required input-monitor rc-select__element">
{{#with analyticsOptions}} {{#each chartOptions}}
<option class="rc-select__option" value="{{value}}" selected="{{selected value}}" dir="auto">{{_ name}}</option>
{{/each}} {{/with}}
</select>
<i class="icon-angle-down"></i>
</div>
<div class="lc-analytics-chart-container">
<canvas id="lc-analytics-chart"></canvas>
</div>
</div>
</div>
</div>
<div class="lc-analytics-chart-ov-col">
<div class="section lc-chart-section">
<div class="section-content border-component-color lc-chart-section-content">
<div class="lc-chart-ov-content">
<div class="list">
<table class="secondary-background-color">
{{#with agentOverviewData}}
<thead>
<tr class="table-row">
{{#each head}}
<th class="content-background-color border-component-color">{{_ name}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each data}}
<tr>
<td class="border-component-color border-component-td">{{name}}</td>
<td class="border-component-color">{{value}}</td>
</tr>
{{/each}}
</tbody>
{{/with}}
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{/requiresPermission}}
</template>

@ -1,264 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
import moment from 'moment';
import { handleError } from '../../../../../utils';
import { popover } from '../../../../../ui-utils';
import { drawLineChart } from '../../../lib/chartHandler';
import { setDateRange, updateDateRange } from '../../../lib/dateHandler';
import { APIClient } from '../../../../../utils/client';
import './livechatAnalytics.html';
let templateInstance; // current template instance/context
let chartContext; // stores context of current chart, used to clean when redrawing
const analyticsAllOptions = () => [{
name: 'Conversations',
value: 'conversations',
chartOptions: [{
name: 'Total_conversations',
value: 'total-conversations',
}, {
name: 'Avg_chat_duration',
value: 'avg-chat-duration',
}, {
name: 'Total_messages',
value: 'total-messages',
}],
}, {
name: 'Productivity',
value: 'productivity',
chartOptions: [{
name: 'Avg_first_response_time',
value: 'avg-first-response-time',
}, {
name: 'Best_first_response_time',
value: 'best_first_response_time',
}, {
name: 'Avg_response_time',
value: 'avg-response-time',
}, {
name: 'Avg_reaction_time',
value: 'avg-reaction-time',
}],
}];
/**
*
* @param {Array} arr
* @param {Integer} chunkCount
*
* @returns {Array{Array}} Array containing arrays
*/
const chunkArray = (arr, chunkCount) => { // split array into n almost equal arrays
const chunks = [];
while (arr.length) {
const chunkSize = Math.ceil(arr.length / chunkCount--);
const chunk = arr.slice(0, chunkSize);
chunks.push(chunk);
arr = arr.slice(chunkSize);
}
return chunks;
};
const getChartDepartment = (department) => department?._id;
const updateAnalyticsChart = () => {
const [department] = templateInstance.selectedDepartments.get();
const departmentId = getChartDepartment(department);
const options = {
daterange: {
from: moment(templateInstance.daterange.get().from, 'MMM D YYYY').toISOString(),
to: moment(templateInstance.daterange.get().to, 'MMM D YYYY').toISOString(),
},
chartOptions: templateInstance.chartOptions.get(),
...departmentId && { departmentId },
};
Meteor.call('livechat:getAnalyticsChartData', options, async function(error, result) {
if (error) {
return handleError(error);
}
if (!(result && result.chartLabel && result.dataLabels && result.dataPoints)) {
console.log('livechat:getAnalyticsChartData => Missing Data');
}
chartContext = await drawLineChart(document.getElementById('lc-analytics-chart'), chartContext, [result.chartLabel], result.dataLabels, [result.dataPoints]);
});
Meteor.call('livechat:getAgentOverviewData', options, function(error, result) {
if (error) {
return handleError(error);
}
if (!result) {
console.log('livechat:getAgentOverviewData => Missing Data');
}
templateInstance.agentOverviewData.set(result);
});
};
const updateAnalyticsOverview = () => {
const [department] = templateInstance.selectedDepartments.get();
const departmentId = getChartDepartment(department);
const options = {
daterange: {
from: moment(templateInstance.daterange.get().from, 'MMM D YYYY').toISOString(),
to: moment(templateInstance.daterange.get().to, 'MMM D YYYY').toISOString(),
},
analyticsOptions: templateInstance.analyticsOptions.get(),
...departmentId && { departmentId },
};
Meteor.call('livechat:getAnalyticsOverviewData', options, (error, result) => {
if (error) {
return handleError(error);
}
if (!result) {
console.log('livechat:getAnalyticsOverviewData => Missing Data');
}
templateInstance.analyticsOverviewData.set(chunkArray(result, 3));
});
};
Template.livechatAnalytics.helpers({
analyticsOverviewData() {
return templateInstance.analyticsOverviewData.get();
},
agentOverviewData() {
return templateInstance.agentOverviewData.get();
},
analyticsAllOptions() {
return analyticsAllOptions();
},
analyticsOptions() {
return templateInstance.analyticsOptions.get();
},
daterange() {
return templateInstance.daterange.get();
},
selected(value) {
if (value === templateInstance.analyticsOptions.get().value || value === templateInstance.chartOptions.get().value) { return 'selected'; }
return false;
},
showLeftNavButton() {
if (templateInstance.daterange.get().value === 'custom') {
return false;
}
return true;
},
showRightNavButton() {
if (templateInstance.daterange.get().value === 'custom' || templateInstance.daterange.get().value === 'today' || templateInstance.daterange.get().value === 'this-week' || templateInstance.daterange.get().value === 'this-month') {
return false;
}
return true;
},
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.livechatAnalytics.onCreated(async function() {
templateInstance = Template.instance();
this.analyticsOverviewData = new ReactiveVar();
this.agentOverviewData = new ReactiveVar();
this.daterange = new ReactiveVar({});
this.analyticsOptions = new ReactiveVar(analyticsAllOptions()[0]); // default selected first
this.chartOptions = new ReactiveVar(analyticsAllOptions()[0].chartOptions[0]); // default selected first
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);
this.autorun(() => {
templateInstance.daterange.set(setDateRange());
});
});
Template.livechatAnalytics.onRendered(() => {
Tracker.autorun(() => {
if (templateInstance.daterange.get()
&& templateInstance.analyticsOptions.get()
&& templateInstance.chartOptions.get()) {
updateAnalyticsOverview();
updateAnalyticsChart();
}
});
});
Template.livechatAnalytics.events({
'click .lc-date-picker-btn'(e) {
e.preventDefault();
const options = [];
const config = {
template: 'livechatAnalyticsDaterange',
currentTarget: e.currentTarget,
data: {
options,
daterange: templateInstance.daterange,
},
offsetVertical: e.currentTarget.clientHeight + 10,
};
popover.open(config);
},
'click .lc-daterange-prev'(e) {
e.preventDefault();
templateInstance.daterange.set(updateDateRange(templateInstance.daterange.get(), -1));
},
'click .lc-daterange-next'(e) {
e.preventDefault();
templateInstance.daterange.set(updateDateRange(templateInstance.daterange.get(), 1));
},
'change #lc-analytics-options'(e) {
e.preventDefault();
templateInstance.analyticsOptions.set(analyticsAllOptions().filter(function(obj) {
return obj.value === e.currentTarget.value;
})[0]);
templateInstance.chartOptions.set(templateInstance.analyticsOptions.get().chartOptions[0]);
},
'change #lc-analytics-chart-options'(e) {
e.preventDefault();
templateInstance.chartOptions.set(templateInstance.analyticsOptions.get().chartOptions.filter(function(obj) {
return obj.value === e.currentTarget.value;
})[0]);
},
});

@ -1,15 +0,0 @@
<template name="livechatAnalyticsCustomDaterange">
<div class="rc-popover__column">
<ul class="rc-popover__list">
<li class="rc-popover__item">
<input type="text" placeholder="{{_ "From" }}" class="lc-custom-daterange rc-input__element lc-custom-daterange-from" name="lc-custom-daterange-from" value="{{from}}">
</li>
<li class="rc-popover__item">
<input type="text" placeholder="{{_ "To"}}" class="lc-custom-daterange rc-input__element lc-custom-daterange-to" name="lc-custom-daterange-to" value="{{to}}">
</li>
<li class="rc-popover__item">
<button class="rc-button lc-custom-daterange-submit">{{_ "Ok"}}</button>
</li>
</ul>
</div>
</template>

@ -1,42 +0,0 @@
import { Template } from 'meteor/templating';
import moment from 'moment';
import { handleError } from '../../../../../utils';
import { popover } from '../../../../../ui-utils';
import { setDateRange } from '../../../lib/dateHandler';
import './livechatAnalyticsCustomDaterange.html';
Template.livechatAnalyticsCustomDaterange.helpers({
from() {
return moment(Template.currentData().daterange.get().from, 'MMM D YYYY').format('L');
},
to() {
return moment(Template.currentData().daterange.get().to, 'MMM D YYYY').format('L');
},
});
Template.livechatAnalyticsCustomDaterange.onRendered(function() {
this.$('.lc-custom-daterange').datepicker({
autoclose: true,
todayHighlight: true,
format: moment.localeData().longDateFormat('L').toLowerCase(),
});
});
Template.livechatAnalyticsCustomDaterange.events({
'click .lc-custom-daterange-submit'(e) {
e.preventDefault();
const from = document.getElementsByClassName('lc-custom-daterange-from')[0].value;
const to = document.getElementsByClassName('lc-custom-daterange-to')[0].value;
if (moment(from).isValid() && moment(to).isValid()) {
Template.currentData().daterange.set(setDateRange('custom', moment(new Date(from)), moment(new Date(to))));
} else {
handleError({ details: { errorTitle: 'Invalid_dates' }, error: 'Error_in_custom_dates' });
}
popover.close();
},
});

@ -1,48 +0,0 @@
<template name="livechatAnalyticsDaterange">
<div class="rc-popover__column">
<ul class="rc-popover__list">
<li class="rc-popover__item {{bold 'today'}}">
<label class="rc-popover__label">
<input type="radio" name="lc-daterange" value="today" class="hidden">
<span class="rc-popover__item-text">{{_ "Today"}}</span>
</label>
</li>
<li class="rc-popover__item {{bold 'yesterday'}}">
<label class="rc-popover__label">
<input type="radio" name="lc-daterange" value="yesterday" class="hidden">
<span class="rc-popover__item-text">{{_ "Yesterday"}}</span>
</label>
</li>
<li class="rc-popover__item {{bold 'this-week'}}">
<label class="rc-popover__label">
<input type="radio" name="lc-daterange" value="this-week" class="hidden">
<span class="rc-popover__item-text">{{_ "This_week"}}</span>
</label>
</li>
<li class="rc-popover__item {{bold 'prev-week'}}">
<label class="rc-popover__label">
<input type="radio" name="lc-daterange" value="prev-week" class="hidden">
<span class="rc-popover__item-text">{{_ "Previous_week"}}</span>
</label>
</li>
<li class="rc-popover__item {{bold 'this-month'}}">
<label class="rc-popover__label">
<input type="radio" name="lc-daterange" value="this-month" class="hidden">
<span class="rc-popover__item-text">{{_ "This_month"}}</span>
</label>
</li>
<li class="rc-popover__item {{bold 'prev-month'}}">
<label class="rc-popover__label">
<input type="radio" name="lc-daterange" value="prev-month" class="hidden">
<span class="rc-popover__item-text">{{_ "Previous_month"}}</span>
</label>
</li>
<li class="rc-popover__item {{bold 'custom'}}">
<label class="rc-popover__label">
<input type="radio" name="lc-daterange" value="custom" class="hidden">
<span class="rc-popover__item-text">{{_ "Custom_dates"}}</span>
</label>
</li>
</ul>
</div>
</template>

@ -1,57 +0,0 @@
import { Template } from 'meteor/templating';
import moment from 'moment';
import { popover } from '../../../../../ui-utils';
import { setDateRange } from '../../../lib/dateHandler';
import './livechatAnalyticsDaterange.html';
Template.livechatAnalyticsDaterange.helpers({
bold(prop) {
return prop === Template.currentData().daterange.get().value ? 'rc-popover__item--bold' : '';
},
});
Template.livechatAnalyticsDaterange.events({
'change input'(e) {
e.preventDefault();
const value = e.currentTarget.getAttribute('type') === 'checkbox' ? e.currentTarget.checked : e.currentTarget.value;
popover.close();
switch (value) {
case 'custom':
const target = document.getElementsByClassName('lc-date-picker-btn')[0];
const options = [];
const config = {
template: 'livechatAnalyticsCustomDaterange',
currentTarget: target,
data: {
options,
daterange: Template.currentData().daterange,
},
offsetVertical: target.clientHeight + 10,
};
popover.open(config);
break;
case 'today':
Template.currentData().daterange.set(setDateRange(value, moment().startOf('day'), moment().startOf('day')));
break;
case 'yesterday':
Template.currentData().daterange.set(setDateRange(value, moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').startOf('day')));
break;
case 'this-week':
Template.currentData().daterange.set(setDateRange(value, moment().startOf('week'), moment().endOf('week')));
break;
case 'prev-week':
Template.currentData().daterange.set(setDateRange(value, moment().subtract(1, 'weeks').startOf('week'), moment().subtract(1, 'weeks').endOf('week')));
break;
case 'this-month':
Template.currentData().daterange.set(setDateRange(value, moment().startOf('month'), moment().endOf('month')));
break;
case 'prev-month':
Template.currentData().daterange.set(setDateRange(value, moment().subtract(1, 'months').startOf('month'), moment().subtract(1, 'months').endOf('month')));
break;
}
},
});

@ -0,0 +1,44 @@
import React, { useEffect, useState, useMemo } from 'react';
import { Table } from '@rocket.chat/fuselage';
import { useMethodData, AsyncState } from '../../contexts/ServerContext';
import { useTranslation } from '../../contexts/TranslationContext';
const style = { width: '100%' };
const AgentOverview = ({ type, dateRange, departmentId }) => {
const t = useTranslation();
const { start, end } = dateRange;
const params = useMemo(() => [{
chartOptions: { name: type },
daterange: { from: start, to: end },
...departmentId && { departmentId },
}], [departmentId, end, start, type]);
const [data, state] = useMethodData('livechat:getAgentOverviewData', params);
const [displayData, setDisplayData] = useState();
useEffect(() => {
if (state === AsyncState.DONE) {
setDisplayData(data);
}
}, [data, state]);
return <Table style={style} fixed>
<Table.Head>
<Table.Row>
{displayData?.head?.map(({ name }, i) => <Table.Cell key={i}>{ t(name) }</Table.Cell>)}
</Table.Row>
</Table.Head>
<Table.Body>
{displayData?.data?.map(({ name, value }, i) => <Table.Row key={i}>
<Table.Cell>{name}</Table.Cell>
<Table.Cell>{value}</Table.Cell>
</Table.Row>)}
</Table.Body>
</Table>;
};
export default AgentOverview;

@ -0,0 +1,82 @@
import React, { useMemo, useState, useEffect } from 'react';
import { Box, Select, Margins } from '@rocket.chat/fuselage';
import DepartmentAutoComplete from '../DepartmentAutoComplete';
import DateRangePicker from './DateRangePicker';
import Overview from './Overview';
import AgentOverview from './AgentOverview';
import Page from '../../components/basic/Page';
import InterchangeableChart from './InterchangeableChart';
import { useTranslation } from '../../contexts/TranslationContext';
const useOptions = (type) => {
const t = useTranslation();
return useMemo(() => {
if (type === 'Conversations') {
return [
['Total_conversations', t('Total_conversations')],
['Avg_chat_duration', t('Avg_chat_duration')],
['Total_messages', t('Total_messages')],
];
}
return [
['Avg_first_response_time', t('Avg_first_response_time')],
['Best_first_response_time', t('Best_first_response_time')],
['Avg_response_time', t('Avg_response_time')],
['Avg_reaction_time', t('Avg_reaction_time')],
];
}, [t, type]);
};
const AnalyticsPage = () => {
const t = useTranslation();
const [type, setType] = useState('Conversations');
const [departmentId, setDepartmentId] = useState(null);
const [dateRange, setDateRange] = useState({ start: null, end: null });
const [chartName, setChartName] = useState();
const typeOptions = useMemo(() => [
['Conversations', t('Conversations')],
['Productivity', t('Productivity')],
], [t]);
const graphOptions = useOptions(type);
useEffect(() => {
setChartName(graphOptions[0][0]);
}, [graphOptions]);
return <Page>
<Page.Header title={t('Analytics')}/>
<Page.ScrollableContentWithShadow display='flex' flexDirection='column'>
<Margins block='x4'>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexWrap='wrap' mi='neg-x4' mb='neg-x4'>
<Box display='flex' flexWrap='nowrap' flexGrow={1} flexShrink={1} justifyContent='stretch' mb='x4'>
<Margins inline='x4'>
<Select options={typeOptions} value={type} onChange={setType} />
<DepartmentAutoComplete placeholder={t('Departments')} value={departmentId} onChange={setDepartmentId}/>
</Margins>
</Box>
<DateRangePicker mi='none' mb='x4' flexWrap='nowrap' display='flex' flexGrow={1} flexShrink={1} justifyContent='stretch' onChange={setDateRange}/>
</Box>
<Overview type={type} dateRange={dateRange} departmentId={departmentId}/>
<Select options={graphOptions} value={chartName} onChange={setChartName} flexGrow={0}/>
<Box display='flex' flexDirection='row' flexGrow={1} flexShrink={1}>
<InterchangeableChart flexShrink={1} w='66%' h='100%' chartName={chartName} departmentId={departmentId} dateRange={dateRange} alignSelf='stretch'/>
<Box
display='flex'
w='33%'
flexDirection='row'
justifyContent='stretch'
p='x10'
mis='x4'
>
<AgentOverview type={chartName} dateRange={dateRange} departmentId={departmentId}/>
</Box>
</Box>
</Margins>
</Page.ScrollableContentWithShadow>
</Page>;
};
export default AnalyticsPage;

@ -0,0 +1,10 @@
import React from 'react';
import AnalyticsPage from './AnalyticsPage';
export default {
title: 'omnichannel/AnalyticsPage',
component: AnalyticsPage,
};
export const Default = () => <AnalyticsPage />;

@ -0,0 +1,122 @@
import React, { useState, useMemo, useEffect } from 'react';
import { Box, InputBox, Menu, Margins } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '../../contexts/TranslationContext';
const date = new Date();
const formatToDateInput = (date) => date.toISOString().slice(0, 10);
const todayDate = formatToDateInput(date);
const getMonthRange = (monthsToSubtractFromToday) => {
const date = new Date();
return {
start: formatToDateInput(new Date(
date.getFullYear(),
date.getMonth() - monthsToSubtractFromToday,
1)),
end: formatToDateInput(new Date(
date.getFullYear(),
date.getMonth() - monthsToSubtractFromToday + 1,
0)),
};
};
const getWeekRange = (daysToSubtractFromStart, daysToSubtractFromEnd) => {
const date = new Date();
return {
start: formatToDateInput(new Date(
date.getFullYear(),
date.getMonth(),
date.getDate() - daysToSubtractFromStart)),
end: formatToDateInput(new Date(
date.getFullYear(),
date.getMonth(),
date.getDate() - daysToSubtractFromEnd)),
};
};
const DateRangePicker = ({ onChange = () => {}, ...props }) => {
const t = useTranslation();
const [range, setRange] = useState({ start: '', end: '' });
const {
start,
end,
} = range;
const handleStart = useMutableCallback(({ currentTarget }) => {
const rangeObj = {
start: currentTarget.value,
end: range.end,
};
setRange(rangeObj);
onChange(rangeObj);
});
const handleEnd = useMutableCallback(({ currentTarget }) => {
const rangeObj = {
end: currentTarget.value,
start: range.start,
};
setRange(rangeObj);
onChange(rangeObj);
});
const handleRange = useMutableCallback((range) => {
setRange(range);
onChange(range);
});
useEffect(() => {
handleRange({
start: todayDate,
end: todayDate,
});
}, [handleRange]);
const options = useMemo(() => ({
today: {
icon: 'history',
label: t('Today'),
action: () => { handleRange(getWeekRange(0, 0)); },
},
yesterday: {
icon: 'history',
label: t('Yesterday'),
action: () => { handleRange(getWeekRange(1, 1)); },
},
thisWeek: {
icon: 'history',
label: t('This_week'),
action: () => { handleRange(getWeekRange(7, 0)); },
},
previousWeek: {
icon: 'history',
label: t('Previous_week'),
action: () => { handleRange(getWeekRange(14, 7)); },
},
thisMonth: {
icon: 'history',
label: t('This_month'),
action: () => { handleRange(getMonthRange(0)); },
},
lastMonth: {
icon: 'history',
label: t('Previous_month'),
action: () => { handleRange(getMonthRange(1)); },
},
}), [handleRange, t]);
return <Box mi='neg-x4' {...props}>
<Margins inline='x4'>
<InputBox type='date' onChange={handleStart} max={todayDate} value={start}/>
<InputBox type='date' onChange={handleEnd} min={start} max={todayDate} value={end}/>
<Menu options={options}/>
</Margins>
</Box>;
};
export default DateRangePicker;

@ -0,0 +1,10 @@
import React from 'react';
import DateRangePicker from './DateRangePicker';
export default {
title: 'DateRange',
component: DateRangePicker,
};
export const Default = () => <DateRangePicker />;

@ -0,0 +1,51 @@
import React, { useRef, useEffect } from 'react';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import Chart from '../realTimeMonitoring/charts/Chart';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { drawLineChart } from '../../../app/livechat/client/lib/chartHandler';
const InterchangeableChart = ({ departmentId, dateRange, chartName, ...props }) => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const canvas = useRef();
const context = useRef();
const {
start,
end,
} = dateRange;
const loadData = useMethod('livechat:getAnalyticsChartData');
const draw = useMutableCallback(async (params) => {
try {
const result = await loadData(params);
if (!(result && result.chartLabel && result.dataLabels && result.dataPoints)) {
return console.log('livechat:getAnalyticsChartData => Missing Data');
}
context.current = await drawLineChart(canvas.current, context.current, [result.chartLabel], result.dataLabels, [result.dataPoints]);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
useEffect(() => {
draw({
daterange: {
from: start,
to: end,
},
chartOptions: { name: chartName },
...departmentId && { departmentId },
});
}, [chartName, departmentId, draw, end, start, t]);
return <Chart border='none' pi='none' ref={canvas} {...props}/>;
};
export default InterchangeableChart;

@ -0,0 +1,55 @@
import React, { useEffect, useState, useMemo } from 'react';
import { Box, Skeleton } from '@rocket.chat/fuselage';
import CounterItem from '../realTimeMonitoring/counter/CounterItem';
import CounterRow from '../realTimeMonitoring/counter/CounterRow';
import { useMethodData, AsyncState } from '../../contexts/ServerContext';
import { useTranslation } from '../../contexts/TranslationContext';
const initialData = Array.from({ length: 3 }).map(() => ({ title: '', value: '' }));
const conversationsInitialData = [initialData, initialData];
const productivityInitialData = [initialData];
const Overview = ({ type, dateRange, departmentId }) => {
const t = useTranslation();
const { start, end } = dateRange;
const params = useMemo(() => [{
analyticsOptions: { name: type },
daterange: { from: start, to: end },
...departmentId && { departmentId },
}], [departmentId, end, start, type]);
const [data, state] = useMethodData('livechat:getAnalyticsOverviewData', params);
const [displayData, setDisplayData] = useState(conversationsInitialData);
useEffect(() => {
setDisplayData(type === 'Conversations' ? conversationsInitialData : productivityInitialData);
}, [type]);
useEffect(() => {
if (state === AsyncState.DONE) {
if (data?.length > 3) {
setDisplayData([data.slice(0, 3), data.slice(3)]);
} else if (data) {
setDisplayData([data]);
}
}
}, [data, state]);
return <Box
pb='x28'
flexDirection='column'
>
{
displayData.map((items = [], i) => <CounterRow key={i} border='0' pb='none'>
{items.map(({ title, value }, i) => <CounterItem flexShrink={1} pb='x8' flexBasis='100%' key={i} title={title ? t(title) : <Skeleton width='x60' />} count={value}/>)}
</CounterRow>)
}
</Box>;
};
export default Overview;

@ -93,13 +93,13 @@ const RealTimeMonitoringPage = () => {
<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}/>
<ChatDurationChart flexGrow={1} flexShrink={1} w='100%' 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}/>
<ResponseTimesChart flexGrow={1} flexShrink={1} w='100%' reloadRef={reloadRef} params={allParams}/>
</Box>
</Margins>
</Page.ScrollableContentWithShadow>

@ -78,3 +78,8 @@ registerOmnichannelRoute('/realtime-monitoring', {
name: 'omnichannel-realTime',
lazyRouteComponent: () => import('./realTimeMonitoring/RealTimeMonitoringPage'),
});
registerOmnichannelRoute('/analytics', {
name: 'omnichannel-analytics',
lazyRouteComponent: () => import('./analytics/AnalyticsPage'),
});

@ -11,7 +11,7 @@ export const {
i18nLabel: 'Current_Chats',
permissionGranted: () => hasPermission('view-livechat-current-chats'),
}, {
href: 'omnichannel/analytics',
href: 'omnichannel-analytics',
i18nLabel: 'Analytics',
permissionGranted: () => hasPermission('view-livechat-analytics'),
}, {

Loading…
Cancel
Save