Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
7 changes: 2 additions & 5 deletions apps/meteor/app/livechat/imports/server/rest/departments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,8 @@ API.v1.addRoute(

let success;
if (permissionToSave) {
success = await LivechatEnterprise.saveDepartment(_id, department);
}

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

if (success) {
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<any>;
Comment thread
KevLehman marked this conversation as resolved.
Resolved
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?.agents?.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<{ agents: string[] }>;
Comment thread
KevLehman marked this conversation as resolved.
Resolved
}
}

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<{ agents: string[] }> {
return this.col.aggregate<{ agents: string[] }>([
[
{
$match: {
businessHourId,
},
},
{
$lookup: {
from: 'rocketchat_livechat_department_agents',
localField: '_id',
foreignField: 'departmentId',
as: 'agents',
},
},
{
$unwind: '$agents',
},
{
$group: {
_id: null,
agents: {
$addToSet: '$agents.agentId',
},
},
},
],
]);
}
}
10 changes: 7 additions & 3 deletions apps/meteor/lib/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
TransferData,
AtLeast,
UserStatus,
ILivechatDepartment,
} from '@rocket.chat/core-typings';
import type { FilterOperators } from 'mongodb';

Expand Down Expand Up @@ -84,7 +85,7 @@ interface EventLikeCallbackSignatures {
'afterValidateLogin': (login: { user: IUser }) => void;
'afterJoinRoom': (user: IUser, room: IRoom) => void;
'livechat.afterDepartmentDisabled': (department: ILivechatDepartmentRecord) => void;
'livechat.afterDepartmentArchived': (department: Pick<ILivechatDepartmentRecord, '_id'>) => void;
'livechat.afterDepartmentArchived': (department: Pick<ILivechatDepartmentRecord, '_id' | 'businessHourId'>) => void;
'beforeSaveUser': ({ user, oldUser }: { user: IUser; oldUser?: IUser }) => void;
'afterSaveUser': ({ user, oldUser }: { user: IUser; oldUser?: IUser | null }) => void;
'livechat.afterTagRemoved': (tag: ILivechatTagRecord) => void;
Expand Down Expand Up @@ -149,8 +150,11 @@ type ChainedCallbackSignatures = {
oldDepartmentId: ILivechatDepartmentRecord['_id'];
};
'livechat.afterInquiryQueued': (inquiry: ILivechatInquiryRecord) => ILivechatInquiryRecord;
'livechat.afterRemoveDepartment': (params: { department: ILivechatDepartmentRecord; agentsId: ILivechatAgent['_id'][] }) => {
departmentId: ILivechatDepartmentRecord['_id'];
'livechat.afterRemoveDepartment': (params: {
department: AtLeast<ILivechatDepartment, '_id' | 'businessHourId'>;
agentsId: ILivechatAgent['_id'][];
}) => {
department: AtLeast<ILivechatDepartment, '_id' | 'businessHourId'>;
agentsId: ILivechatAgent['_id'][];
};
'livechat.applySimultaneousChatRestrictions': (_: undefined, params: { departmentId?: ILivechatDepartmentRecord['_id'] }) => undefined;
Expand Down
Loading