diff --git a/app/livechat/imports/server/rest/rooms.js b/app/livechat/imports/server/rest/rooms.js index 905f8ff2b269..d7556006597a 100644 --- a/app/livechat/imports/server/rest/rooms.js +++ b/app/livechat/imports/server/rest/rooms.js @@ -4,45 +4,51 @@ import { hasPermission } from '../../../../authorization/server'; import { API } from '../../../../api'; import { findRooms } from '../../../server/api/lib/rooms'; +const validateDateParams = (property, date) => { + if (date) { + date = JSON.parse(date); + } + if (date && date.start && isNaN(Date.parse(date.start))) { + throw new Error(`The "${ property }.start" query parameter must be a valid date.`); + } + if (date && date.end && isNaN(Date.parse(date.end))) { + throw new Error(`The "${ property }.end" query parameter must be a valid date.`); + } + return date; +}; + API.v1.addRoute('livechat/rooms', { authRequired: true }, { get() { - try { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } - const { offset, count } = this.getPaginationItems(); - const { sort, fields } = this.parseJsonQuery(); - const { agents, departmentId, open, tags, roomName } = this.requestParams(); - let { createdAt, customFields, closedAt } = this.requestParams(); - check(agents, Match.Maybe([String])); - check(roomName, Match.Maybe(String)); - check(departmentId, Match.Maybe(String)); - check(open, Match.Maybe(String)); - check(tags, Match.Maybe([String])); + if (!hasPermission(this.userId, 'view-livechat-manager')) { + return API.v1.unauthorized(); + } + const { offset, count } = this.getPaginationItems(); + const { sort, fields } = this.parseJsonQuery(); + const { agents, departmentId, open, tags, roomName } = this.requestParams(); + let { createdAt, customFields, closedAt } = this.requestParams(); + check(agents, Match.Maybe([String])); + check(roomName, Match.Maybe(String)); + check(departmentId, Match.Maybe(String)); + check(open, Match.Maybe(String)); + check(tags, Match.Maybe([String])); - if (createdAt) { - createdAt = JSON.parse(createdAt); - } - if (closedAt) { - closedAt = JSON.parse(closedAt); - } - if (customFields) { - customFields = JSON.parse(customFields); - } + createdAt = validateDateParams('createdAt', createdAt); + closedAt = validateDateParams('closedAt', closedAt); - return API.v1.success(Promise.await(findRooms({ - agents, - roomName, - departmentId, - open: open && open === 'true', - createdAt, - closedAt, - tags, - customFields, - options: { offset, count, sort, fields }, - }))); - } catch (e) { - return API.v1.failure(e); + if (customFields) { + customFields = JSON.parse(customFields); } + + return API.v1.success(Promise.await(findRooms({ + agents, + roomName, + departmentId, + open: open && open === 'true', + createdAt, + closedAt, + tags, + customFields, + options: { offset, count, sort, fields }, + }))); }, }); diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index ac6734708319..36cf3352fb87 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -322,10 +322,13 @@ export const Livechat = { const extraData = callbacks.run('livechat.beforeCloseRoom', { room, options }); const now = new Date(); + const { _id: rid, servedBy } = room; + const serviceTimeDuration = servedBy && (now.getTime() - servedBy.ts) / 1000; const closeData = { closedAt: now, chatDuration: (now.getTime() - room.ts) / 1000, + ...serviceTimeDuration && { serviceTimeDuration }, ...extraData, }; @@ -343,7 +346,6 @@ export const Livechat = { }; } - const { _id: rid, servedBy } = room; LivechatRooms.closeByRoomId(rid, closeData); LivechatInquiry.removeByRoomId(rid); diff --git a/app/livechat/server/lib/analytics/agents.js b/app/livechat/server/lib/analytics/agents.js index c3d2b7a61031..734180ec52f1 100644 --- a/app/livechat/server/lib/analytics/agents.js +++ b/app/livechat/server/lib/analytics/agents.js @@ -1,4 +1,4 @@ -import { Users } from '../../../../models/server/raw'; +import { LivechatRooms, LivechatAgentActivity } from '../../../../models/server/raw'; const findAllAverageServiceTimeAsync = async ({ start, @@ -8,9 +8,10 @@ const findAllAverageServiceTimeAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await LivechatRooms.findAllAverageServiceTimeByAgents({ start, end, onlyCount: true }).toArray(); return { - agents: await Users.findAllAverageServiceTime({ start, end, options }), - total: (await Users.findAllAverageServiceTime({ start, end })).length, + agents: await LivechatRooms.findAllAverageServiceTimeByAgents({ start, end, options }).toArray(), + total: total.length ? total[0].total : 0, }; }; @@ -22,9 +23,10 @@ const findAllServiceTimeAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await LivechatRooms.findAllServiceTimeByAgent({ start, end, onlyCount: true }).toArray(); return { - agents: await Users.findAllServiceTime({ start, end, options }), - total: (await Users.findAllServiceTime({ start, end })).length, + agents: await LivechatRooms.findAllServiceTimeByAgent({ start, end, options }).toArray(), + total: total.length ? total[0].total : 0, }; }; @@ -37,9 +39,10 @@ const findAvailableServiceTimeHistoryAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await LivechatAgentActivity.findAvailableServiceTimeHistory({ start, end, fullReport, onlyCount: true }).toArray(); return { - agents: await Users.findAvailableServiceTimeHistory({ start, end, fullReport, options }), - total: (await Users.findAvailableServiceTimeHistory({ start, end, fullReport })).length, + agents: await LivechatAgentActivity.findAvailableServiceTimeHistory({ start, end, fullReport, options }).toArray(), + total: total.length ? total[0].total : 0, }; }; diff --git a/app/livechat/server/lib/analytics/departments.js b/app/livechat/server/lib/analytics/departments.js index dced223f0c28..fbcd952955d7 100644 --- a/app/livechat/server/lib/analytics/departments.js +++ b/app/livechat/server/lib/analytics/departments.js @@ -1,4 +1,4 @@ -import { LivechatRooms } from '../../../../models/server/raw'; +import { LivechatRooms, Messages } from '../../../../models/server/raw'; export const findAllRoomsAsync = async ({ start, @@ -10,9 +10,10 @@ export const findAllRoomsAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await LivechatRooms.findAllRooms({ start, answered, end, departmentId, onlyCount: true }).toArray(); return { - departments: await LivechatRooms.findAllRooms({ start, answered, end, departmentId, options }), - total: (await LivechatRooms.findAllRooms({ start, answered, end, departmentId })).length, + departments: await LivechatRooms.findAllRooms({ start, answered, end, departmentId, options }).toArray(), + total: total.length ? total[0].total : 0, }; }; @@ -25,9 +26,10 @@ export const findAllAverageOfChatDurationTimeAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await LivechatRooms.findAllAverageOfChatDurationTime({ start, end, departmentId, onlyCount: true }).toArray(); return { - departments: await LivechatRooms.findAllAverageOfChatDurationTime({ start, end, departmentId, options }), - total: (await LivechatRooms.findAllAverageOfChatDurationTime({ start, end, departmentId })).length, + departments: await LivechatRooms.findAllAverageOfChatDurationTime({ start, end, departmentId, options }).toArray(), + total: total.length ? total[0].total : 0, }; }; @@ -40,9 +42,10 @@ export const findAllAverageServiceTimeAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await LivechatRooms.findAllAverageOfServiceTime({ start, end, departmentId, onlyCount: true }).toArray(); return { - departments: await LivechatRooms.findAllAverageOfServiceTime({ start, end, departmentId, options }), - total: (await LivechatRooms.findAllAverageOfServiceTime({ start, end, departmentId })).length, + departments: await LivechatRooms.findAllAverageOfServiceTime({ start, end, departmentId, options }).toArray(), + total: total.length ? total[0].total : 0, }; }; @@ -55,9 +58,10 @@ export const findAllServiceTimeAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await LivechatRooms.findAllServiceTime({ start, end, departmentId, onlyCount: true }).toArray(); return { - departments: await LivechatRooms.findAllServiceTime({ start, end, departmentId, options }), - total: (await LivechatRooms.findAllServiceTime({ start, end, departmentId })).length, + departments: await LivechatRooms.findAllServiceTime({ start, end, departmentId, options }).toArray(), + total: total.length ? total[0].total : 0, }; }; @@ -70,9 +74,10 @@ export const findAllAverageWaitingTimeAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await LivechatRooms.findAllAverageWaitingTime({ start, end, departmentId, onlyCount: true }).toArray(); return { - departments: await LivechatRooms.findAllAverageWaitingTime({ start, end, departmentId, options }), - total: (await LivechatRooms.findAllAverageWaitingTime({ start, end, departmentId })).length, + departments: await LivechatRooms.findAllAverageWaitingTime({ start, end, departmentId, options }).toArray(), + total: total.length ? total[0].total : 0, }; }; @@ -85,9 +90,10 @@ export const findAllNumberOfTransferredRoomsAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await Messages.findAllNumberOfTransferredRooms({ start, end, departmentId, onlyCount: true }).toArray(); return { - departments: await LivechatRooms.findAllNumberOfTransferredRooms({ start, end, departmentId, options }), - total: (await LivechatRooms.findAllNumberOfTransferredRooms({ start, end, departmentId })).length, + departments: await Messages.findAllNumberOfTransferredRooms({ start, end, departmentId, options }).toArray(), + total: total.length ? total[0].total : 0, }; }; @@ -100,9 +106,10 @@ export const findAllNumberOfAbandonedRoomsAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await(await LivechatRooms.findAllNumberOfAbandonedRooms({ start, end, departmentId, onlyCount: true })).toArray(); return { - departments: await LivechatRooms.findAllNumberOfAbandonedRooms({ start, end, departmentId, options }), - total: (await LivechatRooms.findAllNumberOfAbandonedRooms({ start, end, departmentId })).length, + departments: await(await LivechatRooms.findAllNumberOfAbandonedRooms({ start, end, departmentId, options })).toArray(), + total: total.length ? total[0].total : 0, }; }; @@ -115,9 +122,10 @@ export const findPercentageOfAbandonedRoomsAsync = async ({ if (!start || !end) { throw new Error('"start" and "end" must be provided'); } + const total = await (await LivechatRooms.findPercentageOfAbandonedRooms({ start, end, departmentId, onlyCount: true })).toArray(); return { - departments: await LivechatRooms.findPercentageOfAbandonedRooms({ start, end, departmentId, options }), - total: (await LivechatRooms.findPercentageOfAbandonedRooms({ start, end, departmentId })).length, + departments: await(await LivechatRooms.findPercentageOfAbandonedRooms({ start, end, departmentId, options })).toArray(), + total: total.length ? total[0].total : 0, }; }; diff --git a/app/models/server/models/LivechatRooms.js b/app/models/server/models/LivechatRooms.js index f4de7632962b..1a93ce690a0a 100644 --- a/app/models/server/models/LivechatRooms.js +++ b/app/models/server/models/LivechatRooms.js @@ -12,6 +12,9 @@ export class LivechatRooms extends Base { this.tryEnsureIndex({ open: 1 }, { sparse: true }); this.tryEnsureIndex({ departmentId: 1 }, { sparse: true }); + this.tryEnsureIndex({ 'metrics.chatDuration': 1 }, { sparse: true }); + this.tryEnsureIndex({ 'metrics.serviceTimeDuration': 1 }, { sparse: true }); + this.tryEnsureIndex({ 'metrics.visitorInactivity': 1 }, { sparse: true }); } findLivechat(filter = {}, offset = 0, limit = 20) { @@ -421,7 +424,7 @@ export class LivechatRooms extends Base { closeByRoomId(roomId, closeInfo) { - const { closer, closedBy, closedAt, chatDuration, ...extraData } = closeInfo; + const { closer, closedBy, closedAt, chatDuration, serviceTimeDuration, ...extraData } = closeInfo; return this.update({ _id: roomId, @@ -432,6 +435,7 @@ export class LivechatRooms extends Base { closedBy, closedAt, 'metrics.chatDuration': chatDuration, + 'metrics.serviceTimeDuration': serviceTimeDuration, 'v.status': 'offline', ...extraData, }, diff --git a/app/models/server/raw/LivechatAgentActivity.js b/app/models/server/raw/LivechatAgentActivity.js index 653a1b994087..7a86cf1bac8a 100644 --- a/app/models/server/raw/LivechatAgentActivity.js +++ b/app/models/server/raw/LivechatAgentActivity.js @@ -1,3 +1,5 @@ +import moment from 'moment'; + import { BaseRaw } from './BaseRaw'; export class LivechatAgentActivityRaw extends BaseRaw { @@ -64,4 +66,58 @@ export class LivechatAgentActivityRaw extends BaseRaw { params.push(project); return this.col.aggregate(params).toArray(); } + + findAvailableServiceTimeHistory({ start, end, fullReport, onlyCount = false, options = {} }) { + const match = { + $match: { + date: { + $gte: parseInt(moment(start).format('YYYYMMDD')), + $lte: parseInt(moment(end).format('YYYYMMDD')), + }, + }, + }; + const lookup = { + $lookup: { + from: 'users', + localField: 'agentId', + foreignField: '_id', + as: 'user', + }, + }; + const unwind = { + $unwind: { + path: '$user', + }, + }; + const group = { + $group: { + _id: { _id: '$user._id', username: '$user.username' }, + serviceHistory: { $first: '$serviceHistory' }, + availableTimeInSeconds: { $sum: '$availableTime' }, + }, + }; + const project = { + $project: { + _id: 0, + username: '$_id.username', + availableTimeInSeconds: 1, + }, + }; + if (fullReport) { + project.$project.serviceHistory = 1; + } + const sort = { $sort: options.sort || { username: 1 } }; + const params = [match, lookup, unwind, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + return this.col.aggregate(params, { allowDiskUse: true }); + } } diff --git a/app/models/server/raw/LivechatRooms.js b/app/models/server/raw/LivechatRooms.js index da9ec4282e41..e996eac6e368 100644 --- a/app/models/server/raw/LivechatRooms.js +++ b/app/models/server/raw/LivechatRooms.js @@ -94,7 +94,7 @@ export class LivechatRoomsRaw extends BaseRaw { return this.col.aggregate(params).toArray(); } - async findAllNumberOfAbandonedRooms({ start, end, departmentId, options = {} }) { + async findAllNumberOfAbandonedRooms({ start, end, departmentId, onlyCount = false, options = {} }) { const match = { $match: { t: 'l', @@ -103,156 +103,98 @@ export class LivechatRoomsRaw extends BaseRaw { closedAt: { $lte: new Date(end) }, }, }; - const lookup = { - $lookup: { - from: 'rocketchat_livechat_department', - localField: 'departmentId', - foreignField: '_id', - as: 'departments', - }, - }; - const unwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; const group = { $group: { _id: { _id: null, - departmentId: '$departments._id', - name: '$departments.name', + departmentId: '$departmentId', }, - rooms: { $push: '$$ROOT' }, + abandonedRooms: { $sum: 1 }, }, }; - const presentationProject = { + const project = { $project: { _id: { $ifNull: ['$_id.departmentId', null] }, - name: { $ifNull: ['$_id.name', null] }, - abandonedRooms: { $size: '$rooms' }, + abandonedRooms: 1, }, }; - const firstParams = [match, lookup, unwind]; if (departmentId) { - firstParams.push({ - $match: { - 'departments._id': departmentId, - }, - }); + match.$match.departmentId = departmentId; } const sort = { $sort: options.sort || { name: 1 } }; - const params = [...firstParams, group, presentationProject, sort]; + const params = [match, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } if (options.offset) { params.push({ $skip: options.offset }); } if (options.count) { params.push({ $limit: options.count }); } - return this.col.aggregate(params).toArray(); + return this.col.aggregate(params); } - async findPercentageOfAbandonedRooms({ start, end, departmentId, options = {} }) { + async findPercentageOfAbandonedRooms({ start, end, departmentId, onlyCount = false, options = {} }) { const match = { $match: { t: 'l', ts: { $gte: new Date(start), $lte: new Date(end) }, }, }; - const lookup = { - $lookup: { - from: 'rocketchat_livechat_department', - localField: 'departmentId', - foreignField: '_id', - as: 'departments', - }, - }; - const departmentsUnwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; - const departmentsGroup = { - $group: { - _id: { - _id: null, - departmentId: '$departments._id', - name: '$departments.name', - }, - rooms: { $push: '$$ROOT' }, - }, - }; - const departmentsProject = { - $project: { - _id: '$_id.departmentId', - name: '$_id.name', - rooms: 1, - sizeOfRooms: { $size: '$rooms' }, - }, - }; - const roomsUnwind = { - $unwind: { - path: '$rooms', - preserveNullAndEmptyArrays: true, - }, - }; - const roomsGroup = { + const group = { $group: { _id: { _id: null, - departmentId: '$_id', - name: '$name', - sizeOfRooms: '$sizeOfRooms', + departmentId: '$departmentId', }, + rooms: { $sum: 1 }, abandonedChats: { $sum: { $cond: [{ $and: [ - { $ifNull: ['$rooms.metrics.visitorInactivity', false] }, - { $gte: ['$rooms.metrics.visitorInactivity', await getValue('Livechat_visitor_inactivity_timeout')] }, + { $ifNull: ['$metrics.visitorInactivity', false] }, + { $gte: ['$metrics.visitorInactivity', await getValue('Livechat_visitor_inactivity_timeout')] }, ], }, 1, 0], }, }, }, }; - const presentationProject = { + const project = { $project: { _id: { $ifNull: ['$_id.departmentId', null] }, - name: { $ifNull: ['$_id.name', null] }, percentageOfAbandonedChats: { $floor: { $cond: [ - { $eq: ['$_id.sizeOfRooms', 0] }, + { $eq: ['$rooms', 0] }, 0, - { $divide: [{ $multiply: ['$abandonedChats', 100] }, '$_id.sizeOfRooms'] }, + { $divide: [{ $multiply: ['$abandonedChats', 100] }, '$rooms'] }, ], }, }, }, }; - const firstParams = [match, lookup, departmentsUnwind]; if (departmentId) { - firstParams.push({ - $match: { - 'departments._id': departmentId, - }, - }); + match.$match.departmentId = departmentId; } const sort = { $sort: options.sort || { name: 1 } }; - const params = [...firstParams, departmentsGroup, departmentsProject, roomsUnwind, roomsGroup, presentationProject, sort]; + const params = [match, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } if (options.offset) { params.push({ $skip: options.offset }); } if (options.count) { params.push({ $limit: options.count }); } - return this.col.aggregate(params).toArray(); + return this.col.aggregate(params); } - findAllAverageOfChatDurationTime({ start, end, departmentId, options = {} }) { + findAllAverageOfChatDurationTime({ start, end, departmentId, onlyCount = false, options = {} }) { const match = { $match: { t: 'l', @@ -260,84 +202,41 @@ export class LivechatRoomsRaw extends BaseRaw { closedAt: { $lte: new Date(end) }, }, }; - const lookup = { - $lookup: { - from: 'rocketchat_livechat_department', - localField: 'departmentId', - foreignField: '_id', - as: 'departments', - }, - }; - const departmentsUnwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; - const departmentsGroup = { - $group: { - _id: { - _id: null, - departmentId: '$departments._id', - name: '$departments.name', - }, - rooms: { $push: '$$ROOT' }, - }, - }; - const departmentsProject = { - $project: { - _id: '$_id.departmentId', - name: '$_id.name', - rooms: 1, - sizeOfRooms: { $size: '$rooms' }, - }, - }; - const roomsUnwind = { - $unwind: { - path: '$rooms', - preserveNullAndEmptyArrays: true, - }, - }; - const roomsGroup = { + const group = { $group: { _id: { _id: null, - departmentId: '$_id', - name: '$name', - sizeOfRooms: '$sizeOfRooms', - }, - chatsDuration: { - $sum: '$rooms.metrics.chatDuration', + departmentId: '$departmentId', }, + rooms: { $sum: 1 }, + chatsDuration: { $sum: '$metrics.chatDuration' }, }, }; - const presentationProject = { + const project = { $project: { _id: { $ifNull: ['$_id.departmentId', null] }, - name: { $ifNull: ['$_id.name', null] }, - averageChatDurationTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$_id.sizeOfRooms', 0] }, 0, { $divide: ['$chatsDuration', '$_id.sizeOfRooms'] }] } }, + averageChatDurationTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$rooms', 0] }, 0, { $divide: ['$chatsDuration', '$rooms'] }] } }, }, }; - const firstParams = [match, lookup, departmentsUnwind]; if (departmentId) { - firstParams.push({ - $match: { - 'departments._id': departmentId, - }, - }); + match.$match.departmentId = departmentId; } const sort = { $sort: options.sort || { name: 1 } }; - const params = [...firstParams, departmentsGroup, departmentsProject, roomsUnwind, roomsGroup, presentationProject, sort]; + const params = [match, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } if (options.offset) { params.push({ $skip: options.offset }); } if (options.count) { params.push({ $limit: options.count }); } - return this.col.aggregate(params).toArray(); + return this.col.aggregate(params); } - findAllAverageWaitingTime({ start, end, departmentId, options = {} }) { + findAllAverageWaitingTime({ start, end, departmentId, onlyCount = false, options = {} }) { const match = { $match: { t: 'l', @@ -345,84 +244,41 @@ export class LivechatRoomsRaw extends BaseRaw { waitingResponse: { $ne: true }, }, }; - const lookup = { - $lookup: { - from: 'rocketchat_room', - localField: '_id', - foreignField: 'departmentId', - as: 'rooms', - }, - }; - const departmentsUnwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; - const departmentsGroup = { - $group: { - _id: { - _id: null, - departmentId: '$departments._id', - name: '$departments.name', - }, - rooms: { $push: '$$ROOT' }, - }, - }; - const departmentsProject = { - $project: { - _id: '$_id.departmentId', - name: '$_id.name', - rooms: 1, - sizeOfRooms: { $size: '$rooms' }, - }, - }; - const roomsUnwind = { - $unwind: { - path: '$rooms', - preserveNullAndEmptyArrays: true, - }, - }; - const roomsGroup = { + const group = { $group: { _id: { _id: null, - departmentId: '$_id', - name: '$name', - sizeOfRooms: '$sizeOfRooms', - }, - chatsFirstResponses: { - $sum: '$rooms.metrics.response.ft', + departmentId: '$departmentId', }, + rooms: { $sum: 1 }, + chatsFirstResponses: { $sum: '$metrics.response.ft' }, }, }; - const presentationProject = { + const project = { $project: { _id: { $ifNull: ['$_id.departmentId', null] }, - name: { $ifNull: ['$_id.name', null] }, - averageWaitingTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$_id.sizeOfRooms', 0] }, 0, { $divide: ['$chatsFirstResponses', '$_id.sizeOfRooms'] }] } }, + averageWaitingTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$rooms', 0] }, 0, { $divide: ['$chatsFirstResponses', '$rooms'] }] } }, }, }; - const firstParams = [match, lookup, departmentsUnwind]; if (departmentId) { - firstParams.push({ - $match: { - 'departments._id': departmentId, - }, - }); + match.$match.departmentId = departmentId; } const sort = { $sort: options.sort || { name: 1 } }; - const params = [...firstParams, departmentsGroup, departmentsProject, roomsUnwind, roomsGroup, presentationProject, sort]; + const params = [match, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } if (options.offset) { params.push({ $skip: options.offset }); } if (options.count) { params.push({ $limit: options.count }); } - return this.col.aggregate(params).toArray(); + return this.col.aggregate(params); } - findAllRooms({ start, end, answered, departmentId, options = {} }) { + findAllRooms({ start, end, answered, departmentId, onlyCount = false, options = {} }) { const match = { $match: { t: 'l', @@ -432,149 +288,81 @@ export class LivechatRoomsRaw extends BaseRaw { if (answered !== undefined) { match.$match.waitingResponse = { [answered ? '$ne' : '$eq']: true }; } - const lookup = { - $lookup: { - from: 'rocketchat_livechat_department', - localField: 'departmentId', - foreignField: '_id', - as: 'departments', - }, - }; - const departmentsUnwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; - const departmentsGroup = { + const group = { $group: { _id: { _id: null, - departmentId: '$departments._id', - name: '$departments.name', + departmentId: '$departmentId', }, - rooms: { $push: '$$ROOT' }, + rooms: { $sum: 1 }, }, }; - const projects = [ - { - $project: { - _id: '$_id.departmentId', - name: '$_id.name', - rooms: { $size: '$rooms' }, - }, - }, - { - $project: { - _id: { $ifNull: ['$_id', null] }, - name: { $ifNull: ['$name', null] }, - rooms: 1, - }, + const project = { + $project: { + _id: { $ifNull: ['$_id.departmentId', null] }, + rooms: 1, }, - ]; - const firstParams = [match, lookup, departmentsUnwind]; + }; if (departmentId) { - firstParams.push({ - $match: { - 'departments._id': departmentId, - }, - }); + match.$match.departmentId = departmentId; } const sort = { $sort: options.sort || { name: 1 } }; - const params = [...firstParams, departmentsGroup, ...projects, sort]; + const params = [match, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } if (options.offset) { params.push({ $skip: options.offset }); } if (options.count) { params.push({ $limit: options.count }); } - return this.col.aggregate(params).toArray(); + return this.col.aggregate(params); } - findAllServiceTime({ start, end, departmentId, options = {} }) { + findAllServiceTime({ start, end, departmentId, onlyCount = false, options = {} }) { const match = { $match: { t: 'l', ts: { $gte: new Date(start) }, closedAt: { $lte: new Date(end) }, + 'metrics.serviceTimeDuration': { $exists: true }, }, }; - const lookup = { - $lookup: { - from: 'rocketchat_livechat_department', - localField: 'departmentId', - foreignField: '_id', - as: 'departments', - }, - }; - const departmentsUnwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; - const departmentsGroup = { + const group = { $group: { _id: { _id: null, - departmentId: '$departments._id', - name: '$departments.name', + departmentId: '$departmentId', }, - rooms: { $push: '$$ROOT' }, + rooms: { $sum: 1 }, + serviceTimeDuration: { $sum: '$metrics.serviceTimeDuration' }, }, }; - const departmentsProject = { - $project: { - _id: '$_id.departmentId', - name: '$_id.name', - rooms: 1, - sizeOfRooms: { $size: '$rooms' }, - }, - }; - const roomsUnwind = { - $unwind: { - path: '$rooms', - preserveNullAndEmptyArrays: true, - }, - }; - const presentationProject = { + const project = { $project: { _id: { $ifNull: ['$_id.departmentId', null] }, - name: { $ifNull: ['$_id.name', null] }, - chats: '$_id.sizeOfRooms', - chatsDuration: { $ceil: '$chatsDuration' }, + chats: '$rooms', + serviceTimeDuration: { $ceil: '$serviceTimeDuration' }, }, }; - const roomsGroup = { - $group: { - _id: { - _id: null, - departmentId: '$_id', - name: '$name', - sizeOfRooms: '$sizeOfRooms', - }, - chatsDuration: { - $sum: '$rooms.metrics.chatDuration', - }, - }, - }; - const firstParams = [match, lookup, departmentsUnwind]; if (departmentId) { - firstParams.push({ - $match: { - 'departments._id': departmentId, - }, - }); + match.$match.departmentId = departmentId; } const sort = { $sort: options.sort || { name: 1 } }; - const params = [...firstParams, departmentsGroup, departmentsProject, roomsUnwind, roomsGroup, presentationProject, sort]; + const params = [match, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } if (options.offset) { params.push({ $skip: options.offset }); } if (options.count) { params.push({ $limit: options.count }); } - return this.col.aggregate(params).toArray(); + return this.col.aggregate(params); } findAllNumberOfTransferredRooms({ start, end, departmentId, options = {} }) { @@ -682,7 +470,7 @@ export class LivechatRoomsRaw extends BaseRaw { if (options.count) { params.push({ $limit: options.count }); } - return this.col.aggregate(params).toArray(); + return this.col.aggregate(params, { allowDiskUse: true }).toArray(); } countAllOpenChatsBetweenDate({ start, end, departmentId }) { @@ -957,6 +745,7 @@ export class LivechatRoomsRaw extends BaseRaw { $match: { t: 'l', ts: { $gte: new Date(start), $lte: new Date(end) }, + 'metrics.chatDuration': { $exists: true }, }, }; const group = { @@ -997,7 +786,7 @@ export class LivechatRoomsRaw extends BaseRaw { return this.col.aggregate([match, group, project]).toArray(); } - findAllAverageOfServiceTime({ start, end, departmentId, options = {} }) { + findAllAverageOfServiceTime({ start, end, departmentId, onlyCount = false, options = {} }) { const match = { $match: { t: 'l', @@ -1006,87 +795,38 @@ export class LivechatRoomsRaw extends BaseRaw { 'servedBy.ts': { $exists: true }, }, }; - const lookup = { - $lookup: { - from: 'rocketchat_livechat_department', - localField: 'departmentId', - foreignField: '_id', - as: 'departments', - }, - }; - const departmentsUnwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; - const departmentsGroup = { - $group: { - _id: { - _id: null, - departmentId: '$departments._id', - name: '$departments.name', - }, - rooms: { $push: '$$ROOT' }, - }, - }; - const departmentsProject = { - $project: { - _id: '$_id.departmentId', - name: '$_id.name', - rooms: 1, - sizeOfRooms: { $size: '$rooms' }, - }, - }; - const roomsUnwind = { - $unwind: { - path: '$rooms', - preserveNullAndEmptyArrays: true, - }, - }; - const serviceTimeProject = { - $project: { - name: 1, - sizeOfRooms: 1, - allServiceTime: { $divide: [{ $subtract: ['$rooms.responseBy.lastMessageTs', '$rooms.servedBy.ts'] }, 1000] }, - }, - }; - const roomsGroup = { + const group = { $group: { _id: { _id: null, - departmentId: '$_id', - name: '$name', - sizeOfRooms: '$sizeOfRooms', - allServiceTime: '$allServiceTime', + departmentId: '$departmentId', }, + rooms: { $sum: 1 }, + allServiceTime: { $sum: { $divide: [{ $subtract: ['$responseBy.lastMessageTs', '$servedBy.ts'] }, 1000] } }, }, - }; - const presentationProject = { + const project = { $project: { _id: { $ifNull: ['$_id.departmentId', null] }, - name: { $ifNull: ['$_id.name', null] }, - averageServiceTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$_id.sizeOfRooms', 0] }, 0, { $divide: ['$_id.allServiceTime', '$_id.sizeOfRooms'] }] } }, + averageServiceTimeInSeconds: { $ceil: { $cond: [{ $eq: ['$rooms', 0] }, 0, { $divide: ['$allServiceTime', '$rooms'] }] } }, }, }; - const firstParams = [match, lookup, departmentsUnwind]; if (departmentId) { - firstParams.push({ - $match: { - 'departments._id': departmentId, - }, - }); + match.$match.departmentId = departmentId; } const sort = { $sort: options.sort || { name: 1 } }; - const params = [...firstParams, departmentsGroup, departmentsProject, roomsUnwind, serviceTimeProject, roomsGroup, presentationProject, sort]; + const params = [match, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } if (options.offset) { params.push({ $skip: options.offset }); } if (options.count) { params.push({ $limit: options.count }); } - return this.col.aggregate(params).toArray(); + return this.col.aggregate(params); } findByVisitorId(visitorId, options) { @@ -1140,4 +880,97 @@ export class LivechatRoomsRaw extends BaseRaw { } return this.find(query, { sort: options.sort || { name: 1 }, skip: options.offset, limit: options.count }); } + + findAllServiceTimeByAgent({ start, end, onlyCount = false, options = {} }) { + const match = { + $match: { + t: 'l', + 'servedBy._id': { $exists: true }, + 'metrics.serviceTimeDuration': { $exists: true }, + ts: { + $gte: start, + $lte: end, + }, + }, + }; + const group = { + $group: { + _id: { _id: '$servedBy._id', username: '$servedBy.username' }, + chats: { $sum: 1 }, + serviceTimeDuration: { $sum: '$metrics.serviceTimeDuration' }, + }, + }; + const project = { + $project: { + _id: '$_id._id', + username: '$_id.username', + chats: 1, + serviceTimeDuration: { $ceil: '$serviceTimeDuration' }, + }, + }; + const sort = { $sort: options.sort || { username: 1 } }; + const params = [match, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + return this.col.aggregate(params); + } + + findAllAverageServiceTimeByAgents({ start, end, onlyCount = false, options = {} }) { + const match = { + $match: { + t: 'l', + 'servedBy._id': { $exists: true }, + 'metrics.serviceTimeDuration': { $exists: true }, + ts: { + $gte: start, + $lte: end, + }, + }, + }; + const group = { + $group: { + _id: { _id: '$servedBy._id', username: '$servedBy.username' }, + chats: { $sum: 1 }, + serviceTimeDuration: { $sum: '$metrics.serviceTimeDuration' }, + }, + }; + const project = { + $project: { + _id: '$_id._id', + username: '$_id.username', + name: '$_id.name', + active: '$_id.active', + averageServiceTimeInSeconds: { + $ceil: { + $cond: [ + { $eq: ['$chats', 0] }, + 0, + { $divide: ['$serviceTimeDuration', '$chats'] }, + ], + }, + }, + }, + }; + const sort = { $sort: options.sort || { username: 1 } }; + const params = [match, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + return this.col.aggregate(params); + } } diff --git a/app/models/server/raw/Messages.js b/app/models/server/raw/Messages.js index c1d4d7e7a570..fcda4e0c85ca 100644 --- a/app/models/server/raw/Messages.js +++ b/app/models/server/raw/Messages.js @@ -47,4 +47,63 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } + + findAllNumberOfTransferredRooms({ start, end, departmentId, onlyCount = false, options = {} }) { + const match = { + $match: { + t: 'livechat_transfer_history', + ts: { $gte: new Date(start), $lte: new Date(end) }, + }, + }; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: 'rid', + foreignField: '_id', + as: 'room', + }, + }; + const unwind = { + $unwind: { + path: '$room', + preserveNullAndEmptyArrays: true, + }, + }; + const group = { + $group: { + _id: { + _id: null, + departmentId: '$room.departmentId', + }, + numberOfTransferredRooms: { $sum: 1 }, + }, + }; + const project = { + $project: { + _id: { $ifNull: ['$_id.departmentId', null] }, + numberOfTransferredRooms: 1, + }, + }; + const firstParams = [match, lookup, unwind]; + if (departmentId) { + firstParams.push({ + $match: { + 'room.departmentId': departmentId, + }, + }); + } + const sort = { $sort: options.sort || { name: 1 } }; + const params = [...firstParams, group, project, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + return this.col.aggregate(params, { allowDiskUse: true }); + } } diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index 1dad38cb27b1..1984357c52db 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -1,4 +1,3 @@ -import moment from 'moment'; import s from 'underscore.string'; import { BaseRaw } from './BaseRaw'; @@ -98,168 +97,6 @@ export class UsersRaw extends BaseRaw { ]).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 sort = { $sort: options.sort || { username: 1 } }; - const params = [match, lookup, ...projects, sort]; - if (options.offset) { - params.push({ $skip: options.offset }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - 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 sort = { $sort: options.sort || { username: 1 } }; - const params = [match, lookup, ...projects, sort]; - if (options.offset) { - params.push({ $skip: options.offset }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - 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 sort = { $sort: options.sort || { username: 1 } }; - const params = [match, lookup, sessionProject, presentationProject, sort]; - if (options.offset) { - params.push({ $skip: options.offset }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params).toArray(); - } - findActiveByUsernameOrNameRegexWithExceptionsAndConditions(searchTerm, exceptions, conditions, options) { if (exceptions == null) { exceptions = []; } if (conditions == null) { conditions = {}; }