Refactor: Omnichannel Analytics (#18766)
parent
af8794184f
commit
b3d132fe5c
@ -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; |
Loading…
Reference in new issue