feat: Omnichannel MAC limitations (#30463)
parent
747ec6c70e
commit
5b9d6883bf
@ -0,0 +1,10 @@ |
||||
--- |
||||
"@rocket.chat/meteor": patch |
||||
"@rocket.chat/core-services": patch |
||||
"@rocket.chat/core-typings": patch |
||||
"@rocket.chat/rest-typings": patch |
||||
"@rocket.chat/ddp-client": patch |
||||
--- |
||||
|
||||
feat: Improve UI when MAC limits are reached |
||||
feat: Limit endpoints on MAC limit reached |
||||
@ -0,0 +1,30 @@ |
||||
import { Omnichannel } from '@rocket.chat/core-services'; |
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import { isEditedMessage } from '@rocket.chat/core-typings'; |
||||
|
||||
import { callbacks } from '../../../../lib/callbacks'; |
||||
|
||||
callbacks.add('beforeSaveMessage', async (message, room) => { |
||||
if (!room || room.t !== 'l') { |
||||
return message; |
||||
} |
||||
|
||||
if (isEditedMessage(message)) { |
||||
return message; |
||||
} |
||||
|
||||
if (message.token) { |
||||
return message; |
||||
} |
||||
|
||||
if (message.t) { |
||||
return message; |
||||
} |
||||
|
||||
const canSendMessage = await Omnichannel.isWithinMACLimit(room as IOmnichannelRoom); |
||||
if (!canSendMessage) { |
||||
throw new Error('error-mac-limit-reached'); |
||||
} |
||||
|
||||
return message; |
||||
}); |
||||
@ -0,0 +1,6 @@ |
||||
import { useOmnichannel } from './useOmnichannel'; |
||||
|
||||
export const useIsOverMacLimit = (): boolean => { |
||||
const { isOverMacLimit } = useOmnichannel(); |
||||
return isOverMacLimit; |
||||
}; |
||||
@ -0,0 +1,23 @@ |
||||
import type { IRoom } from '@rocket.chat/core-typings'; |
||||
import { isOmnichannelRoom, type IOmnichannelGenericRoom, isVoipRoom } from '@rocket.chat/core-typings'; |
||||
|
||||
import { useIsOverMacLimit } from './useIsOverMacLimit'; |
||||
|
||||
const getPeriod = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; |
||||
|
||||
export const useIsRoomOverMacLimit = (room: IRoom) => { |
||||
const isOverMacLimit = useIsOverMacLimit(); |
||||
|
||||
if (!isOmnichannelRoom(room) && !isVoipRoom(room)) { |
||||
return false; |
||||
} |
||||
|
||||
if (!room.open) { |
||||
return false; |
||||
} |
||||
|
||||
const { v: { activity = [] } = {} } = room as IOmnichannelGenericRoom; |
||||
|
||||
const currentPeriod = getPeriod(new Date()); |
||||
return isOverMacLimit && !activity.includes(currentPeriod); |
||||
}; |
||||
@ -0,0 +1,22 @@ |
||||
import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; |
||||
import { isOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import React from 'react'; |
||||
|
||||
import { RoomActivityIcon } from '../../../ee/client/omnichannel/components/RoomActivityIcon'; |
||||
import { useOmnichannelPriorities } from '../../../ee/client/omnichannel/hooks/useOmnichannelPriorities'; |
||||
import { PriorityIcon } from '../../../ee/client/omnichannel/priorities/PriorityIcon'; |
||||
|
||||
export const OmnichannelBadges = ({ room }: { room: ISubscription & IRoom }) => { |
||||
const { enabled: isPriorityEnabled } = useOmnichannelPriorities(); |
||||
|
||||
if (!isOmnichannelRoom(room)) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
{isPriorityEnabled ? <PriorityIcon level={room.priorityWeight} /> : null} |
||||
<RoomActivityIcon room={room} /> |
||||
</> |
||||
); |
||||
}; |
||||
@ -0,0 +1,21 @@ |
||||
import { Icon, SidebarBanner } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import type { ReactElement } from 'react'; |
||||
import React from 'react'; |
||||
|
||||
export const OverMacLimitSection = (): ReactElement => { |
||||
const t = useTranslation(); |
||||
|
||||
const handleClick = () => { |
||||
window.open('https://rocket.chat/pricing', '_blank'); |
||||
}; |
||||
|
||||
return ( |
||||
<SidebarBanner |
||||
text={t('You_have_reached_the_limit_active_costumers_this_month')} |
||||
description={t('Learn_more')} |
||||
addon={<Icon name='warning' color='danger' size='x24' />} |
||||
onClick={handleClick} |
||||
/> |
||||
); |
||||
}; |
||||
@ -0,0 +1,20 @@ |
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
import { Icon } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import type { ReactElement } from 'react'; |
||||
import React from 'react'; |
||||
|
||||
import { useIsRoomOverMacLimit } from '../../../../../client/hooks/omnichannel/useIsRoomOverMacLimit'; |
||||
|
||||
type RoomActivityIconProps = { |
||||
room: IOmnichannelRoom; |
||||
}; |
||||
|
||||
export const RoomActivityIcon = ({ room }: RoomActivityIconProps): ReactElement | null => { |
||||
const t = useTranslation(); |
||||
const isRoomOverMacLimit = useIsRoomOverMacLimit(room); |
||||
|
||||
return isRoomOverMacLimit ? ( |
||||
<Icon name='warning' verticalAlign='middle' size='x20' color='danger' title={t('Workspace_exceeded_MAC_limit_disclaimer')} /> |
||||
) : null; |
||||
}; |
||||
@ -0,0 +1,69 @@ |
||||
import type { ILivechatVisitor } from '@rocket.chat/core-typings'; |
||||
import { expect } from 'chai'; |
||||
import { before, describe, it } from 'mocha'; |
||||
import moment from 'moment'; |
||||
|
||||
import { api, getCredentials, request, credentials } from '../../../data/api-data'; |
||||
import { |
||||
createVisitor, |
||||
createLivechatRoom, |
||||
createAgent, |
||||
makeAgentAvailable, |
||||
sendAgentMessage, |
||||
getLivechatRoomInfo, |
||||
} from '../../../data/livechat/rooms'; |
||||
import { IS_EE } from '../../../e2e/config/constants'; |
||||
|
||||
(IS_EE ? describe : describe.skip)('MAC', () => { |
||||
before((done) => getCredentials(done)); |
||||
|
||||
before(async () => { |
||||
await createAgent(); |
||||
await makeAgentAvailable(); |
||||
}); |
||||
|
||||
describe('MAC rooms', () => { |
||||
let visitor: ILivechatVisitor; |
||||
it('Should create an innactive room by default', async () => { |
||||
const visitor = await createVisitor(); |
||||
const room = await createLivechatRoom(visitor.token); |
||||
|
||||
expect(room).to.be.an('object'); |
||||
expect(room.v.activity).to.be.undefined; |
||||
}); |
||||
|
||||
it('should mark room as active when agent sends a message', async () => { |
||||
visitor = await createVisitor(); |
||||
const room = await createLivechatRoom(visitor.token); |
||||
|
||||
await sendAgentMessage(room._id); |
||||
|
||||
const updatedRoom = await getLivechatRoomInfo(room._id); |
||||
|
||||
expect(updatedRoom).to.have.nested.property('v.activity').and.to.be.an('array'); |
||||
}); |
||||
|
||||
it('should mark multiple rooms as active when they come from same visitor', async () => { |
||||
const room = await createLivechatRoom(visitor.token); |
||||
|
||||
await sendAgentMessage(room._id); |
||||
|
||||
const updatedRoom = await getLivechatRoomInfo(room._id); |
||||
|
||||
expect(updatedRoom).to.have.nested.property('v.activity').and.to.be.an('array'); |
||||
}); |
||||
|
||||
it('visitor should be marked as active for period', async () => { |
||||
const { body } = await request |
||||
.get(api(`livechat/visitors.info?visitorId=${visitor._id}`)) |
||||
.set(credentials) |
||||
.expect('Content-Type', 'application/json') |
||||
.expect(200); |
||||
|
||||
expect(body).to.have.nested.property('visitor').and.to.be.an('object'); |
||||
expect(body.visitor).to.have.nested.property('activity').and.to.be.an('array'); |
||||
expect(body.visitor.activity).to.have.lengthOf(1); |
||||
expect(body.visitor.activity[0]).to.equal(moment.utc().format('YYYY-MM')); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,7 +1,8 @@ |
||||
import type { IOmnichannelQueue } from '@rocket.chat/core-typings'; |
||||
import type { AtLeast, IOmnichannelQueue, IOmnichannelRoom } from '@rocket.chat/core-typings'; |
||||
|
||||
import type { IServiceClass } from './ServiceClass'; |
||||
|
||||
export interface IOmnichannelService extends IServiceClass { |
||||
getQueueWorker(): IOmnichannelQueue; |
||||
isWithinMACLimit(_room: AtLeast<IOmnichannelRoom, 'v'>): Promise<boolean>; |
||||
} |
||||
|
||||
Loading…
Reference in new issue