Skip to content
3 changes: 1 addition & 2 deletions app/livechat/client/stylesheets/livechat.css
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,7 @@
.lc-analytics-table {
display: flex;

height: ~"calc(100% - 280px)";
height: ~"-webkit-calc-height(100% - 280px)";
height: 100%;

min-height: 300px;
flex-flow: column;
Expand Down
22 changes: 22 additions & 0 deletions app/livechat/client/views/app/analytics/livechatAnalytics.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@
<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">
Expand Down
49 changes: 47 additions & 2 deletions app/livechat/client/views/app/analytics/livechatAnalytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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
Expand Down Expand Up @@ -62,13 +63,19 @@ const chunkArray = (arr, chunkCount) => { // split array into n almost equal arr
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) {
Expand Down Expand Up @@ -97,12 +104,16 @@ const updateAnalyticsChart = () => {
};

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) => {
Expand Down Expand Up @@ -150,17 +161,49 @@ Template.livechatAnalytics.helpers({
}
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(function() {
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());
Expand All @@ -169,7 +212,9 @@ Template.livechatAnalytics.onCreated(function() {

Template.livechatAnalytics.onRendered(() => {
Tracker.autorun(() => {
if (templateInstance.daterange.get() && templateInstance.analyticsOptions.get() && templateInstance.chartOptions.get()) {
if (templateInstance.daterange.get()
&& templateInstance.analyticsOptions.get()
&& templateInstance.chartOptions.get()) {
updateAnalyticsOverview();
updateAnalyticsChart();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@
</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 }}
Expand Down
118 changes: 91 additions & 27 deletions app/livechat/client/views/app/analytics/livechatRealTimeMonitoring.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ const updateChartData = async (chartId, label, data) => {

let timer;

const getChartDepartment = (department) => department?._id;

const getDaterange = () => {
const today = moment(new Date());
return {
Expand All @@ -110,8 +112,11 @@ const getDaterange = () => {
};
};

const loadConversationOverview = async ({ start, end }) => {
const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/conversation-totalizers?start=${ start }&end=${ end }`);
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;
};

Expand All @@ -121,8 +126,9 @@ const updateConversationOverview = async (totalizers) => {
}
};

const loadAgentsOverview = async ({ start, end }) => {
const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/agents-productivity-totalizers?start=${ start }&end=${ end }`);
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;
};

Expand All @@ -131,8 +137,9 @@ const updateAgentsOverview = async (totalizers) => {
templateInstance.agentsOverview.set(totalizers);
}
};
const loadChatsOverview = async ({ start, end }) => {
const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/chats-totalizers?start=${ start }&end=${ end }`);
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;
};

Expand All @@ -142,8 +149,9 @@ const updateChatsOverview = async (totalizers) => {
}
};

const loadProductivityOverview = async ({ start, end }) => {
const { totalizers } = await APIClient.v1.get(`livechat/analytics/dashboards/productivity-totalizers?start=${ start }&end=${ end }`);
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;
};

Expand All @@ -153,27 +161,37 @@ const updateProductivityOverview = async (totalizers) => {
}
};

const loadChatsChartData = ({ start, end }) => APIClient.v1.get(`livechat/analytics/dashboards/charts/chats?start=${ start }&end=${ end }`);
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 }) => {
const result = await APIClient.v1.get(`livechat/analytics/dashboards/charts/chats-per-agent?start=${ start }&end=${ end }`);
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 = (agents) => {
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 = () => APIClient.v1.get('livechat/analytics/dashboards/charts/agents-status');
const loadAgentsStatusChartData = ({ departmentId }) => {
const additionalParams = parseAdditionalParams({ departmentId }, '?');
return APIClient.v1.get(`livechat/analytics/dashboards/charts/agents-status${ additionalParams }`);
};

const updateAgentStatusChart = async (statusData) => {
if (!statusData) {
Expand All @@ -186,19 +204,26 @@ const updateAgentStatusChart = async (statusData) => {
await updateChartData('lc-agents-chart', 'Busy', [statusData.busy]);
};

const loadChatsPerDepartmentChartData = async ({ start, end }) => {
const result = await APIClient.v1.get(`livechat/analytics/dashboards/charts/chats-per-department?start=${ start }&end=${ end }`);
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 = (departments) => {
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 }) => APIClient.v1.get(`livechat/analytics/dashboards/charts/timings?start=${ start }&end=${ end }`);
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');
Expand Down Expand Up @@ -229,9 +254,27 @@ Template.livechatRealTimeMonitoring.helpers({
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(function() {
Template.livechatRealTimeMonitoring.onCreated(async function() {
templateInstance = Template.instance();
this.isLoading = new ReactiveVar(false);
this.conversationsOverview = new ReactiveVar();
Expand All @@ -240,22 +283,43 @@ Template.livechatRealTimeMonitoring.onCreated(function() {
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();
updateConversationOverview(await loadConversationOverview(daterange));
updateProductivityOverview(await loadProductivityOverview(daterange));
updateChatsChart(await loadChatsChartData(daterange));
updateChatsPerAgentChart(await loadChatsPerAgentChartData(daterange));
updateAgentStatusChart(await loadAgentsStatusChartData());
updateDepartmentsChart(await loadChatsPerDepartmentChartData(daterange));
updateTimingsChart(await loadTimingsChartData(daterange));
updateAgentsOverview(await loadAgentsOverview(daterange));
updateChatsOverview(await loadChatsOverview(daterange));
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) {
Expand Down
Loading