diff --git a/app/livechat/server/lib/analytics/agents.js b/app/livechat/server/lib/analytics/agents.js new file mode 100644 index 0000000000000..c3d2b7a61031e --- /dev/null +++ b/app/livechat/server/lib/analytics/agents.js @@ -0,0 +1,48 @@ +import { Users } from '../../../../models/server/raw'; + +const findAllAverageServiceTimeAsync = async ({ + start, + end, + options = {}, +}) => { + if (!start || !end) { + throw new Error('"start" and "end" must be provided'); + } + return { + agents: await Users.findAllAverageServiceTime({ start, end, options }), + total: (await Users.findAllAverageServiceTime({ start, end })).length, + }; +}; + +const findAllServiceTimeAsync = async ({ + start, + end, + options = {}, +}) => { + if (!start || !end) { + throw new Error('"start" and "end" must be provided'); + } + return { + agents: await Users.findAllServiceTime({ start, end, options }), + total: (await Users.findAllServiceTime({ start, end })).length, + }; +}; + +const findAvailableServiceTimeHistoryAsync = async ({ + start, + end, + fullReport, + options = {}, +}) => { + if (!start || !end) { + throw new Error('"start" and "end" must be provided'); + } + return { + agents: await Users.findAvailableServiceTimeHistory({ start, end, fullReport, options }), + total: (await Users.findAvailableServiceTimeHistory({ start, end, fullReport })).length, + }; +}; + +export const findAllAverageServiceTime = ({ start, end, options }) => Promise.await(findAllAverageServiceTimeAsync({ start, end, options })); +export const findAllServiceTime = ({ start, end, options }) => Promise.await(findAllServiceTimeAsync({ start, end, options })); +export const findAvailableServiceTimeHistory = ({ start, end, fullReport, options }) => Promise.await(findAvailableServiceTimeHistoryAsync({ start, end, fullReport, options })); diff --git a/app/livechat/server/lib/analytics/departments.js b/app/livechat/server/lib/analytics/departments.js new file mode 100644 index 0000000000000..1dbf78449dde7 --- /dev/null +++ b/app/livechat/server/lib/analytics/departments.js @@ -0,0 +1,108 @@ +import { LivechatDepartment } from '../../../../models/server/raw'; + +const findAllRoomsAsync = async ({ + start, + end, + answered, + options = {}, +}) => { + if (!start || !end) { + throw new Error('"start" and "end" must be provided'); + } + return { + departments: await LivechatDepartment.findAllRooms({ start, answered, end, options }), + total: (await LivechatDepartment.findAllRooms({ start, answered, end })).length, + }; +}; + +const findAllAverageServiceTimeAsync = async ({ + start, + end, + options = {}, +}) => { + if (!start || !end) { + throw new Error('"start" and "end" must be provided'); + } + return { + departments: await LivechatDepartment.findAllAverageServiceTime({ start, end, options }), + total: (await LivechatDepartment.findAllAverageServiceTime({ start, end })).length, + }; +}; + +const findAllServiceTimeAsync = async ({ + start, + end, + options = {}, +}) => { + if (!start || !end) { + throw new Error('"start" and "end" must be provided'); + } + return { + departments: await LivechatDepartment.findAllServiceTime({ start, end, options }), + total: (await LivechatDepartment.findAllServiceTime({ start, end })).length, + }; +}; + +const findAllAverageWaitingTimeAsync = async ({ + start, + end, + options = {}, +}) => { + if (!start || !end) { + throw new Error('"start" and "end" must be provided'); + } + return { + departments: await LivechatDepartment.findAllAverageWaitingTime({ start, end, options }), + total: (await LivechatDepartment.findAllAverageWaitingTime({ start, end })).length, + }; +}; + +const findAllNumberOfTransferedRoomsAsync = async ({ + start, + end, + options = {}, +}) => { + if (!start || !end) { + throw new Error('"start" and "end" must be provided'); + } + return { + departments: await LivechatDepartment.findAllNumberOfTransferedRooms({ start, end, options }), + total: (await LivechatDepartment.findAllNumberOfTransferedRooms({ start, end })).length, + }; +}; + +const findAllNumberOfAbandonedRoomsAsync = async ({ + start, + end, + options = {}, +}) => { + if (!start || !end) { + throw new Error('"start" and "end" must be provided'); + } + return { + departments: await LivechatDepartment.findAllNumberOfAbandonedRooms({ start, end, options }), + total: (await LivechatDepartment.findAllNumberOfAbandonedRooms({ start, end })).length, + }; +}; + +const findPercentageOfAbandonedRoomsAsync = async ({ + start, + end, + options = {}, +}) => { + if (!start || !end) { + throw new Error('"start" and "end" must be provided'); + } + return { + departments: await LivechatDepartment.findPercentageOfAbandonedRooms({ start, end, options }), + total: (await LivechatDepartment.findPercentageOfAbandonedRooms({ start, end })).length, + }; +}; + +export const findAllAverageServiceTime = ({ start, end, options }) => Promise.await(findAllAverageServiceTimeAsync({ start, end, options })); +export const findAllRooms = ({ start, end, answered, options }) => Promise.await(findAllRoomsAsync({ start, end, answered, options })); +export const findAllServiceTime = ({ start, end, options }) => Promise.await(findAllServiceTimeAsync({ start, end, options })); +export const findAllAverageWaitingTime = ({ start, end, options }) => Promise.await(findAllAverageWaitingTimeAsync({ start, end, options })); +export const findAllNumberOfTransferedRooms = ({ start, end, options }) => Promise.await(findAllNumberOfTransferedRoomsAsync({ start, end, options })); +export const findAllNumberOfAbandonedRooms = ({ start, end, options }) => Promise.await(findAllNumberOfAbandonedRoomsAsync({ start, end, options })); +export const findPercentageOfAbandonedRooms = ({ start, end, options }) => Promise.await(findPercentageOfAbandonedRoomsAsync({ start, end, options })); diff --git a/app/models/server/raw/LivechatDepartment.js b/app/models/server/raw/LivechatDepartment.js index 295f56bc154b8..cfb66a6789fcb 100644 --- a/app/models/server/raw/LivechatDepartment.js +++ b/app/models/server/raw/LivechatDepartment.js @@ -1,5 +1,418 @@ import { BaseRaw } from './BaseRaw'; +import { getValue } from '../../../settings/server/raw'; export class LivechatDepartmentRaw extends BaseRaw { + findAllRooms({ start, end, answered, options = {} }) { + const roomsFilter = [ + { $gte: ['$$room.ts', new Date(start)] }, + { $lte: ['$$room.ts', new Date(end)] }, + ]; + if (answered !== undefined) { + roomsFilter.push({ [answered ? '$ne' : '$eq']: ['$$room.waitingResponse', true] }); + } + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: 'departmentId', + as: 'rooms', + }, + }; + const project = { + $project: { + name: 1, + description: 1, + enabled: 1, + rooms: { + $size: { + $filter: { + input: '$rooms', + as: 'room', + cond: { + $and: roomsFilter, + }, + }, + }, + }, + }, + }; + const params = [lookup, project]; + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + if (options.sort) { + params.push({ $sort: { name: 1 } }); + } + return this.col.aggregate(params).toArray(); + } + findAllAverageServiceTime({ start, end, options = {} }) { + const roomsFilter = [ + { $gte: ['$$room.ts', new Date(start)] }, + { $lte: ['$$room.ts', new Date(end)] }, + ]; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: 'departmentId', + as: 'rooms', + }, + }; + const projects = [ + { + $project: { + department: '$$ROOT', + rooms: { + $filter: { + input: '$rooms', + as: 'room', + cond: { + $and: roomsFilter, + }, + }, + }, + }, + }, + { + $project: { + department: '$department', + chats: { $size: '$rooms' }, + chatsDuration: { $sum: '$rooms.metrics.chatDuration' }, + }, + }, + { + $project: { + name: '$department.name', + description: '$department.description', + enabled: '$department.enabled', + averageServiceTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$chats', 0] }, 0, { $divide: ['$chatsDuration', '$chats'] }] } }, + }, + }]; + const params = [lookup, ...projects]; + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + if (options.sort) { + params.push({ $sort: { name: 1 } }); + } + return this.col.aggregate(params).toArray(); + } + + findAllServiceTime({ start, end, options = {} }) { + const roomsFilter = [ + { $gte: ['$$room.ts', new Date(start)] }, + { $lte: ['$$room.ts', new Date(end)] }, + ]; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: 'departmentId', + as: 'rooms', + }, + }; + const projects = [ + { + $project: { + department: '$$ROOT', + rooms: { + $filter: { + input: '$rooms', + as: 'room', + cond: { + $and: roomsFilter, + }, + }, + }, + }, + }, + { + $project: { + name: '$department.name', + description: '$department.description', + enabled: '$department.enabled', + chats: { $size: '$rooms' }, + chatsDuration: { $ceil: { $sum: '$rooms.metrics.chatDuration' } }, + }, + }]; + const params = [lookup, ...projects]; + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + if (options.sort) { + params.push({ $sort: { name: 1 } }); + } + return this.col.aggregate(params).toArray(); + } + + findAllAverageWaitingTime({ start, end, options = {} }) { + const roomsFilter = [ + { $gte: ['$$room.ts', new Date(start)] }, + { $lte: ['$$room.ts', new Date(end)] }, + { $ne: ['$$room.waitingResponse', true] }, + ]; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: 'departmentId', + as: 'rooms', + }, + }; + const projects = [{ + $project: { + department: '$$ROOT', + rooms: { + $filter: { + input: '$rooms', + as: 'room', + cond: { + $and: roomsFilter, + }, + }, + }, + }, + }, + { + $project: { + department: '$department', + chats: { $size: '$rooms' }, + chatsFirstResponses: { $sum: '$rooms.metrics.response.ft' }, + }, + }, + { + $project: { + name: '$department.name', + description: '$department.description', + enabled: '$department.enabled', + averageWaitingTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$chats', 0] }, 0, { $divide: ['$chatsFirstResponses', '$chats'] }] } }, + }, + }]; + const params = [lookup, ...projects]; + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + if (options.sort) { + params.push({ $sort: { name: 1 } }); + } + return this.col.aggregate(params).toArray(); + } + + findAllNumberOfTransferedRooms({ start, end, options = {} }) { + const roomsFilter = [ + { $gte: ['$$room.ts', new Date(start)] }, + { $lte: ['$$room.ts', new Date(end)] }, + ]; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: 'departmentId', + as: 'rooms', + }, + }; + const projectRooms = { + $project: { + department: '$$ROOT', + rooms: { + $filter: { + input: '$rooms', + as: 'room', + cond: { + $and: roomsFilter, + }, + }, + }, + }, + }; + const projectTransfersSize = { + $project: { + department: '$department', + transfers: { $size: { $ifNull: ['$rooms.transferHistory', []] } }, + }, + }; + const group = { + $group: { + _id: { + departmentId: '$department._id', + name: '$department.name', + description: '$department.description', + enabled: '$department.enabled', + }, + numberOfTransferedRooms: { $sum: '$transfers' }, + }, + }; + const presentationProject = { + $project: { + _id: '$_id.departmentId', + name: '$_id.name', + description: '$_id.description', + enabled: '$_id.enabled', + numberOfTransferedRooms: 1, + }, + }; + const unwind = { + $unwind: { + path: '$rooms', + preserveNullAndEmptyArrays: true, + }, + }; + const params = [lookup, projectRooms, unwind, projectTransfersSize, group, presentationProject]; + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + if (options.sort) { + params.push({ $sort: { name: 1 } }); + } + return this.col.aggregate(params).toArray(); + } + + async findAllNumberOfAbandonedRooms({ start, end, options = {} }) { + const roomsFilter = [ + { $gte: ['$$room.ts', new Date(start)] }, + { $lte: ['$$room.ts', new Date(end)] }, + { $gte: ['$$room.metrics.visitorInactivity', await getValue('Livechat_visitor_inactivity_timeout')] }, + ]; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: 'departmentId', + as: 'rooms', + }, + }; + const projects = [{ + $project: { + department: '$$ROOT', + rooms: { + $filter: { + input: '$rooms', + as: 'room', + cond: { + $and: roomsFilter, + }, + }, + }, + }, + }, + { + $project: { + name: '$department.name', + description: '$department.description', + enabled: '$department.enabled', + abandonedRooms: { $size: '$rooms' }, + }, + }]; + const params = [lookup, ...projects]; + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + if (options.sort) { + params.push({ $sort: { name: 1 } }); + } + return this.col.aggregate(params).toArray(); + } + + findPercentageOfAbandonedRooms({ start, end, options = {} }) { + const roomsFilter = [ + { $gte: ['$$room.ts', new Date(start)] }, + { $lte: ['$$room.ts', new Date(end)] }, + ]; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: 'departmentId', + as: 'rooms', + }, + }; + const projectRooms = { + $project: { + department: '$$ROOT', + rooms: { + $filter: { + input: '$rooms', + as: 'room', + cond: { + $and: roomsFilter, + }, + }, + }, + }, + }; + const unwind = { + $unwind: { + path: '$rooms', + preserveNullAndEmptyArrays: true, + }, + }; + const group = { + $group: { + _id: { + departmentId: '$department._id', + name: '$department.name', + description: '$department.description', + enabled: '$department.enabled', + }, + abandonedChats: { + $sum: { + $cond: [{ + $and: [ + { $ifNull: ['$rooms.metrics.visitorInactivity', false] }, + { $gte: ['$rooms.metrics.visitorInactivity', 1] }, + ], + }, 1, 0], + }, + }, + chats: { $sum: 1 }, + }, + }; + const presentationProject = { + $project: { + _id: '$_id.departmentId', + name: '$_id.name', + description: '$_id.description', + enabled: '$_id.enabled', + percentageOfAbandonedChats: { + $floor: { + $cond: [ + { $eq: ['$chats', 0] }, + 0, + { $divide: [{ $multiply: ['$abandonedChats', 100] }, '$chats'] }, + ], + }, + }, + }, + }; + const params = [lookup, projectRooms, unwind, group, presentationProject]; + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + if (options.sort) { + params.push({ $sort: { name: 1 } }); + } + return this.col.aggregate(params).toArray(); + } } diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index a9bffcdaff839..f68696a6a54b9 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -1,3 +1,5 @@ +import moment from 'moment'; + import { BaseRaw } from './BaseRaw'; import { Users } from '..'; @@ -86,4 +88,172 @@ export class UsersRaw extends BaseRaw { { $group: { _id: '$_id', tokens: { $push: '$tokens' } } }, ]).toArray(); } + + findAllAverageServiceTime({ start, end, options = {} }) { + const roomsFilter = [ + { $gte: ['$$room.ts', new Date(start)] }, + { $lte: ['$$room.ts', new Date(end)] }, + ]; + const match = { $match: { roles: { $in: ['livechat-agent'] } } }; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: 'servedBy._id', + as: 'rooms', + }, + }; + const projects = [ + { + $project: { + user: '$$ROOT', + rooms: { + $filter: { + input: '$rooms', + as: 'room', + cond: { + $and: roomsFilter, + }, + }, + }, + }, + }, + { + $project: { + user: '$user', + chats: { $size: '$rooms' }, + chatsDuration: { $sum: '$rooms.metrics.chatDuration' }, + }, + }, + { + $project: { + username: '$user.username', + name: '$user.name', + active: '$user.active', + averageServiceTimeInSeconds: { + $ceil: { + $cond: [ + { $eq: ['$chats', 0] }, + 0, + { $divide: ['$chatsDuration', '$chats'] }, + ], + }, + }, + }, + }]; + const params = [match, lookup, ...projects]; + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + if (options.sort) { + params.push({ $sort: { username: 1 } }); + } + return this.col.aggregate(params).toArray(); + } + + findAllServiceTime({ start, end, options = {} }) { + const roomsFilter = [ + { $gte: ['$$room.ts', new Date(start)] }, + { $lte: ['$$room.ts', new Date(end)] }, + ]; + const match = { $match: { roles: { $in: ['livechat-agent'] } } }; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: 'servedBy._id', + as: 'rooms', + }, + }; + const projects = [ + { + $project: { + user: '$$ROOT', + rooms: { + $filter: { + input: '$rooms', + as: 'room', + cond: { + $and: roomsFilter, + }, + }, + }, + }, + }, + { + $project: { + username: '$user.username', + name: '$user.name', + active: '$user.active', + chats: { $size: '$rooms' }, + chatsDuration: { $ceil: { $sum: '$rooms.metrics.chatDuration' } }, + }, + }]; + const params = [match, lookup, ...projects]; + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + if (options.sort) { + params.push({ $sort: { username: 1 } }); + } + return this.col.aggregate(params).toArray(); + } + + findAvailableServiceTimeHistory({ start, end, fullReport, options = {} }) { + const sessionFilter = [ + { $gte: ['$$session.date', parseInt(moment(start).format('YYYYMMDD'))] }, + { $lte: ['$$session.date', parseInt(moment(end).format('YYYYMMDD'))] }, + ]; + const match = { $match: { roles: { $in: ['livechat-agent'] } } }; + const lookup = { + $lookup: { + from: 'rocketchat_livechat_agent_activity', + localField: '_id', + foreignField: 'agentId', + as: 'sessions', + }, + }; + const sessionProject = { + $project: { + user: '$$ROOT', + sessions: { + $filter: { + input: '$sessions', + as: 'session', + cond: { + $and: sessionFilter, + }, + }, + }, + }, + }; + const presentationProject = { + $project: { + username: '$user.username', + name: '$user.name', + active: '$user.active', + availableTimeInSeconds: { $sum: '$sessions.availableTime' }, + }, + }; + if (fullReport) { + presentationProject.$project['sessions.serviceHistory'] = 1; + } + const params = [match, lookup, sessionProject, presentationProject]; + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + if (options.sort) { + params.push({ $sort: { username: 1 } }); + } + return this.col.aggregate(params).toArray(); + } }