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
8 changes: 8 additions & 0 deletions .changeset/strange-rivers-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@rocket.chat/core-typings': minor
'@rocket.chat/i18n': minor
'@rocket.chat/meteor': minor
---

Added support for allowing agents to forward inquiries to departments that may not have any online agents given that `Allow department to receive forwarded inquiries even when there's no available agents` is set to `true` in the department configuration.
This configuration empowers agents to seamlessly direct incoming requests to the designated department, ensuring efficient handling of queries even when departmental resources are not actively online. When an agent becomes available, any pending inquiries will be automatically routed to them if the routing algorithm supports it.
23 changes: 16 additions & 7 deletions apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,10 +539,24 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi
agent = { agentId, username };
}

if (!RoutingManager.getConfig()?.autoAssignAgent || !(await Omnichannel.isWithinMACLimit(room))) {
const department = await LivechatDepartment.findOneById<
Pick<ILivechatDepartment, 'allowReceiveForwardOffline' | 'fallbackForwardDepartment' | 'name'>
>(departmentId, {
projection: {
allowReceiveForwardOffline: 1,
fallbackForwardDepartment: 1,
name: 1,
},
});

if (
!RoutingManager.getConfig()?.autoAssignAgent ||
!(await Omnichannel.isWithinMACLimit(room)) ||
(department?.allowReceiveForwardOffline && !(await LivechatTyped.checkOnlineAgents(departmentId)))
Comment thread
ggazzo marked this conversation as resolved.
) {
logger.debug(`Room ${room._id} will be on department queue`);
await LivechatTyped.saveTransferHistory(room, transferData);
return RoutingManager.unassignAgent(inquiry, departmentId);
return RoutingManager.unassignAgent(inquiry, departmentId, true);
}

// Fake the department to forward the inquiry - Case the forward process does not success
Expand All @@ -559,11 +573,6 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi

const { servedBy, chatQueued } = roomTaken;
if (!chatQueued && oldServedBy && servedBy && oldServedBy._id === servedBy._id) {
const department = departmentId
? await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'fallbackForwardDepartment' | 'name'>>(departmentId, {
projection: { fallbackForwardDepartment: 1, name: 1 },
})
: null;
if (!department?.fallbackForwardDepartment?.length) {
logger.debug(`Cannot forward room ${room._id}. Chat assigned to agent ${servedBy._id} (Previous was ${oldServedBy._id})`);
throw new Error('error-no-agents-online-in-department');
Expand Down
8 changes: 6 additions & 2 deletions apps/meteor/app/livechat/server/lib/RoutingManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type Routing = {
options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any } },
): Promise<(IOmnichannelRoom & { chatQueued?: boolean }) | null | void>;
assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent): Promise<InquiryWithAgentInfo>;
unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string): Promise<boolean>;
unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string, shouldQueue?: boolean): Promise<boolean>;
takeInquiry(
inquiry: Omit<
ILivechatInquiryRecord,
Expand Down Expand Up @@ -158,7 +158,7 @@ export const RoutingManager: Routing = {
return inquiry;
},

async unassignAgent(inquiry, departmentId) {
async unassignAgent(inquiry, departmentId, shouldQueue = false) {
const { rid, department } = inquiry;
const room = await LivechatRooms.findOneById(rid);

Expand All @@ -181,6 +181,10 @@ export const RoutingManager: Routing = {

const { servedBy } = room;

if (shouldQueue) {
Comment thread
AllanPazRibeiro marked this conversation as resolved.
await LivechatInquiry.queueInquiry(inquiry._id);
}

if (servedBy) {
await LivechatRooms.removeAgentByRoomId(rid);
await this.removeAllRoomSubscriptions(room);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ declare module '@rocket.chat/ui-contexts' {
chatClosingTags?: string[];
fallbackForwardDepartment?: string;
departmentsAllowedToForward?: string[];
allowReceiveForwardOffline?: boolean;
},
departmentAgents?:
| {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type FormValues = {
fallbackForwardDepartment: string;
agentList: IDepartmentAgent[];
chatClosingTags: string[];
allowReceiveForwardOffline: boolean;
};

function withDefault<T>(key: T | undefined | null, defaultValue: T) {
Expand All @@ -96,6 +97,7 @@ const getInitialValues = ({ department, agents, allowedToForwardData }: InitialV
fallbackForwardDepartment: withDefault(department?.fallbackForwardDepartment, ''),
chatClosingTags: department?.chatClosingTags ?? [],
agentList: agents || [],
allowReceiveForwardOffline: withDefault(department?.allowReceiveForwardOffline, false),
});

function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmentProps) {
Expand Down Expand Up @@ -151,6 +153,7 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen
waitingQueueMessage,
departmentsAllowedToForward,
fallbackForwardDepartment,
allowReceiveForwardOffline,
} = data;

const payload = {
Expand All @@ -169,6 +172,7 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen
waitingQueueMessage,
departmentsAllowedToForward: departmentsAllowedToForward?.map((dep) => dep.value),
fallbackForwardDepartment,
allowReceiveForwardOffline,
};

try {
Expand Down Expand Up @@ -214,6 +218,7 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen
const fallbackForwardDepartmentField = useUniqueId();
const requestTagBeforeClosingChatField = useUniqueId();
const chatClosingTagsField = useUniqueId();
const allowReceiveForwardOffline = useUniqueId();

return (
<Page flexDirection='row'>
Expand Down Expand Up @@ -424,6 +429,15 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen
<ToggleSwitch id={requestTagBeforeClosingChatField} {...register('requestTagBeforeClosingChat')} />
</FieldRow>
</Field>
<Field>
<FieldRow>
<FieldLabel htmlFor={allowReceiveForwardOffline}>{t('Accept_receive_inquiry_no_online_agents')}</FieldLabel>
<ToggleSwitch id={allowReceiveForwardOffline} {...register('allowReceiveForwardOffline')} />
Comment thread
AllanPazRibeiro marked this conversation as resolved.
</FieldRow>
<FieldRow>
<FieldHint id={`${allowReceiveForwardOffline}-hint`}>{t('Accept_receive_inquiry_no_online_agents_Hint')}</FieldHint>
</FieldRow>
</Field>
{requestTagBeforeClosingChat && (
<Field>
<FieldLabel htmlFor={chatClosingTagsField} required>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ export const LivechatEnterprise = {
chatClosingTags: Match.Optional([String]),
fallbackForwardDepartment: Match.Optional(String),
departmentsAllowedToForward: Match.Optional([String]),
allowReceiveForwardOffline: Match.Optional(Boolean),
};

// The Livechat Form department support addition/custom fields, so those fields need to be added before validating
Expand Down
52 changes: 42 additions & 10 deletions apps/meteor/tests/data/livechat/department.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect } from 'chai';
import type { ILivechatDepartment, IUser, LivechatDepartmentDTO } from '@rocket.chat/core-typings';
import { api, credentials, methodCall, request } from '../api-data';
import { IUserCredentialsHeader } from '../user';
import { createAnOnlineAgent } from './users';
import { createAnOnlineAgent, createAnOfflineAgent } from './users';
import { WithRequiredProperty } from './utils';

export const NewDepartmentData = ((): Partial<ILivechatDepartment> => ({
Expand All @@ -29,22 +29,29 @@ export const updateDepartment = async (departmentId: string, departmentData: Par
return response.body.department;
};

export const createDepartmentWithMethod = (initialAgents: { agentId: string, username: string }[] = []) =>
export const createDepartmentWithMethod = (
initialAgents: { agentId: string, username: string }[] = [],
allowReceiveForwardOffline = false) =>
new Promise((resolve, reject) => {
request
.post(methodCall('livechat:saveDepartment'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:saveDepartment',
params: ['', {
enabled: true,
email: faker.internet.email(),
showOnRegistration: true,
showOnOfflineForm: true,
name: `new department ${Date.now()}`,
description: 'created from api',
}, initialAgents],
params: [
'',
{
enabled: true,
email: faker.internet.email(),
showOnRegistration: true,
showOnOfflineForm: true,
name: `new department ${Date.now()}`,
description: 'created from api',
allowReceiveForwardOffline,
},
initialAgents,
],
id: 'id',
msg: 'method',
}),
Expand Down Expand Up @@ -102,6 +109,31 @@ export const addOrRemoveAgentFromDepartment = async (departmentId: string, agent
throw new Error('Failed to add or remove agent from department. Status code: ' + response.status + '\n' + response.body);
}
}
export const createDepartmentWithAnOfflineAgent = async ({
allowReceiveForwardOffline = false,
}: {
allowReceiveForwardOffline: boolean;
}): Promise<{
department: ILivechatDepartment;
agent: {
credentials: IUserCredentialsHeader;
user: WithRequiredProperty<IUser, 'username'>;
};
}> => {
const { user, credentials } = await createAnOfflineAgent();

const department = (await createDepartmentWithMethod(undefined, allowReceiveForwardOffline)) as ILivechatDepartment;

await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true);

return {
department,
agent: {
credentials,
user,
},
};
};

export const archiveDepartment = async (departmentId: string): Promise<void> => {
await request.post(api(`livechat/department/${ departmentId }/archive`)).set(credentials).expect(200);
Expand Down
20 changes: 19 additions & 1 deletion apps/meteor/tests/data/livechat/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { faker } from "@faker-js/faker";
import type { ILivechatAgent, IUser } from "@rocket.chat/core-typings";
import { IUserCredentialsHeader, password } from "../user";
import { createUser, login } from "../users.helper";
import { createAgent, makeAgentAvailable } from "./rooms";
import { createAgent, makeAgentAvailable, makeAgentUnavailable } from "./rooms";
import { api, credentials, request } from "../api-data";

export const createBotAgent = async (): Promise<{
Expand Down Expand Up @@ -57,3 +57,21 @@ export const createAnOnlineAgent = async (): Promise<{
user: agent,
};
}

export const createAnOfflineAgent = async (): Promise<{
credentials: IUserCredentialsHeader;
user: IUser & { username: string };
}> => {
const username = `user.test.${Date.now()}.offline`;
const email = `${username}.offline@rocket.chat`;
const { body } = await request.post(api('users.create')).set(credentials).send({ email, name: username, username, password });
const agent = body.user;
const createdUserCredentials = await login(agent.username, password);
await createAgent(agent.username);
await makeAgentUnavailable(createdUserCredentials);

return {
credentials: createdUserCredentials,
user: agent,
};
};
Loading