Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f3241e8
Add livechat transfer history to room object
Aug 21, 2019
4493e70
Add metrics to get the visitor inactivity on livechat room close
Aug 26, 2019
ef4ebff
Add setting to know when its considered visitor abandonment
Aug 27, 2019
c15b6a0
Merge branch 'develop' into livechat-analytics
Aug 27, 2019
1c66120
Add collection to track available time and history of livechat agents
Aug 28, 2019
c6d196b
Change date format
Aug 28, 2019
8b00fe6
Merge branch 'develop' into livechat-analytics
Aug 28, 2019
50803ca
Merge branch 'develop' into livechat-analytics
Sep 16, 2019
efcda9e
Merge remote-tracking branch 'origin/livechat-analytics' into livecha…
Sep 16, 2019
d579936
Merge branch 'develop' into livechat-analytics
Sep 17, 2019
f1a7f43
Apply suggestions from review
Sep 18, 2019
6960c6f
Merge branch 'develop' into livechat-analytics
Sep 19, 2019
8acf467
Merge branch 'develop' into livechat-analytics
renatobecker Sep 20, 2019
6f2ae7b
Merge branch 'develop' into livechat-analytics
Nov 7, 2019
81c1376
Apply suggestions from review
Nov 7, 2019
eaf4360
Apply suggestions from review
Nov 8, 2019
9c51a37
fix property name
Nov 8, 2019
0fd5a96
Rename var and add ptbr translation
Nov 8, 2019
c5c1dc9
code improve
Nov 8, 2019
0e7b1a1
improve
Nov 8, 2019
4eb2772
Fix typo.
renatobecker Nov 8, 2019
8b7bb33
Fix typos.
renatobecker Nov 8, 2019
738be31
livechat agent history save history by day
Nov 11, 2019
0aa93f0
Convert livechat room transfer data to system message
Nov 11, 2019
032b5ee
Merge branch 'develop' into livechat-analytics
Nov 11, 2019
e47cc80
Change query to retrieve transferred data from messages
Nov 11, 2019
2421b94
remove code duplication
Nov 11, 2019
ab97a6b
Add support to filter by department id
Nov 12, 2019
29d0500
Code improve
Nov 12, 2019
862e93f
Fix message filter
Nov 12, 2019
d3be44e
Fix cronjob
Nov 12, 2019
b6dc580
Fix livechat agent service history
Nov 12, 2019
d4ab16e
Add a condition to read the department model.
renatobecker Nov 13, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/livechat/server/api/v1/room.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ API.v1.addRoute('livechat/room.transfer', {
// update visited page history to not expire
Messages.keepHistoryForToken(token);

if (!Promise.await(Livechat.transfer(room, guest, { roomId: rid, departmentId: department }))) {
if (!Promise.await(Livechat.transfer(room, guest, { roomId: rid, departmentId: department, transferedBy: guest._id }))) {
return API.v1.failure();
}

Expand Down
63 changes: 37 additions & 26 deletions app/livechat/server/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,32 +124,6 @@ Meteor.startup(function() {
i18nLabel: 'Livechat_room_count',
});

settings.add('Livechat_agent_leave_action', 'none', {
type: 'select',
group: 'Livechat',
values: [
{ key: 'none', i18nLabel: 'None' },
{ key: 'forward', i18nLabel: 'Forward' },
{ key: 'close', i18nLabel: 'Close' },
],
i18nLabel: 'How_to_handle_open_sessions_when_agent_goes_offline',
});

settings.add('Livechat_agent_leave_action_timeout', 60, {
type: 'int',
group: 'Livechat',
enableQuery: { _id: 'Livechat_agent_leave_action', value: { $ne: 'none' } },
i18nLabel: 'How_long_to_wait_after_agent_goes_offline',
i18nDescription: 'Time_in_seconds',
});

settings.add('Livechat_agent_leave_comment', '', {
type: 'string',
group: 'Livechat',
enableQuery: { _id: 'Livechat_agent_leave_action', value: 'close' },
i18nLabel: 'Comment_to_leave_on_closing_session',
});

settings.add('Livechat_webhookUrl', false, {
type: 'string',
group: 'Livechat',
Expand Down Expand Up @@ -445,4 +419,41 @@ Meteor.startup(function() {
i18nDescription: 'Data_processing_consent_text_description',
enableQuery: { _id: 'Livechat_force_accept_data_processing_consent', value: true },
});

settings.add('Livechat_agent_leave_action', 'none', {
type: 'select',
group: 'Livechat',
section: 'Sessions',
values: [
{ key: 'none', i18nLabel: 'None' },
{ key: 'forward', i18nLabel: 'Forward' },
{ key: 'close', i18nLabel: 'Close' },
],
i18nLabel: 'How_to_handle_open_sessions_when_agent_goes_offline',
});

settings.add('Livechat_agent_leave_action_timeout', 60, {
type: 'int',
group: 'Livechat',
section: 'Sessions',
enableQuery: { _id: 'Livechat_agent_leave_action', value: { $ne: 'none' } },
i18nLabel: 'How_long_to_wait_after_agent_goes_offline',
i18nDescription: 'Time_in_seconds',
});

settings.add('Livechat_agent_leave_comment', '', {
type: 'string',
group: 'Livechat',
section: 'Sessions',
enableQuery: { _id: 'Livechat_agent_leave_action', value: 'close' },
i18nLabel: 'Comment_to_leave_on_closing_session',
});

settings.add('Livechat_visitor_inactivity', 3600, {
Comment thread
renatobecker-zz marked this conversation as resolved.
Outdated
type: 'int',
group: 'Livechat',
section: 'Sessions',
i18nLabel: 'How_long_to_wait_to_consider_visitor_abandonment',
i18nDescription: 'Time_in_seconds',
});
});
58 changes: 58 additions & 0 deletions app/livechat/server/hooks/processRoomAbandonment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import moment from 'moment';

import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks';
import { LivechatRooms, Messages, LivechatOfficeHour } from '../../../models';

const getSecondsWhenOfficeHoursIsDisabled = (room, agentLastMessage) => moment(new Date(room.closedAt)).diff(moment(new Date(agentLastMessage.ts)), 'seconds');
const getOfficeHoursDictionary = () => LivechatOfficeHour.find().fetch().reduce((acc, day) => {
acc[day.day] = {
start: day.start,
finish: day.finish,
open: day.open,
};
return acc;
}, {});
const getSecondsSinceLastAgentResponse = (room, agentLastMessage) => {
if (!settings.get('Livechat_enable_office_hours')) {
return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
}

let totalSeconds = 0;
const officeDays = getOfficeHoursDictionary();
const endOfConversation = moment(new Date(room.closedAt));
const startOfInactivity = moment(new Date(agentLastMessage.ts));
const daysOfInactivity = endOfConversation.clone().startOf('day').diff(startOfInactivity.clone().startOf('day'), 'days');
const inactivityDay = moment(new Date(agentLastMessage.ts));

for (let index = 0; index <= daysOfInactivity; index++) {
const today = inactivityDay.clone().format('dddd');
const officeDay = officeDays[today];
const startTodaysOfficeHour = moment(officeDay.start, 'HH:mm').add(index, 'days');
const endTodaysOfficeHour = moment(officeDay.finish, 'HH:mm').add(index, 'days');
if (officeDays[today].open) {
const firstDayOfInactivity = startOfInactivity.clone().format('D') === inactivityDay.clone().format('D');
const lastDayOfInactivity = endOfConversation.clone().format('D') === inactivityDay.clone().format('D');

if (!firstDayOfInactivity && !lastDayOfInactivity) {
totalSeconds += endTodaysOfficeHour.clone().diff(startTodaysOfficeHour, 'seconds');
} else {
const end = endOfConversation.isBefore(endTodaysOfficeHour) ? endOfConversation : endTodaysOfficeHour;
const start = firstDayOfInactivity ? inactivityDay : startTodaysOfficeHour;
totalSeconds += end.clone().diff(start, 'seconds');
}
}
inactivityDay.add(1, 'days');
}
return totalSeconds;
};

callbacks.add('livechat.closeRoom', (room) => {
const closedByAgent = room.closer !== 'visitor';
const wasThelastMessageSentByAgent = room.lastMessage && !room.lastMessage.token;
if (closedByAgent && wasThelastMessageSentByAgent) {
const agentLastMessage = Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs).fetch()[0];
const secondsSinceLastAgentResponse = getSecondsSinceLastAgentResponse(room, agentLastMessage);
LivechatRooms.setVisitorInactivityInSecondsByRoomId(room._id, secondsSinceLastAgentResponse);
}
}, callbacks.priority.HIGH, 'process-room-abandonment');
13 changes: 13 additions & 0 deletions app/livechat/server/hooks/saveLastVisitorMessageTs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { callbacks } from '../../../callbacks';
import { LivechatRooms } from '../../../models';

callbacks.add('afterSaveMessage', function(message, room) {
if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.v && room.v.token)) {
return message;
}
const sentByVisitor = Boolean(message.token);
if (sentByVisitor) {
Comment thread
renatobecker-zz marked this conversation as resolved.
Outdated
LivechatRooms.setVisitorLastMessageTimestampByRoomId(room._id, message.ts);
}
return message;
}, callbacks.priority.HIGH, 'save-last-visitor-message-timestamp');
2 changes: 2 additions & 0 deletions app/livechat/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import './hooks/RDStation';
import './hooks/saveAnalyticsData';
import './hooks/sendToCRM';
import './hooks/sendToFacebook';
import './hooks/processRoomAbandonment';
import './hooks/saveLastVisitorMessageTs';
import './methods/addAgent';
import './methods/addManager';
import './methods/changeLivechatStatus';
Expand Down
28 changes: 27 additions & 1 deletion app/livechat/server/lib/Livechat.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,33 @@ export const Livechat = {
}
},

