-
-
-
-
- | {{_ "Agent"}} |
- {{_ "Count"}} |
- {{_ "Order"}} |
-
-
-
- {{#each users}}
-
- | {{username}} |
- {{count}} |
- {{order}} |
-
- {{else}}
-
- | {{_ "Nobody_available"}} |
-
- {{/each}}
-
-
+
- {{/each}}
+
+
+
+ {{#table fixed='true' onScroll=onTableScroll onResize=onTableResize onSort=onTableSort}}
+
+
+ |
+ {{_ "Served_By"}}
+ |
+
+ {{_ "Department"}}
+ |
+
+ {{_ "Total"}}
+ |
+
+ {{_ "Status"}}
+ |
+
+
+
+ {{#each queue}}
+
+
+
+ {{> avatar username=user.username}}
+
+ {{user.username}}
+
+
+ |
+ {{department.name}} |
+ {{chats}} |
+ {{user.status}} |
+
+ {{/each}}
+ {{#if isLoading}}
+
+ | {{> loading}} |
+
+ {{/if}}
+
+ {{/table}}
+
+ {{#if hasMore}}
+
+
+
+ {{/if}}
{{else}}
{{_ "You_are_not_authorized_to_view_this_page"}}
{{/if}}
diff --git a/app/livechat/client/views/app/livechatQueue.js b/app/livechat/client/views/app/livechatQueue.js
index e7bc76c7c9c..848afa2551f 100644
--- a/app/livechat/client/views/app/livechatQueue.js
+++ b/app/livechat/client/views/app/livechatQueue.js
@@ -5,45 +5,45 @@ import { Template } from 'meteor/templating';
import { settings } from '../../../../settings';
import { hasPermission } from '../../../../authorization';
import { Users } from '../../../../models';
-import { LivechatQueueUser } from '../../collections/LivechatQueueUser';
import './livechatQueue.html';
import { APIClient } from '../../../../utils/client';
+const QUEUE_COUNT = 50;
+
Template.livechatQueue.helpers({
departments() {
return Template.instance().departments.get().filter((department) => department.enabled === true);
},
-
- users() {
- const users = [];
-
- const showOffline = Template.instance().showOffline.get();
-
- LivechatQueueUser.find({
- departmentId: this._id,
- }, {
- sort: {
- count: 1,
- order: 1,
- username: 1,
- },
- }).forEach((user) => {
- const options = { fields: { _id: 1 } };
- const userFilter = { _id: user.agentId, status: { $ne: 'offline' } };
- const agent = Template.instance().agents.get().find((agent) => agent._id === user.agentId && agent.statusLivechat === 'available');
-
- if (showOffline[this._id] || (Meteor.users.findOne(userFilter, options) && agent)) {
- users.push(user);
- }
- });
-
- return users;
+ onSelectAgents() {
+ return Template.instance().onSelectAgents;
+ },
+ agentModifier() {
+ return (filter, text = '') => {
+ const f = filter.get();
+ return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `
${ part }`) }`;
+ };
+ },
+ selectedAgents() {
+ return Template.instance().selectedAgents.get();
+ },
+ onClickTagAgent() {
+ return Template.instance().onClickTagAgent;
+ },
+ queue() {
+ return Template.instance().queue.get();
+ },
+ isLoading() {
+ return Template.instance().isLoading.get();
},
-
hasPermission() {
const user = Users.findOne(Meteor.userId(), { fields: { statusLivechat: 1 } });
return hasPermission(Meteor.userId(), 'view-livechat-queue') || (user.statusLivechat === 'available' && settings.get('Livechat_show_queue_list_link'));
},
+ hasMore() {
+ const instance = Template.instance();
+ const queue = instance.queue.get();
+ return instance.total.get() > queue.length;
+ },
});
Template.livechatQueue.events({
@@ -54,17 +54,67 @@ Template.livechatQueue.events({
instance.showOffline.set(showOffline);
},
+ 'submit form'(event, instance) {
+ event.preventDefault();
+ instance.queue.set([]);
+ instance.offset.set(0);
+
+ const filter = {};
+ $(':input', event.currentTarget).each(function() {
+ if (!this.name) {
+ return;
+ }
+
+ const value = $(this).val();
+
+ filter[this.name] = value;
+ });
+ const agents = instance.selectedAgents.get();
+ if (agents && agents.length > 0) {
+ filter.agent = agents[0]._id;
+ }
+ instance.filter.set(filter);
+ },
+ 'click .js-load-more'(event, instance) {
+ instance.offset.set(instance.offset.get() + QUEUE_COUNT);
+ },
});
Template.livechatQueue.onCreated(async function() {
- this.showOffline = new ReactiveVar({});
- this.agents = new ReactiveVar([]);
+ this.selectedAgents = new ReactiveVar([]);
this.departments = new ReactiveVar([]);
+ this.limit = new ReactiveVar(20);
+ this.filter = new ReactiveVar({});
+ this.queue = new ReactiveVar([]);
+ this.isLoading = new ReactiveVar(true);
+ this.offset = new ReactiveVar(0);
+ this.total = new ReactiveVar(0);
- this.subscribe('livechat:queue');
- const { users } = await APIClient.v1.get('livechat/users/agent');
- const { departments } = await APIClient.v1.get('livechat/department?sort={"name": 1}');
+ this.onSelectAgents = ({ item: agent }) => {
+ this.selectedAgents.set([agent]);
+ };
- this.agents.set(users);
+ this.onClickTagAgent = ({ username }) => {
+ this.selectedAgents.set(this.selectedAgents.get().filter((user) => user.username !== username));
+ };
+
+ this.autorun(async () => {
+ this.isLoading.set(true);
+ const filter = this.filter.get();
+ const offset = this.offset.get();
+ let query = `includeOfflineAgents=${ filter.agentStatus === 'offline' }`;
+ if (filter.agent) {
+ query += `&agentId=${ filter.agent }`;
+ }
+ if (filter.department) {
+ query += `&departmentId=${ filter.department }`;
+ }
+ const { queue, total } = await APIClient.v1.get(`livechat/queue?${ query }&count=${ QUEUE_COUNT }&offset=${ offset }`);
+ this.total.set(total);
+ this.queue.set(this.queue.get().concat(queue));
+ this.isLoading.set(false);
+ });
+
+ const { departments } = await APIClient.v1.get('livechat/department?sort={"name": 1}');
this.departments.set(departments);
});
diff --git a/app/livechat/imports/server/rest/queue.js b/app/livechat/imports/server/rest/queue.js
new file mode 100644
index 00000000000..d5f319f2e3c
--- /dev/null
+++ b/app/livechat/imports/server/rest/queue.js
@@ -0,0 +1,23 @@
+import { API } from '../../../../api';
+import { findQueueMetrics } from '../../../server/api/lib/queue';
+
+API.v1.addRoute('livechat/queue', { authRequired: true }, {
+ get() {
+ const { offset, count } = this.getPaginationItems();
+ const { sort } = this.parseJsonQuery();
+ const { agentId, includeOfflineAgents, departmentId } = this.requestParams();
+ const users = Promise.await(findQueueMetrics({
+ userId: this.userId,
+ agentId,
+ includeOfflineAgents: includeOfflineAgents === 'true',
+ departmentId,
+ pagination: {
+ offset,
+ count,
+ sort,
+ },
+ }));
+
+ return API.v1.success(users);
+ },
+});
diff --git a/app/livechat/server/api.js b/app/livechat/server/api.js
index daa85569a53..555fc643bc0 100644
--- a/app/livechat/server/api.js
+++ b/app/livechat/server/api.js
@@ -11,3 +11,4 @@ import '../imports/server/rest/triggers.js';
import '../imports/server/rest/integrations.js';
import '../imports/server/rest/messages.js';
import '../imports/server/rest/visitors.js';
+import '../imports/server/rest/queue.js';
diff --git a/app/livechat/server/api/lib/queue.js b/app/livechat/server/api/lib/queue.js
new file mode 100644
index 00000000000..3f209a0066a
--- /dev/null
+++ b/app/livechat/server/api/lib/queue.js
@@ -0,0 +1,25 @@
+import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission';
+import { LivechatRooms } from '../../../../models/server/raw';
+
+export async function findQueueMetrics({ userId, agentId, includeOfflineAgents, departmentId, pagination: { offset, count, sort } }) {
+ if (!await hasPermissionAsync(userId, 'view-l-room')) {
+ throw new Error('error-not-authorized');
+ }
+
+ const queue = await LivechatRooms.getQueueMetrics({ departmentId,
+ agentId,
+ includeOfflineAgents,
+ options: {
+ sort: sort || { chats: 1 },
+ offset,
+ count,
+ } });
+ const total = (await LivechatRooms.getQueueMetrics({ departmentId, agentId, includeOfflineAgents })).length;
+
+ return {
+ queue,
+ count: queue.length,
+ offset,
+ total,
+ };
+}
diff --git a/app/livechat/server/publications/livechatQueue.js b/app/livechat/server/publications/livechatQueue.js
index 92a4e7fae5e..acfb3c082ce 100644
--- a/app/livechat/server/publications/livechatQueue.js
+++ b/app/livechat/server/publications/livechatQueue.js
@@ -4,6 +4,7 @@ import { hasPermission } from '../../../authorization';
import { LivechatDepartmentAgents } from '../../../models';
Meteor.publish('livechat:queue', function() {
+ console.warn('The publication "livechat:queue" is deprecated and will be removed after version v3.0.0');
if (!this.userId) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:queue' }));
}
@@ -11,24 +12,6 @@ Meteor.publish('livechat:queue', function() {
if (!hasPermission(this.userId, 'view-l-room')) {
return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:queue' }));
}
-
- // let sort = { count: 1, sort: 1, username: 1 };
- // let onlineUsers = {};
-
- // let handleUsers = RocketChat.models.Users.findOnlineAgents().observeChanges({
- // added(id, fields) {
- // onlineUsers[fields.username] = 1;
- // // this.added('livechatQueueUser', id, fields);
- // },
- // changed(id, fields) {
- // onlineUsers[fields.username] = 1;
- // // this.changed('livechatQueueUser', id, fields);
- // },
- // removed(id) {
- // this.removed('livechatQueueUser', id);
- // }
- // });
-
const self = this;
const handleDepts = LivechatDepartmentAgents.findUsersInQueue().observeChanges({
@@ -46,7 +29,6 @@ Meteor.publish('livechat:queue', function() {
this.ready();
this.onStop(() => {
- // handleUsers.stop();
handleDepts.stop();
});
});
diff --git a/app/models/server/raw/LivechatDepartmentAgents.js b/app/models/server/raw/LivechatDepartmentAgents.js
index 4e30c4eac22..853126ab01a 100644
--- a/app/models/server/raw/LivechatDepartmentAgents.js
+++ b/app/models/server/raw/LivechatDepartmentAgents.js
@@ -1,5 +1,14 @@
import { BaseRaw } from './BaseRaw';
export class LivechatDepartmentAgentsRaw extends BaseRaw {
+ findUsersInQueue(usersList, options) {
+ const query = {};
+ if (Array.isArray(usersList) && usersList.length) {
+ query.username = {
+ $in: usersList,
+ };
+ }
+ return this.find(query, options);
+ }
}
diff --git a/app/models/server/raw/LivechatRooms.js b/app/models/server/raw/LivechatRooms.js
index 9b5f657d2aa..896ff0ed241 100644
--- a/app/models/server/raw/LivechatRooms.js
+++ b/app/models/server/raw/LivechatRooms.js
@@ -1,5 +1,97 @@
import { BaseRaw } from './BaseRaw';
export class LivechatRoomsRaw extends BaseRaw {
-
+ getQueueMetrics({ departmentId, agentId, includeOfflineAgents, options = {} }) {
+ const match = { $match: { t: 'l', open: true, servedBy: { $exists: true } } };
+ const matchUsers = { $match: {} };
+ if (departmentId) {
+ match.$match.departmentId = departmentId;
+ }
+ if (agentId) {
+ matchUsers.$match['user._id'] = agentId;
+ }
+ if (!includeOfflineAgents) {
+ matchUsers.$match['user.status'] = { $ne: 'offline' };
+ matchUsers.$match['user.statusLivechat'] = { $eq: 'available' };
+ }
+ const departmentsLookup = {
+ $lookup: {
+ from: 'rocketchat_livechat_department',
+ localField: 'departmentId',
+ foreignField: '_id',
+ as: 'departments',
+ },
+ };
+ const departmentsUnwind = {
+ $unwind: {
+ path: '$departments',
+ preserveNullAndEmptyArrays: true,
+ },
+ };
+ const departmentsGroup = {
+ $group: {
+ _id: {
+ departmentId: '$departmentId',
+ name: '$departments.name',
+ room: '$$ROOT',
+ },
+ },
+ };
+ const usersLookup = {
+ $lookup: {
+ from: 'users',
+ localField: '_id.room.servedBy._id',
+ foreignField: '_id',
+ as: 'user',
+ },
+ };
+ const usersUnwind = {
+ $unwind: {
+ path: '$user',
+ preserveNullAndEmptyArrays: true,
+ },
+ };
+ const usersGroup = {
+ $group: {
+ _id: {
+ userId: '$user._id',
+ username: '$user.username',
+ status: '$user.status',
+ departmentId: '$_id.departmentId',
+ departmentName: '$_id.name',
+ },
+ chats: { $sum: 1 },
+ },
+ };
+ const project = {
+ $project: {
+ _id: 0,
+ user: {
+ _id: '$_id.userId',
+ username: '$_id.username',
+ status: '$_id.status',
+ },
+ department: {
+ _id: { $ifNull: ['$_id.departmentId', null] },
+ name: { $ifNull: ['$_id.departmentName', null] },
+ },
+ chats: 1,
+ },
+ };
+ const firstParams = [match, departmentsLookup, departmentsUnwind, departmentsGroup, usersLookup, usersUnwind];
+ if (Object.keys(matchUsers.$match)) {
+ firstParams.push(matchUsers);
+ }
+ const params = [...firstParams, usersGroup, 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();
+ }
}
diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json
index aeda8651912..26306d47057 100644
--- a/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -1668,6 +1668,7 @@
"Inclusive": "Inclusive",
"Incoming_Livechats": "Incoming Livechats",
"Incoming_WebHook": "Incoming WebHook",
+ "Include_Offline_Agents": "Include offline agents",
"Industry": "Industry",
"Info": "Info",
"initials_avatar": "Initials Avatar",
diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
index 747497bac60..23501fafeb9 100644
--- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
+++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
@@ -1565,6 +1565,7 @@
"Inclusive": "Inclusive",
"Incoming_Livechats": "Livechats entrantes",
"Incoming_WebHook": "WebHook entrante",
+ "Include_Offline_Agents": "Incluir agentes offline",
"Industry": "Indústria",
"Info": "Informações",
"initials_avatar": "Iniciais Avatar",
diff --git a/tests/end-to-end/api/livechat/queue.js b/tests/end-to-end/api/livechat/queue.js
new file mode 100644
index 00000000000..0dd996aee05
--- /dev/null
+++ b/tests/end-to-end/api/livechat/queue.js
@@ -0,0 +1,46 @@
+import { getCredentials, api, request, credentials } from '../../../data/api-data.js';
+import { updatePermission, updateSetting } from '../../../data/permissions.helper';
+
+describe('LIVECHAT - Queue', function() {
+ this.retries(0);
+
+ before((done) => getCredentials(done));
+
+ before((done) => {
+ updateSetting('Livechat_enabled', true).then(done);
+ });
+
+ describe('livechat/queue', () => {
+ it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => {
+ updatePermission('view-l-room', [])
+ .then(() => {
+ request.get(api('livechat/queue'))
+ .set(credentials)
+ .expect('Content-Type', 'application/json')
+ .expect(400)
+ .expect((res) => {
+ expect(res.body).to.have.property('success', false);
+ expect(res.body.error).to.be.equal('error-not-authorized');
+ })
+ .end(done);
+ });
+ });
+ it('should return an array of queued metrics', (done) => {
+ updatePermission('view-l-room', ['admin'])
+ .then(() => {
+ request.get(api('livechat/queue'))
+ .set(credentials)
+ .expect('Content-Type', 'application/json')
+ .expect(200)
+ .expect((res) => {
+ expect(res.body).to.have.property('success', true);
+ expect(res.body.queue).to.be.an('array');
+ expect(res.body).to.have.property('offset');
+ expect(res.body).to.have.property('total');
+ expect(res.body).to.have.property('count');
+ })
+ .end(done);
+ });
+ });
+ });
+});