diff --git a/.changeset/seven-jobs-tickle.md b/.changeset/seven-jobs-tickle.md new file mode 100644 index 00000000000..870bafbb7d9 --- /dev/null +++ b/.changeset/seven-jobs-tickle.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +fix: agent role being removed upon user deactivation diff --git a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts index 2c5ffe38169..55de5bbf631 100644 --- a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -1,13 +1,10 @@ -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import type { ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { 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'; import type { UpdateFilter } from 'mongodb'; import type { IWorkHoursCronJobsWrapper } from '../../../../server/models/raw/LivechatBusinessHours'; -import { businessHourLogger } from '../lib/logger'; -import { filterBusinessHoursThatMustBeOpened } from './Helper'; export interface IBusinessHourBehavior { findHoursToCreateJobs(): Promise; @@ -61,29 +58,6 @@ export abstract class AbstractBusinessHourBehavior { { livechatStatusSystemModified: true }, ); } - - async onNewAgentCreated(agentId: string): Promise { - businessHourLogger.debug(`Executing onNewAgentCreated for agentId: ${agentId}`); - - const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour(); - if (!defaultBusinessHour) { - businessHourLogger.debug(`No default business hour found for agentId: ${agentId}`); - return; - } - - const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]); - if (!businessHourToOpen.length) { - businessHourLogger.debug( - `No business hour to open found for agentId: ${agentId}. Default business hour is closed. Setting agentId: ${agentId} to status: ${ILivechatAgentStatus.NOT_AVAILABLE}`, - ); - await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); - return; - } - - await Users.addBusinessHourByAgentIds([agentId], defaultBusinessHour._id); - - businessHourLogger.debug(`Setting agentId: ${agentId} to status: ${ILivechatAgentStatus.AVAILABLE}`); - } } export abstract class AbstractBusinessHourType { diff --git a/apps/meteor/app/livechat/server/business-hour/Single.ts b/apps/meteor/app/livechat/server/business-hour/Single.ts index 63135fa5322..d899f271737 100644 --- a/apps/meteor/app/livechat/server/business-hour/Single.ts +++ b/apps/meteor/app/livechat/server/business-hour/Single.ts @@ -1,9 +1,10 @@ -import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior } from './AbstractBusinessHour'; import { AbstractBusinessHourBehavior } from './AbstractBusinessHour'; -import { openBusinessHourDefault } from './Helper'; +import { filterBusinessHoursThatMustBeOpened, openBusinessHourDefault } from './Helper'; export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior { async openBusinessHoursByDayAndHour(): Promise { @@ -26,6 +27,35 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp return openBusinessHourDefault(); } + async onNewAgentCreated(agentId: string): Promise { + const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour(); + if (!defaultBusinessHour) { + businessHourLogger.debug('No default business hour found for agentId', { + agentId, + }); + return; + } + + const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]); + if (!businessHourToOpen.length) { + businessHourLogger.debug({ + msg: 'No business hours found. Moving agent to NOT_AVAILABLE status', + agentId, + newStatus: ILivechatAgentStatus.NOT_AVAILABLE, + }); + await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); + return; + } + + await Users.addBusinessHourByAgentIds([agentId], defaultBusinessHour._id); + + businessHourLogger.debug({ + msg: 'Business hours found. Moving agent to AVAILABLE status', + agentId, + newStatus: ILivechatAgentStatus.AVAILABLE, + }); + } + afterSaveBusinessHours(): Promise { return openBusinessHourDefault(); } diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts index 50fe5846637..30900481c4e 100644 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts @@ -1,4 +1,5 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import { type IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { Livechat } from '../lib/Livechat'; @@ -33,8 +34,11 @@ const handleAgentCreated = async (user: IUser) => { const handleDeactivateUser = async (user: IUser) => { if (wasAgent(user)) { - callbackLogger.debug('Removing agent', user._id); - await Livechat.removeAgent(user.username); + callbackLogger.debug({ + msg: 'Removing agent extension & making agent unavailable', + userId: user._id, + }); + await Users.makeAgentUnavailableAndUnsetExtension(user._id); } }; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts index 8b8c19de713..22379e27698 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts @@ -1,12 +1,16 @@ -import type { ILivechatDepartment, ILivechatBusinessHour } 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'; import { businessHourManager } from '../../../../../app/livechat/server/business-hour'; import type { IBusinessHourBehavior } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour'; import { AbstractBusinessHourBehavior } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour'; -import { filterBusinessHoursThatMustBeOpened } from '../../../../../app/livechat/server/business-hour/Helper'; +import { + filterBusinessHoursThatMustBeOpened, + filterBusinessHoursThatMustBeOpenedByDay, +} from '../../../../../app/livechat/server/business-hour/Helper'; import { settings } from '../../../../../app/settings/server'; +import { isTruthy } from '../../../../../lib/isTruthy'; import { bhLogger } from '../lib/logger'; import { closeBusinessHour, openBusinessHour, removeBusinessHourByAgentIds } from './Helper'; @@ -248,6 +252,108 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior return this.UsersRepository.isAgentWithinBusinessHours(agentId); } + async onNewAgentCreated(agentId: string): Promise { + bhLogger.debug({ + msg: 'Executing onNewAgentCreated for agent', + agentId, + }); + + await this.applyAnyOpenBusinessHourToAgent(agentId); + + await Users.updateLivechatStatusBasedOnBusinessHours([agentId]); + } + + private async applyAnyOpenBusinessHourToAgent(agentId: string): Promise { + const currentTime = moment().utc(); + const day = currentTime.format('dddd'); + const allActiveBusinessHoursForEntireWeek = await this.BusinessHourRepository.findActiveBusinessHours({ + projection: { + workHours: 1, + timezone: 1, + type: 1, + active: 1, + }, + }); + const openedBusinessHours = await filterBusinessHoursThatMustBeOpenedByDay(allActiveBusinessHoursForEntireWeek, day); + if (!openedBusinessHours.length) { + bhLogger.debug({ + msg: 'Business hour status check failed for agent. No opened business hour found for the current day', + agentId, + }); + return; + } + + const agentDepartments = await LivechatDepartmentAgents.find( + { departmentEnabled: true, agentId }, + { projection: { agentId: 1, departmentId: 1 } }, + ).toArray(); + + if (!agentDepartments.length) { + // check if default businessHour is active + const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT); + if (isDefaultBHActive?._id) { + await Users.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId); + + bhLogger.debug({ + msg: 'Business hour status check passed for agent. Found default business hour to be active', + agentId, + }); + return; + } + + bhLogger.debug({ + msg: 'Business hour status check failed for agent. Found default business hour to be inactive', + agentId, + }); + return; + } + + // check if any one these departments have a opened business hour linked to it + const departments = (await LivechatDepartment.findInIds( + agentDepartments.map(({ departmentId }) => departmentId), + { projection: { _id: 1, businessHourId: 1 } }, + ).toArray()) as Pick[]; + + const departmentsWithActiveBH = departments.filter( + ({ businessHourId }) => businessHourId && openedBusinessHours.findIndex(({ _id }) => _id === businessHourId) !== -1, + ); + + if (!departmentsWithActiveBH.length) { + // No opened business hour found for any of the departments connected to the agent + // check if this agent has any departments that is connected to any non-default business hour + // if no such departments found then check default BH and if it is active, then allow the agent to change service status + const hasAtLeastOneDepartmentWithNonDefaultBH = departments.some(({ businessHourId }) => { + // check if business hour is active + return businessHourId && allActiveBusinessHoursForEntireWeek.findIndex(({ _id }) => _id === businessHourId) !== -1; + }); + if (!hasAtLeastOneDepartmentWithNonDefaultBH) { + const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT); + if (isDefaultBHActive?._id) { + await Users.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId); + + bhLogger.debug({ + msg: 'Business hour status check passed for agentId. Found default business hour to be active and agent has no departments with non-default business hours', + agentId, + }); + return; + } + } + bhLogger.debug({ + msg: 'Business hour status check failed for agent. No opened business hour found for any of the departments connected to the agent', + agentId, + }); + return; + } + + const activeBusinessHoursForAgent = departmentsWithActiveBH.map(({ businessHourId }) => businessHourId).filter(isTruthy); + await Users.openAgentBusinessHoursByBusinessHourIdsAndAgentId(activeBusinessHoursForAgent, agentId); + + bhLogger.debug({ + msg: `Business hour status check passed for agent. Found opened business hour for departments connected to the agent`, + activeBusinessHoursForAgent, + }); + } + private async handleRemoveAgentsFromDepartments(department: Record, agentsIds: string[], options: any): Promise { const agentIdsWithoutDepartment: string[] = []; const agentIdsToRemoveCurrentBusinessHour: string[] = []; diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index dc344109289..0663bbdcda2 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -1,3 +1,4 @@ +import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; import { Subscriptions } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -6,6 +7,8 @@ import { BaseRaw } from './BaseRaw'; const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdle) => ({ statusLivechat: 'available', roles: 'livechat-agent', + // ignore deactivated users + active: true, ...(!isLivechatEnabledWhenAgentIdle && { $or: [ { @@ -933,7 +936,7 @@ export class UsersRaw extends BaseRaw { }, }; - return this.updateMany(query, update); + return this.updateOne(query, update); } addBusinessHourByAgentIds(agentIds = [], businessHourId) { @@ -1031,6 +1034,8 @@ export class UsersRaw extends BaseRaw { const query = { $or: [{ openBusinessHours: { $exists: false } }, { openBusinessHours: { $size: 0 } }], roles: 'livechat-agent', + // exclude deactivated users + active: true, // Avoid unnecessary updates statusLivechat: 'available', ...(Array.isArray(userIds) && userIds.length > 0 && { _id: { $in: userIds } }), @@ -1687,6 +1692,24 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } + makeAgentUnavailableAndUnsetExtension(userId) { + const query = { + _id: userId, + roles: 'livechat-agent', + }; + + const update = { + $set: { + statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, + }, + $unset: { + extension: 1, + }, + }; + + return this.updateOne(query, update); + } + setLivechatData(userId, data = {}) { // TODO: Create class Agent const query = { diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index f8eda6a3639..c0ce51f79f4 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -258,6 +258,7 @@ export interface IUsersModel extends IBaseModel { getNextAgent(ignoreAgentId?: string, extraQuery?: Filter): Promise<{ agentId: string; username: string } | null>; getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username: string } | null>; setLivechatStatus(userId: string, status: ILivechatAgentStatus): Promise; + makeAgentUnavailableAndUnsetExtension(userId: string): Promise; setLivechatData(userId: string, data?: Record): Promise; closeOffice(): Promise; openOffice(): Promise;