saveTransferHistory(room, transferData) {
const transferedToDepartment = Boolean(transferData.departmentId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use destructuring assignment here:

Suggested change
const transferedToDepartment = Boolean(transferData.departmentId);
const { departmentId: previousDepartment, servedBy: { _id: previousAgent } = {} } = room;
const { departmentId: nextDepartment, userId: nextAgent, transferedBy } = transferData;
const transferedToDepartment = Boolean(transferData.departmentId);

const transferedToAgent = Boolean(transferData.userId);
const transfer = {
Comment thread
renatobecker-zz marked this conversation as resolved.
transferedBy: transferData.transferedBy,
ts: new Date(),
};
if (transferedToDepartment) {
transfer.scope = 'department';
transfer.previousDepartment = room.departmentId;
transfer.nextDepartment = transferData.departmentId;
}
if (transferedToAgent) {
transfer.scope = 'agent';
transfer.previousAgent = room.servedBy && room.servedBy._id;
transfer.nextAgent = transferData.userId;
}
LivechatRooms.updateTransferHistoryByRoomId(room._id, transfer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LivechatRooms.updateTransferHistoryByRoomId(room._id, transfer);
return LivechatRooms.updateTransferHistoryByRoomId(room._id, transfer);

},

async transfer(room, guest, transferData) {
return RoutingManager.transferRoom(room, guest, transferData);
const result = await RoutingManager.transferRoom(room, guest, transferData);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const result = await RoutingManager.transferRoom(room, guest, transferData);
const result = await RoutingManager.transferRoom(room, guest, transferData) && this.saveTransferHistory(room, transferData);

if (!result) {
return false;
}
this.saveTransferHistory(room, transferData);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.saveTransferHistory(room, transferData);
return this.saveTransferHistory(room, transferData);

return true;
},

returnRoomAsInquiry(rid, departmentId) {
Expand Down Expand Up @@ -842,6 +867,7 @@ export const Livechat = {
status,
});
});
callbacks.runAsync('livechat.agentStatusChanged', { userId, status });

