Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 8 additions & 14 deletions apps/meteor/app/livechat/imports/server/rest/departments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,23 +117,17 @@ API.v1.addRoute(
const { _id } = this.urlParams;
const { department, agents } = this.bodyParams;

let success;
if (permissionToSave) {
success = await LivechatEnterprise.saveDepartment(_id, department);
if (!permissionToSave) {
throw new Error('error-not-allowed');
}

if (success && agents && permissionToAddAgents) {
success = await LivechatTs.saveDepartmentAgents(_id, { upsert: agents });
}
const agentParam = permissionToAddAgents && agents ? { upsert: agents } : {};
await LivechatEnterprise.saveDepartment(_id, department, agentParam);

if (success) {
return API.v1.success({
department: await LivechatDepartment.findOneById(_id),
agents: await LivechatDepartmentAgents.findByDepartmentId(_id).toArray(),
});
}

return API.v1.failure();
return API.v1.success({
department: await LivechatDepartment.findOneById(_id),
agents: await LivechatDepartmentAgents.findByDepartmentId(_id).toArray(),
});
},
async delete() {
check(this.urlParams, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ API.v1.addRoute(
const { department } = this.queryParams;
const ourQuery: { status: string; department?: string } = { status: 'queued' };
if (department) {
const departmentFromDB = await LivechatDepartment.findOneByIdOrName(department);
const departmentFromDB = await LivechatDepartment.findOneByIdOrName(department, { projection: { _id: 1 } });
if (departmentFromDB) {
ourQuery.department = departmentFromDB._id;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ILivechatAgentStatus, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings';
import type { AtLeast, ILivechatAgentStatus, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings';
import type { ILivechatBusinessHoursModel, IUsersModel } from '@rocket.chat/model-typings';
import { LivechatBusinessHours, Users } from '@rocket.chat/models';
import moment from 'moment-timezone';
Expand All @@ -14,8 +14,8 @@ export interface IBusinessHourBehavior {
onAddAgentToDepartment(options?: { departmentId: string; agentsId: string[] }): Promise<any>;
onRemoveAgentFromDepartment(options?: Record<string, any>): Promise<any>;
onRemoveDepartment(options: { department: ILivechatDepartment; agentsIds: string[] }): Promise<any>;
onDepartmentDisabled(department?: ILivechatDepartment): Promise<any>;
onDepartmentArchived(department: Pick<ILivechatDepartment, '_id'>): Promise<void>;
onDepartmentDisabled(department?: AtLeast<ILivechatDepartment, '_id' | 'businessHourId'>): Promise<void>;
onDepartmentArchived(department: Pick<ILivechatDepartment, '_id' | 'businessHourId'>): Promise<void>;
onStartBusinessHours(): Promise<void>;
afterSaveBusinessHours(businessHourData: ILivechatBusinessHour): Promise<void>;
allowAgentChangeServiceStatus(agentId: string): Promise<boolean>;
Expand Down
6 changes: 4 additions & 2 deletions apps/meteor/app/livechat/server/lib/Departments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
import type { ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms } from '@rocket.chat/models';

Expand All @@ -10,7 +10,9 @@ class DepartmentHelperClass {
async removeDepartment(departmentId: string) {
this.logger.debug(`Removing department: ${departmentId}`);

const department = await LivechatDepartment.findOneById(departmentId);
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'businessHourId'>>(departmentId, {
projection: { _id: 1, businessHourId: 1 },
});
if (!department) {
throw new Error('error-department-not-found');
}
Expand Down
25 changes: 18 additions & 7 deletions apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,13 +648,24 @@ export const updateDepartmentAgents = async (
departmentEnabled: boolean,
) => {
check(departmentId, String);
check(
agents,
Match.ObjectIncluding({
upsert: Match.Maybe(Array),
remove: Match.Maybe(Array),
}),
);
check(agents, {
upsert: Match.Maybe([
Match.ObjectIncluding({
agentId: String,
username: Match.Maybe(String),
count: Match.Maybe(Match.Integer),
order: Match.Maybe(Match.Integer),
}),
]),
remove: Match.Maybe([
Match.ObjectIncluding({
agentId: String,
username: Match.Maybe(String),
count: Match.Maybe(Match.Integer),
order: Match.Maybe(Match.Integer),
}),
]),
});

const { upsert = [], remove = [] } = agents;
const agentsRemoved = [];
Expand Down
8 changes: 5 additions & 3 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ class LivechatClass {
},
};

const dep = await LivechatDepartment.findOneById(department);
const dep = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id'>>(department, { projection: { _id: 1 } });
if (!dep) {
throw new Meteor.Error('invalid-department', 'Provided department does not exists');
}
Expand Down Expand Up @@ -987,7 +987,9 @@ class LivechatClass {
}

async archiveDepartment(_id: string) {
const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } });
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'businessHourId'>>(_id, {
projection: { _id: 1, businessHourId: 1 },
});

if (!department) {
throw new Error('department-not-found');
Expand Down Expand Up @@ -1053,7 +1055,7 @@ class LivechatClass {
}

if (transferData.departmentId) {
const department = await LivechatDepartment.findOneById(transferData.departmentId, {
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'name' | '_id'>>(transferData.departmentId, {
projection: { name: 1 },
});
if (!department) {
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Meteor.startup(async () => {
await createDefaultBusinessHourIfNotExists();

settings.watch<boolean>('Livechat_enable_business_hours', async (value) => {
logger.info(`Changing business hour type to ${value}`);
logger.debug(`Starting business hour manager ${value}`);
if (value) {
await businessHourManager.startManager();
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ export const openBusinessHour = async (
totalAgents: agentIds.length,
top10AgentIds: agentIds.slice(0, 10),
});

await Users.addBusinessHourByAgentIds(agentIds, businessHour._id);
await Users.makeAgentsWithinBusinessHourAvailable(agentIds);

if (updateLivechatStatus) {
await Users.updateLivechatStatusBasedOnBusinessHours();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AtLeast } from '@rocket.chat/core-typings';
import { type ILivechatDepartment, type ILivechatBusinessHour, LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models';
import moment from 'moment';
Expand Down Expand Up @@ -80,8 +81,8 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior

async afterSaveBusinessHours(businessHourData: IBusinessHoursExtraProperties): Promise<void> {
const departments = businessHourData.departmentsToApplyBusinessHour?.split(',').filter(Boolean);
const currentDepartments = businessHourData.departments?.map((dept: any) => dept._id);
const toRemove = [...(currentDepartments || []).filter((dept: Record<string, any>) => !departments.includes(dept._id))];
const currentDepartments = businessHourData.departments?.map((dept) => dept._id);
const toRemove = [...(currentDepartments || []).filter((dept) => !departments.includes(dept))];
await this.removeBusinessHourFromRemovedDepartmentsUsersIfNeeded(businessHourData._id, toRemove);
const businessHour = await this.BusinessHourRepository.findOneById(businessHourData._id);
if (!businessHour) {
Expand Down Expand Up @@ -135,6 +136,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
if (!businessHourToOpen.length) {
return options;
}

await this.UsersRepository.addBusinessHourByAgentIds(agentsId, businessHour._id);
await this.UsersRepository.makeAgentsWithinBusinessHourAvailable(agentsId);

Expand All @@ -152,15 +154,15 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
return this.handleRemoveAgentsFromDepartments(department, agentsId, options);
}

async onRemoveDepartment(options: { department: ILivechatDepartment; agentsIds: string[] }): Promise<any> {
async onRemoveDepartment(options: { department: AtLeast<ILivechatDepartment, '_id' | 'businessHourId'>; agentsIds: string[] }) {
const { department, agentsIds } = options;
if (!department || !agentsIds?.length) {
return options;
}
return this.onDepartmentDisabled(department);
}

async onDepartmentDisabled(department: ILivechatDepartment): Promise<void> {
async onDepartmentDisabled(department: AtLeast<ILivechatDepartment, 'businessHourId' | '_id'>): Promise<void> {
if (!department.businessHourId) {
return;
}
Expand Down Expand Up @@ -209,22 +211,13 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
for await (const bh of businessHourToOpen) {
await openBusinessHour(bh, false);
}

await Users.updateLivechatStatusBasedOnBusinessHours();

await businessHourManager.restartCronJobsIfNecessary();
}

async onDepartmentArchived(department: Pick<ILivechatDepartment, '_id'>): Promise<void> {
async onDepartmentArchived(department: Pick<ILivechatDepartment, '_id' | 'businessHourId'>): Promise<void> {
bhLogger.debug('Processing department archived event on multiple business hours', department);
const dbDepartment = await LivechatDepartment.findOneById(department._id, { projection: { businessHourId: 1, _id: 1 } });

if (!dbDepartment) {
bhLogger.error(`No department found with id: ${department._id} when archiving it`);
return;
}

return this.onDepartmentDisabled(dbDepartment);
return this.onDepartmentDisabled(department);
}

allowAgentChangeServiceStatus(agentId: string): Promise<boolean> {
Expand Down Expand Up @@ -321,12 +314,17 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
private async handleRemoveAgentsFromDepartments(department: Record<string, any>, agentsIds: string[], options: any): Promise<any> {
const agentIdsWithoutDepartment: string[] = [];
const agentIdsToRemoveCurrentBusinessHour: string[] = [];
for await (const agentId of agentsIds) {
if ((await LivechatDepartmentAgents.findByAgentId(agentId).count()) === 0) {

const [agentsWithDepartment, [agentsOfDepartment] = []] = await Promise.all([
LivechatDepartmentAgents.findByAgentIds(agentsIds, { projection: { agentId: 1 } }).toArray(),
LivechatDepartment.findAgentsByBusinessHourId(department.businessHourId).toArray(),
]);

for (const agentId of agentsIds) {
if (!agentsWithDepartment.find((agent) => agent.agentId === agentId)) {
agentIdsWithoutDepartment.push(agentId);
}
// TODO: We're doing a full fledged aggregation with lookups and getting the whole array just for getting the length? :(
if (!(await LivechatDepartmentAgents.findAgentsByAgentIdAndBusinessHourId(agentId, department.businessHourId)).length) {
if (!agentsOfDepartment?.agentIds?.find((agent) => agent === agentId)) {
agentIdsToRemoveCurrentBusinessHour.push(agentId);
}
}
Expand Down Expand Up @@ -359,7 +357,9 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior
if (!departmentsToRemove.length) {
return;
}
const agentIds = (await LivechatDepartmentAgents.findByDepartmentIds(departmentsToRemove).toArray()).map((dept: any) => dept.agentId);
const agentIds = (
await LivechatDepartmentAgents.findByDepartmentIds(departmentsToRemove, { projection: { agentId: 1 } }).toArray()
).map((dept) => dept.agentId);
await removeBusinessHourByAgentIds(agentIds, businessHourId);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatRooms, LivechatDepartment } from '@rocket.chat/models';

import { callbacks } from '../../../../../lib/callbacks';
Expand All @@ -9,7 +10,7 @@ callbacks.add(
return room;
}

const department = await LivechatDepartment.findOneById(room.departmentId, {
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'ancestors'>>(room.departmentId, {
projection: { ancestors: 1 },
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import type { ILivechatDepartment, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms, LivechatDepartment } from '@rocket.chat/models';

import { callbacks } from '../../../../../lib/callbacks';
Expand All @@ -17,7 +17,7 @@ callbacks.add(
}
await LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id);

const department = await LivechatDepartment.findOneById(newDepartmentId, {
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'ancestors'>>(newDepartmentId, {
projection: { ancestors: 1 },
});
if (!department) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { ILivechatAgent, ILivechatDepartmentRecord } from '@rocket.chat/core-typings';
import type { AtLeast, ILivechatAgent, ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment } from '@rocket.chat/models';

import { callbacks } from '../../../../../lib/callbacks';
import { cbLogger } from '../lib/logger';

const afterRemoveDepartment = async (options: { department: ILivechatDepartmentRecord; agentsId: ILivechatAgent['_id'][] }) => {
const afterRemoveDepartment = async (options: {
department: AtLeast<ILivechatDepartment, '_id' | 'businessHourId'>;
agentsId: ILivechatAgent['_id'][];
}) => {
if (!options?.department) {
cbLogger.warn('No department found in options', options);
return options;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment } from '@rocket.chat/models';

import { callbacks } from '../../../../../lib/callbacks';
Expand All @@ -9,7 +10,7 @@ callbacks.add(
if (!departmentId) {
return options;
}
const department = await LivechatDepartment.findOneById(departmentId, {
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'departmentsAllowedToForward'>>(departmentId, {
projection: { departmentsAllowedToForward: 1 },
});
if (!department) {
Expand Down
45 changes: 44 additions & 1 deletion apps/meteor/ee/server/models/raw/LivechatDepartment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import type { ILivechatDepartment, RocketChatRecordDeleted, LivechatDepartmentDTO } from '@rocket.chat/core-typings';
import type { ILivechatDepartmentModel } from '@rocket.chat/model-typings';
import { LivechatUnit } from '@rocket.chat/models';
import type { Collection, DeleteResult, Document, Filter, FindCursor, FindOptions, UpdateFilter, UpdateResult, Db } from 'mongodb';
import type {
Collection,
DeleteResult,
Document,
Filter,
FindCursor,
FindOptions,
UpdateFilter,
UpdateResult,
Db,
AggregationCursor,
} from 'mongodb';

import { LivechatDepartmentRaw } from '../../../../server/models/raw/LivechatDepartment';

Expand All @@ -22,6 +33,7 @@ declare module '@rocket.chat/model-typings' {
projection: FindOptions<ILivechatDepartment>['projection'],
): Promise<FindCursor<ILivechatDepartment>>;
findByParentId(parentId: string, options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>;
findAgentsByBusinessHourId(businessHourId: string): AggregationCursor<{ agentIds: string[] }>;
}
}

Expand Down Expand Up @@ -80,4 +92,35 @@ export class LivechatDepartmentEE extends LivechatDepartmentRaw implements ILive
findByParentId(parentId: string, options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment> {
return this.col.find({ parentId }, options);
}

findAgentsByBusinessHourId(businessHourId: string): AggregationCursor<{ agentIds: string[] }> {
return this.col.aggregate<{ agentIds: string[] }>([
[
{
$match: {
businessHourId,
},
},
{
$lookup: {
from: 'rocketchat_livechat_department_agents',
localField: '_id',
foreignField: 'departmentId',
as: 'agents',
},
},
{
$unwind: '$agents',
},
{
$group: {
_id: null,
agentIds: {
$addToSet: '$agents.agentId',
},
},
},
],
]);
}
}
Loading