snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 1s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 1s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
This commit is contained in:
180
apps/meteor/ee/server/lib/omnichannel/business-hour/Custom.ts
Normal file
180
apps/meteor/ee/server/lib/omnichannel/business-hour/Custom.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
|
||||
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
|
||||
import { LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models';
|
||||
|
||||
import { businessHourManager } from '../../../../../server/lib/omnichannel/business-hour';
|
||||
import type { IBusinessHourType } from '../../../../../server/lib/omnichannel/business-hour/AbstractBusinessHour';
|
||||
import { AbstractBusinessHourType } from '../../../../../server/lib/omnichannel/business-hour/AbstractBusinessHour';
|
||||
import {
|
||||
filterBusinessHoursThatMustBeOpened,
|
||||
makeAgentsUnavailableBasedOnBusinessHour,
|
||||
} from '../../../../../server/lib/omnichannel/business-hour/Helper';
|
||||
import { bhLogger } from '../logger';
|
||||
|
||||
type IBusinessHoursExtraProperties = {
|
||||
timezoneName: string;
|
||||
departmentsToApplyBusinessHour?: string;
|
||||
};
|
||||
|
||||
class CustomBusinessHour extends AbstractBusinessHourType implements IBusinessHourType {
|
||||
name = LivechatBusinessHourTypes.CUSTOM;
|
||||
|
||||
async getBusinessHour(id: string): Promise<ILivechatBusinessHour | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const businessHour = await this.BusinessHourRepository.findOneById(id);
|
||||
if (!businessHour) {
|
||||
return null;
|
||||
}
|
||||
|
||||
businessHour.departments = await LivechatDepartment.findByBusinessHourId(businessHour._id, {
|
||||
projection: { name: 1 },
|
||||
}).toArray();
|
||||
return businessHour;
|
||||
}
|
||||
|
||||
async saveBusinessHour(businessHour: ILivechatBusinessHour & IBusinessHoursExtraProperties): Promise<ILivechatBusinessHour> {
|
||||
const existingBusinessHour = (await this.BusinessHourRepository.findOne(
|
||||
{ name: businessHour.name },
|
||||
{ projection: { _id: 1 } },
|
||||
)) as ILivechatBusinessHour;
|
||||
if (existingBusinessHour && existingBusinessHour._id !== businessHour._id) {
|
||||
throw new Error('error-business-hour-name-already-in-use');
|
||||
}
|
||||
const { timezoneName, departmentsToApplyBusinessHour, ...businessHourData } = businessHour;
|
||||
businessHourData.timezone = {
|
||||
name: timezoneName,
|
||||
utc: this.getUTCFromTimezone(timezoneName),
|
||||
};
|
||||
const businessHourToReturn = { ...businessHourData, departmentsToApplyBusinessHour };
|
||||
delete businessHourData.departments;
|
||||
|
||||
const businessHourId = await this.baseSaveBusinessHour(businessHourData);
|
||||
|
||||
// Internal callers (e.g. the DST verifier) re-save business hours without the
|
||||
// departments field; only reconcile department links when it is provided,
|
||||
// otherwise an internal re-save would unlink every department.
|
||||
if (departmentsToApplyBusinessHour !== undefined) {
|
||||
const departments = departmentsToApplyBusinessHour.split(',').filter(Boolean);
|
||||
const currentDepartments = (
|
||||
await LivechatDepartment.findByBusinessHourId(businessHourId, {
|
||||
projection: { _id: 1 },
|
||||
}).toArray()
|
||||
).map((dept) => dept._id);
|
||||
const toRemove = [...currentDepartments.filter((dept) => !departments.includes(dept))];
|
||||
const toAdd = [...departments.filter((dept: string) => !currentDepartments.includes(dept))];
|
||||
|
||||
await this.removeBusinessHourFromDepartmentsIfNeeded(businessHourId, toRemove);
|
||||
await this.removeBusinessHourFromDepartmentsAgents(businessHourId, toRemove);
|
||||
|
||||
// Now will check if the department which we're currently adding to BH is not
|
||||
// associated with any other BH. If it is, then it will remove the old BH from all user's
|
||||
// cache. It will not add the new BH right now as it will be done in afterSaveBusinessHour.
|
||||
await this.removeBHFromPreviouslyConnectedDepartmentAgentsIfRequired(toAdd);
|
||||
|
||||
await this.addBusinessHourToDepartmentsIfNeeded(businessHourId, toAdd);
|
||||
}
|
||||
|
||||
businessHourToReturn._id = businessHourId;
|
||||
return businessHourToReturn;
|
||||
}
|
||||
|
||||
async removeBusinessHourById(businessHourId: string): Promise<void> {
|
||||
const businessHour = await this.BusinessHourRepository.findOneById(businessHourId, {});
|
||||
if (!businessHour) {
|
||||
return;
|
||||
}
|
||||
await this.BusinessHourRepository.removeById(businessHourId);
|
||||
await this.removeBusinessHourFromAgents(businessHourId);
|
||||
await LivechatDepartment.removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId);
|
||||
|
||||
await makeAgentsUnavailableBasedOnBusinessHour();
|
||||
}
|
||||
|
||||
private async removeBusinessHourFromAgents(businessHourId: string): Promise<void> {
|
||||
const departmentIds = (
|
||||
await LivechatDepartment.findByBusinessHourId(businessHourId, {
|
||||
projection: { _id: 1 },
|
||||
}).toArray()
|
||||
).map((dept) => dept._id);
|
||||
await this.removeBusinessHourFromDepartmentsAgents(businessHourId, departmentIds);
|
||||
}
|
||||
|
||||
private async removeBusinessHourFromDepartmentsAgents(businessHourId: string, departmentIds: string[]): Promise<void> {
|
||||
if (!departmentIds.length) {
|
||||
return;
|
||||
}
|
||||
const agentIds = (
|
||||
await LivechatDepartmentAgents.findByDepartmentIds(departmentIds, {
|
||||
projection: { agentId: 1 },
|
||||
}).toArray()
|
||||
).map((dept) => dept.agentId);
|
||||
await this.UsersRepository.removeBusinessHourByAgentIds(agentIds, businessHourId);
|
||||
}
|
||||
|
||||
private async removeBusinessHourFromDepartmentsIfNeeded(businessHourId: string, departmentsToRemove: string[]): Promise<void> {
|
||||
if (!departmentsToRemove.length) {
|
||||
return;
|
||||
}
|
||||
await LivechatDepartment.removeBusinessHourFromDepartmentsByIdsAndBusinessHourId(departmentsToRemove, businessHourId);
|
||||
}
|
||||
|
||||
private async removeBHFromPreviouslyConnectedDepartmentAgentsIfRequired(departmentIds: string[]): Promise<void> {
|
||||
// we need to do 2 things here.
|
||||
// 1st is to check if any of the departments are associated with any BH. If they are, then we need to remove
|
||||
// that BH from all agents of that department.
|
||||
// 2nd is to check if any of the departments are not associated with BH, meaning default BH
|
||||
// is applied. So if default BH is open, then we need to remove it from all agents of that department.
|
||||
const bhIdsConnectedToDepartment = [];
|
||||
const departmentsWithoutBH = [];
|
||||
|
||||
const departments = await LivechatDepartment.findInIds(departmentIds, {
|
||||
projection: {
|
||||
businessHourId: 1,
|
||||
},
|
||||
}).toArray();
|
||||
|
||||
for (const dept of departments) {
|
||||
if (dept?.businessHourId) {
|
||||
bhIdsConnectedToDepartment.push(dept.businessHourId);
|
||||
} else {
|
||||
departmentsWithoutBH.push(dept._id);
|
||||
}
|
||||
}
|
||||
await Users.closeAgentsBusinessHoursByBusinessHourIds(bhIdsConnectedToDepartment);
|
||||
|
||||
// start of step 2
|
||||
const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour();
|
||||
if (!defaultBusinessHour) {
|
||||
bhLogger.error('No default business hour found');
|
||||
return;
|
||||
}
|
||||
|
||||
const isDefaultBHActive = (await filterBusinessHoursThatMustBeOpened([defaultBusinessHour])).length > 0;
|
||||
if (!isDefaultBHActive) {
|
||||
bhLogger.debug('Default business hour is not active. No need to remove it from agents');
|
||||
return;
|
||||
}
|
||||
|
||||
const agentsConnectedToDefaultBH = await (
|
||||
await LivechatDepartmentAgents.findByDepartmentIds(departmentIds, {
|
||||
projection: { agentId: 1 },
|
||||
}).toArray()
|
||||
).map((dept) => dept.agentId);
|
||||
|
||||
await Users.removeBusinessHourByAgentIds(agentsConnectedToDefaultBH, defaultBusinessHour._id);
|
||||
|
||||
await makeAgentsUnavailableBasedOnBusinessHour();
|
||||
}
|
||||
|
||||
private async addBusinessHourToDepartmentsIfNeeded(businessHourId: string, departmentsToAdd: string[]): Promise<void> {
|
||||
if (!departmentsToAdd.length) {
|
||||
return;
|
||||
}
|
||||
await LivechatDepartment.addBusinessHourToDepartmentsByIds(departmentsToAdd, businessHourId);
|
||||
}
|
||||
}
|
||||
|
||||
businessHourManager.registerBusinessHourType(new CustomBusinessHour());
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
|
||||
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
|
||||
import { LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models';
|
||||
|
||||
import {
|
||||
makeAgentsUnavailableBasedOnBusinessHour,
|
||||
makeOnlineAgentsAvailable,
|
||||
} from '../../../../../server/lib/omnichannel/business-hour/Helper';
|
||||
import { getAgentIdsForBusinessHour } from '../../../../../server/lib/omnichannel/business-hour/getAgentIdsForBusinessHour';
|
||||
import { businessHourLogger } from '../../../../../server/lib/omnichannel/logger';
|
||||
|
||||
export const getAgentIdsToHandle = async (businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>): Promise<string[]> => {
|
||||
if (businessHour.type === LivechatBusinessHourTypes.DEFAULT) {
|
||||
return getAgentIdsForBusinessHour();
|
||||
}
|
||||
const departmentIds = (
|
||||
await LivechatDepartment.findEnabledByBusinessHourId(businessHour._id, {
|
||||
projection: { _id: 1 },
|
||||
}).toArray()
|
||||
).map((dept) => dept._id);
|
||||
return (
|
||||
await LivechatDepartmentAgents.findByDepartmentIds(departmentIds, {
|
||||
projection: { agentId: 1 },
|
||||
}).toArray()
|
||||
).map((dept) => dept.agentId);
|
||||
};
|
||||
|
||||
export const openBusinessHour = async (
|
||||
businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>,
|
||||
updateLivechatStatus = true,
|
||||
): Promise<void> => {
|
||||
const agentIds = await getAgentIdsToHandle(businessHour);
|
||||
businessHourLogger.debug({
|
||||
msg: 'Opening business hour',
|
||||
businessHour: businessHour._id,
|
||||
totalAgents: agentIds.length,
|
||||
top10AgentIds: agentIds.slice(0, 10),
|
||||
});
|
||||
await Users.addBusinessHourByAgentIds(agentIds, businessHour._id);
|
||||
await makeOnlineAgentsAvailable(agentIds);
|
||||
|
||||
if (updateLivechatStatus) {
|
||||
await makeAgentsUnavailableBasedOnBusinessHour();
|
||||
}
|
||||
};
|
||||
|
||||
export const removeBusinessHourByAgentIds = async (agentIds: string[], businessHourId: string): Promise<void> => {
|
||||
if (!agentIds.length) {
|
||||
return;
|
||||
}
|
||||
await Users.removeBusinessHourByAgentIds(agentIds, businessHourId);
|
||||
await makeAgentsUnavailableBasedOnBusinessHour();
|
||||
};
|
||||
362
apps/meteor/ee/server/lib/omnichannel/business-hour/Multiple.ts
Normal file
362
apps/meteor/ee/server/lib/omnichannel/business-hour/Multiple.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
|
||||
import type { AtLeast, ILivechatDepartment, ILivechatBusinessHour } from '@rocket.chat/core-typings';
|
||||
import { LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models';
|
||||
import { isTruthy } from '@rocket.chat/tools';
|
||||
import moment from 'moment';
|
||||
|
||||
import { openBusinessHour, removeBusinessHourByAgentIds } from './Helper';
|
||||
import { businessHourManager } from '../../../../../server/lib/omnichannel/business-hour';
|
||||
import type { IBusinessHourBehavior } from '../../../../../server/lib/omnichannel/business-hour/AbstractBusinessHour';
|
||||
import { AbstractBusinessHourBehavior } from '../../../../../server/lib/omnichannel/business-hour/AbstractBusinessHour';
|
||||
import {
|
||||
filterBusinessHoursThatMustBeOpened,
|
||||
filterBusinessHoursThatMustBeOpenedByDay,
|
||||
makeOnlineAgentsAvailable,
|
||||
makeAgentsUnavailableBasedOnBusinessHour,
|
||||
} from '../../../../../server/lib/omnichannel/business-hour/Helper';
|
||||
import { closeBusinessHour } from '../../../../../server/lib/omnichannel/business-hour/closeBusinessHour';
|
||||
import { settings } from '../../../../../server/settings';
|
||||
import { bhLogger } from '../logger';
|
||||
|
||||
export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior {
|
||||
constructor() {
|
||||
super();
|
||||
this.onAddAgentToDepartment = this.onAddAgentToDepartment.bind(this);
|
||||
this.onRemoveAgentFromDepartment = this.onRemoveAgentFromDepartment.bind(this);
|
||||
this.onRemoveDepartment = this.onRemoveDepartment.bind(this);
|
||||
this.onDepartmentArchived = this.onDepartmentArchived.bind(this);
|
||||
this.onDepartmentDisabled = this.onDepartmentDisabled.bind(this);
|
||||
this.onNewAgentCreated = this.onNewAgentCreated.bind(this);
|
||||
}
|
||||
|
||||
async onStartBusinessHours(): Promise<void> {
|
||||
try {
|
||||
await this.UsersRepository.removeBusinessHoursFromAllUsers();
|
||||
|
||||
// TODO is this required? since we're calling `this.openBusinessHour(businessHour)` later on, which will call this again (kinda)
|
||||
await makeAgentsUnavailableBasedOnBusinessHour();
|
||||
|
||||
const currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
|
||||
const day = currentTime.format('dddd');
|
||||
const activeBusinessHours = await this.BusinessHourRepository.findActiveAndOpenBusinessHoursByDay(day, {
|
||||
projection: {
|
||||
workHours: 1,
|
||||
timezone: 1,
|
||||
type: 1,
|
||||
active: 1,
|
||||
},
|
||||
});
|
||||
const businessHoursToOpen = await filterBusinessHoursThatMustBeOpened(activeBusinessHours);
|
||||
bhLogger.info({
|
||||
msg: 'Starting Multiple Business Hours',
|
||||
totalBusinessHoursToOpen: businessHoursToOpen.length,
|
||||
top10BusinessHoursToOpen: businessHoursToOpen.slice(0, 10),
|
||||
});
|
||||
for (const businessHour of businessHoursToOpen) {
|
||||
void this.openBusinessHour(businessHour).catch((err) => {
|
||||
bhLogger.error({ msg: 'Error while opening business hour during start', businessHourId: businessHour._id, err });
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
bhLogger.error({ msg: 'Error while starting business hours', err });
|
||||
}
|
||||
}
|
||||
|
||||
async openBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
|
||||
const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToOpen(day, hour, undefined, {
|
||||
projection: {
|
||||
_id: 1,
|
||||
type: 1,
|
||||
},
|
||||
});
|
||||
for (const businessHour of businessHours) {
|
||||
void this.openBusinessHour(businessHour);
|
||||
}
|
||||
}
|
||||
|
||||
async closeBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
|
||||
const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToClose(day, hour, undefined, {
|
||||
projection: {
|
||||
_id: 1,
|
||||
type: 1,
|
||||
},
|
||||
});
|
||||
for (const businessHour of businessHours) {
|
||||
void this.closeBusinessHour(businessHour);
|
||||
}
|
||||
}
|
||||
|
||||
async afterSaveBusinessHours(businessHourData: ILivechatBusinessHour): Promise<void> {
|
||||
const businessHour = await this.BusinessHourRepository.findOneById(businessHourData._id);
|
||||
if (!businessHour) {
|
||||
return;
|
||||
}
|
||||
const businessHourIdToOpen = (await filterBusinessHoursThatMustBeOpened([businessHour])).map((businessHour) => businessHour._id);
|
||||
if (!businessHourIdToOpen.length) {
|
||||
return closeBusinessHour(businessHour);
|
||||
}
|
||||
return openBusinessHour(businessHour);
|
||||
}
|
||||
|
||||
async onAddAgentToDepartment(options: { departmentId: string; agentsId: string[] }): Promise<any> {
|
||||
const { departmentId, agentsId } = options;
|
||||
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'businessHourId'>>(departmentId, {
|
||||
projection: { businessHourId: 1 },
|
||||
});
|
||||
if (!department || !agentsId.length) {
|
||||
return options;
|
||||
}
|
||||
const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour();
|
||||
if (!defaultBusinessHour) {
|
||||
return options;
|
||||
}
|
||||
if (!department.businessHourId) {
|
||||
// If this department doesn't have a business hour, we need to apply default business hour to these agents
|
||||
// And then reset their status based on these BH
|
||||
const isDefaultBusinessHourActive = (await filterBusinessHoursThatMustBeOpened([defaultBusinessHour])).length > 0;
|
||||
if (!isDefaultBusinessHourActive) {
|
||||
bhLogger.debug('Default business hour is not active. No need to apply it to agents');
|
||||
return options;
|
||||
}
|
||||
|
||||
await this.UsersRepository.addBusinessHourByAgentIds(agentsId, defaultBusinessHour._id);
|
||||
await makeOnlineAgentsAvailable(agentsId);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// This department has a business hour, so we need to
|
||||
// 1. Remove default business hour from these agents if they have it
|
||||
// 2. Add this department's business hour to these agents
|
||||
// 3. Update their status based on these BH
|
||||
await removeBusinessHourByAgentIds(agentsId, defaultBusinessHour._id);
|
||||
|
||||
const businessHour = await this.BusinessHourRepository.findOneById(department.businessHourId);
|
||||
if (!businessHour) {
|
||||
return options;
|
||||
}
|
||||
const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([businessHour]);
|
||||
if (!businessHourToOpen.length) {
|
||||
return options;
|
||||
}
|
||||
|
||||
await this.UsersRepository.addBusinessHourByAgentIds(agentsId, businessHour._id);
|
||||
await makeOnlineAgentsAvailable(agentsId);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async onRemoveAgentFromDepartment(options: Record<string, any> = {}): Promise<any> {
|
||||
const { departmentId, agentsId } = options;
|
||||
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'businessHourId'>>(departmentId, {
|
||||
projection: { businessHourId: 1 },
|
||||
});
|
||||
if (!department || !agentsId.length) {
|
||||
return options;
|
||||
}
|
||||
|
||||
return this.handleRemoveAgentsFromDepartments(department, agentsId, options);
|
||||
}
|
||||
|
||||
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: AtLeast<ILivechatDepartment, 'businessHourId' | '_id'>): Promise<void> {
|
||||
if (!department.businessHourId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get business hour
|
||||
let businessHour = await this.BusinessHourRepository.findOneById(department.businessHourId);
|
||||
if (!businessHour) {
|
||||
bhLogger.error({
|
||||
msg: 'onDepartmentDisabled: business hour not found',
|
||||
businessHourId: department.businessHourId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Unlink business hour from department
|
||||
await LivechatDepartment.removeBusinessHourFromDepartmentsByIdsAndBusinessHourId([department._id], businessHour._id);
|
||||
|
||||
// cleanup user's cache for default business hour and this business hour
|
||||
const defaultBH = await this.BusinessHourRepository.findOneDefaultBusinessHour();
|
||||
if (!defaultBH) {
|
||||
throw new Error('Default business hour not found');
|
||||
}
|
||||
await this.UsersRepository.closeAgentsBusinessHoursByBusinessHourIds([businessHour._id, defaultBH._id]);
|
||||
|
||||
// If i'm the only one, disable the business hour
|
||||
const imTheOnlyOne = !(await LivechatDepartment.countByBusinessHourIdExcludingDepartmentId(businessHour._id, department._id));
|
||||
if (imTheOnlyOne) {
|
||||
bhLogger.warn({
|
||||
msg: 'onDepartmentDisabled: department is the only one on business hour, disabling it',
|
||||
departmentId: department._id,
|
||||
businessHourId: businessHour._id,
|
||||
});
|
||||
await this.BusinessHourRepository.disableBusinessHour(businessHour._id);
|
||||
|
||||
businessHour = await this.BusinessHourRepository.findOneById(department.businessHourId);
|
||||
if (!businessHour) {
|
||||
throw new Error(`Business hour ${department.businessHourId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// start default business hour and this BH if needed
|
||||
if (!settings.get('Livechat_enable_business_hours')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([businessHour, defaultBH]);
|
||||
for await (const bh of businessHourToOpen) {
|
||||
await openBusinessHour(bh, false);
|
||||
}
|
||||
|
||||
await makeAgentsUnavailableBasedOnBusinessHour();
|
||||
await businessHourManager.restartCronJobsIfNecessary();
|
||||
}
|
||||
|
||||
async onDepartmentArchived(department: Pick<ILivechatDepartment, '_id' | 'businessHourId'>): Promise<void> {
|
||||
bhLogger.debug({ msg: 'Processing department archived event on multiple business hours', department });
|
||||
return this.onDepartmentDisabled(department);
|
||||
}
|
||||
|
||||
override allowAgentChangeServiceStatus(agentId: string): Promise<boolean> {
|
||||
return this.UsersRepository.isAgentWithinBusinessHours(agentId);
|
||||
}
|
||||
|
||||
async onNewAgentCreated(agentId: string): Promise<void> {
|
||||
await this.applyAnyOpenBusinessHourToAgent(agentId);
|
||||
|
||||
await makeAgentsUnavailableBasedOnBusinessHour([agentId]);
|
||||
}
|
||||
|
||||
private async applyAnyOpenBusinessHourToAgent(agentId: string): Promise<void> {
|
||||
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);
|
||||
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<ILivechatDepartment, '_id' | 'businessHourId'>[];
|
||||
|
||||
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);
|
||||
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<string, any>, agentsIds: string[], options: any): Promise<any> {
|
||||
const agentIdsWithoutDepartment: string[] = [];
|
||||
const agentIdsToRemoveCurrentBusinessHour: string[] = [];
|
||||
|
||||
const [agentsWithDepartment, [agentsOfDepartment] = []] = await Promise.all([
|
||||
LivechatDepartmentAgents.findByAgentIds(agentsIds, { projection: { agentId: 1 } }).toArray(),
|
||||
...[department?.businessHourId ? LivechatDepartment.findAgentsByBusinessHourId(department.businessHourId).toArray() : []],
|
||||
]);
|
||||
|
||||
for (const agentId of agentsIds) {
|
||||
if (!agentsWithDepartment.find((agent) => agent.agentId === agentId)) {
|
||||
agentIdsWithoutDepartment.push(agentId);
|
||||
}
|
||||
if (!agentsOfDepartment?.agentIds?.find((agent) => agent === agentId)) {
|
||||
agentIdsToRemoveCurrentBusinessHour.push(agentId);
|
||||
}
|
||||
}
|
||||
if (department.businessHourId) {
|
||||
await removeBusinessHourByAgentIds(agentIdsToRemoveCurrentBusinessHour, department.businessHourId);
|
||||
}
|
||||
if (!agentIdsWithoutDepartment.length) {
|
||||
return options;
|
||||
}
|
||||
const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour();
|
||||
if (!defaultBusinessHour) {
|
||||
return options;
|
||||
}
|
||||
const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]);
|
||||
if (!businessHourToOpen.length) {
|
||||
return options;
|
||||
}
|
||||
await this.UsersRepository.addBusinessHourByAgentIds(agentIdsWithoutDepartment, defaultBusinessHour._id);
|
||||
return options;
|
||||
}
|
||||
|
||||
private async openBusinessHour(businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>): Promise<void> {
|
||||
return openBusinessHour(businessHour);
|
||||
}
|
||||
|
||||
private async closeBusinessHour(businessHour: Pick<ILivechatBusinessHour, '_id' | 'type'>): Promise<void> {
|
||||
await closeBusinessHour(businessHour);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import './Custom';
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
|
||||
import { LivechatBusinessHours, LivechatDepartment } from '@rocket.chat/models';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
|
||||
import { hasPermissionAsync } from '../../../../../../server/lib/authorization/hasPermission';
|
||||
import type { IPaginatedResponse, IPagination } from '../../../../api/v1/omnichannel/lib/definition';
|
||||
|
||||
interface IResponse extends IPaginatedResponse {
|
||||
businessHours: ILivechatBusinessHour[];
|
||||
}
|
||||
|
||||
export async function findBusinessHours(userId: string, { offset, count, sort }: IPagination, name?: string): Promise<IResponse> {
|
||||
if (!(await hasPermissionAsync(userId, 'view-livechat-business-hours'))) {
|
||||
throw new Error('error-not-authorized');
|
||||
}
|
||||
const query = {};
|
||||
if (name) {
|
||||
const filterReg = new RegExp(escapeRegExp(name), 'i');
|
||||
Object.assign(query, { name: filterReg });
|
||||
}
|
||||
const { cursor, totalCount } = LivechatBusinessHours.findPaginated(query, {
|
||||
sort: sort || { name: 1 },
|
||||
skip: offset,
|
||||
limit: count,
|
||||
});
|
||||
|
||||
const [businessHours, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
// add departments to businessHours
|
||||
const businessHoursWithDepartments = await Promise.all(
|
||||
businessHours.map(async (businessHour) => {
|
||||
const currentDepartments = await LivechatDepartment.findByBusinessHourId(businessHour._id, {
|
||||
projection: { _id: 1, name: 1 },
|
||||
}).toArray();
|
||||
|
||||
if (currentDepartments.length) {
|
||||
businessHour.departments = currentDepartments;
|
||||
}
|
||||
|
||||
return businessHour;
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
businessHours: businessHoursWithDepartments,
|
||||
count: businessHours.length,
|
||||
offset,
|
||||
total,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user