@renatobecker-zz renatobecker-zz Nov 8, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This callback doesn't depend on the 'Livechat_show_agent_info setting, so I suggest to run the callback at the beginning of the method, before testing the Livechat_show_agent_info setting.

},

allowAgentChangeServiceStatus(statusLivechat) {
Expand Down
1 change: 1 addition & 0 deletions app/livechat/server/methods/setDepartmentForVisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Meteor.methods({
const transferData = {
roomId,
departmentId,
transferedBy: visitor._id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
transferedBy: visitor._id,
transferredBy: visitor._id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the transferedBy prop is an object, right?

};

return Livechat.transfer(room, visitor, transferData);
Expand Down
1 change: 1 addition & 0 deletions app/livechat/server/methods/transfer.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Meteor.methods({
}

const guest = LivechatVisitors.findOneById(room.v && room.v._id);
transferData.transferedBy = Meteor.userId();

return Livechat.transfer(room, guest, transferData);
},
Expand Down
3 changes: 3 additions & 0 deletions app/livechat/server/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { LivechatInquiry } from '../lib/LivechatInquiry';
import { LivechatDepartment, LivechatDepartmentAgents } from '../../models/server';
import { RoutingManager } from './lib/RoutingManager';
import { createLivechatQueueView } from './lib/Helper';
import { LivechatSessionsMonitor } from './statistics/LivechatSessionsMonitor';
Comment thread
renatobecker-zz marked this conversation as resolved.
Outdated

Meteor.startup(() => {
roomTypes.setRoomFind('l', (_id) => LivechatRooms.findOneById(_id));
Expand Down Expand Up @@ -69,4 +70,6 @@ Meteor.startup(() => {
}, callbacks.priority.LOW, 'cant-leave-room');

createLivechatQueueView();

new LivechatSessionsMonitor().start();
Comment thread
renatobecker-zz marked this conversation as resolved.
Outdated
});
86 changes: 86 additions & 0 deletions app/livechat/server/statistics/LivechatSessionsMonitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import moment from 'moment';
import { Meteor } from 'meteor/meteor';

import { callbacks } from '../../../callbacks/server';
import { LivechatSessions, Sessions, Users } from '../../../models/server';

const formatDate = (dateTime = new Date()) => ({
date: parseInt(moment(dateTime).format('YYYYMMDD')),
});

export class LivechatSessionsMonitor {
Comment thread
renatobecker-zz marked this conversation as resolved.
Outdated
constructor() {
this._handleMeteorConnection = this._handleMeteorConnection.bind(this);
this._handleAgentStatusChanged = this._handleAgentStatusChanged.bind(this);
this._handleUserStatusLivechatChanged = this._handleUserStatusLivechatChanged.bind(this);
}

start() {
this._setupListeners();
}

_setupListeners() {
Meteor.onConnection(this._handleMeteorConnection);
callbacks.add('livechat.agentStatusChanged', this._handleAgentStatusChanged);
callbacks.add('livechat.setUserStatusLivechat', this._handleUserStatusLivechatChanged);
}

_handleMeteorConnection(connection) {
const session = Sessions.findOne({ sessionId: connection.id });
if (!session) {
return;
}
const user = Users.findOneById(session.userId);
if (user && user.status !== 'offline' && user.statusLivechat === 'available') {
this._createOrUpdateSession(user._id);
}
connection.onClose(() => {
if (session) {
this._updateSessionWhenAgentStop(session.userId);
}
});
}

_handleAgentStatusChanged({ userId, status }) {
const user = Users.findOneById(userId);
if (!user || user.statusLivechat !== 'available') {
return;
}

if (status !== 'offline') {
this._createOrUpdateSession(userId);
} else {
this._updateSessionWhenAgentStop(userId);
}
}

_handleUserStatusLivechatChanged({ userId, status }) {
const user = Users.findOneById(userId);
if (user && user.status === 'offline') {
return;
}

if (status === 'available') {
this._createOrUpdateSession(userId);
}
if (status === 'not-available') {
this._updateSessionWhenAgentStop(userId);
}
}

_createOrUpdateSession(userId) {
const data = { ...formatDate(), agentId: userId };
LivechatSessions.createOrUpdate(data);
}

_updateSessionWhenAgentStop(userId) {
const data = { ...formatDate(), agentId: userId };
const livechatSession = LivechatSessions.findOne(data);
if (livechatSession) {
const stopedAt = new Date();
const availableTime = moment(stopedAt).diff(moment(new Date(livechatSession.lastStartedAt)), 'seconds');
LivechatSessions.updateLastStoppedAt({ ...data, availableTime, lastStopedAt: stopedAt });
LivechatSessions.updateServiceHistory({ ...data, historyObject: { startedAt: livechatSession.lastStartedAt, stopedAt } });
}
}
}
2 changes: 2 additions & 0 deletions app/models/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import LivechatPageVisited from './models/LivechatPageVisited';
import LivechatRooms from './models/LivechatRooms';
import LivechatTrigger from './models/LivechatTrigger';
import LivechatVisitors from './models/LivechatVisitors';
import LivechatSessions from './models/LivechatSessions';
Comment thread
renatobecker-zz marked this conversation as resolved.
Outdated
import ReadReceipts from './models/ReadReceipts';

export { AppsLogsModel } from './models/apps-logs-model';
Expand Down Expand Up @@ -77,5 +78,6 @@ export {
LivechatRooms,
LivechatTrigger,
LivechatVisitors,
LivechatSessions,
Comment thread
renatobecker-zz marked this conversation as resolved.
Outdated
ReadReceipts,
};
39 changes: 39 additions & 0 deletions app/models/server/models/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,45 @@ export class LivechatRooms extends Base {

return this.remove(query);
}

updateTransferHistoryByRoomId(roomId, transfer) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
updateTransferHistoryByRoomId(roomId, transfer) {
updateTransferHistoryByRoomId(roomId, transferHistory) {

const query = {
_id: roomId,
};
const update = {
$addToSet: {
transferHistory: transfer,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
transferHistory: transfer,
transferHistory,

},
};

return this.update(query, update);
}

setVisitorLastMessageTimestampByRoomId(roomId, lastMessageTs) {
const query = {
_id: roomId,
};
const update = {
$set: {
'v.lastMessageTs': lastMessageTs,
},
};

return this.update(query, update);
}

setVisitorInactivityInSecondsByRoomId(roomId, visitorInactivity) {
const query = {
_id: roomId,
};
const update = {
$set: {
'metrics.visitorInactivity': visitorInactivity,
},
};

return this.update(query, update);
}
}

export default new LivechatRooms(Rooms.model, true);
Loading