diff --git a/client/contexts/ServerContext/endpoints.ts b/client/contexts/ServerContext/endpoints.ts index 5d11d62c3581e..6e1d47204534b 100644 --- a/client/contexts/ServerContext/endpoints.ts +++ b/client/contexts/ServerContext/endpoints.ts @@ -11,6 +11,9 @@ import { ListEndpoint as EmojiCustomListEndpoint } from './endpoints/v1/emoji-cu import { FilesEndpoint as GroupsFilesEndpoint } from './endpoints/v1/groups/files'; import { FilesEndpoint as ImFilesEndpoint } from './endpoints/v1/im/files'; import { AppearanceEndpoint as LivechatAppearanceEndpoint } from './endpoints/v1/livechat/appearance'; +import { LivechatDepartment } from './endpoints/v1/livechat/department'; +import { LivechatDepartmentsByUnit } from './endpoints/v1/livechat/departmentsByUnit'; +import { LivechatMonitorsList } from './endpoints/v1/livechat/monitorsList'; import { LivechatRoomOnHoldEndpoint } from './endpoints/v1/livechat/onHold'; import { LivechatVisitorInfoEndpoint } from './endpoints/v1/livechat/visitorInfo'; import { AutocompleteAvailableForTeamsEndpoint as RoomsAutocompleteTeamsEndpoint } from './endpoints/v1/rooms/autocompleteAvailableForTeams'; @@ -40,6 +43,9 @@ export type ServerEndpoints = { 'teams.addRooms': TeamsAddRoomsEndpoint; 'livechat/visitors.info': LivechatVisitorInfoEndpoint; 'livechat/room.onHold': LivechatRoomOnHoldEndpoint; + 'livechat/monitors.list': LivechatMonitorsList; + 'livechat/department': LivechatDepartment; + 'livechat/departments.by-unit/': LivechatDepartmentsByUnit; }; export type ServerEndpointPath = keyof ServerEndpoints; diff --git a/client/contexts/ServerContext/endpoints/v1/livechat/department.ts b/client/contexts/ServerContext/endpoints/v1/livechat/department.ts new file mode 100644 index 0000000000000..00e9631589463 --- /dev/null +++ b/client/contexts/ServerContext/endpoints/v1/livechat/department.ts @@ -0,0 +1,7 @@ +export type LivechatDepartment = { + GET: (params: { + query: string; + }) => { + statuses: unknown[]; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/livechat/departmentsByUnit.ts b/client/contexts/ServerContext/endpoints/v1/livechat/departmentsByUnit.ts new file mode 100644 index 0000000000000..b51e499914723 --- /dev/null +++ b/client/contexts/ServerContext/endpoints/v1/livechat/departmentsByUnit.ts @@ -0,0 +1,13 @@ +import { ILivechatDepartment } from '../../../../../../definition/ILivechatDepartment'; +import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi'; + +export type LivechatDepartmentsByUnit = { + GET: (params: { + text: string; + offset: number; + count: number; + }) => { + departments: ObjectFromApi[]; + total: number; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/livechat/monitorsList.ts b/client/contexts/ServerContext/endpoints/v1/livechat/monitorsList.ts new file mode 100644 index 0000000000000..b00c039c3e16a --- /dev/null +++ b/client/contexts/ServerContext/endpoints/v1/livechat/monitorsList.ts @@ -0,0 +1,13 @@ +import { ILivechatMonitor } from '../../../../../../definition/ILivechatMonitor'; +import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi'; + +export type LivechatMonitorsList = { + GET: (params: { + text: string; + offset: number; + count: number; + }) => { + monitors: ObjectFromApi[]; + total: number; + }; +}; diff --git a/client/views/hooks/useDepartmentsList.ts b/client/views/hooks/useDepartmentsList.ts new file mode 100644 index 0000000000000..f17eac663aee7 --- /dev/null +++ b/client/views/hooks/useDepartmentsList.ts @@ -0,0 +1,63 @@ +import { useCallback, useState } from 'react'; + +import { ILivechatDepartmentRecord } from '../../../definition/ILivechatDepartmentRecord'; +import { useEndpoint } from '../../contexts/ServerContext'; +import { useScrollableRecordList } from '../../hooks/lists/useScrollableRecordList'; +import { useComponentDidUpdate } from '../../hooks/useComponentDidUpdate'; +import { RecordList } from '../../lib/lists/RecordList'; + +type DepartmentsListOptions = { + unitId: string; + filter: string; +}; + +export const useDepartmentsList = ( + options: DepartmentsListOptions, +): { + itemsList: RecordList; + initialItemCount: number; + reload: () => void; + loadMoreItems: (start: number, end: number) => void; +} => { + const [itemsList, setItemsList] = useState(() => new RecordList()); + const reload = useCallback(() => setItemsList(new RecordList()), []); + const endpoint = `livechat/departments.available-by-unit/${ + options.unitId || 'none' + }` as 'livechat/departments.by-unit/'; + + const getDepartments = useEndpoint('GET', endpoint); + + useComponentDidUpdate(() => { + options && reload(); + }, [options, reload]); + + const fetchData = useCallback( + async (start, end) => { + const { departments, total } = await getDepartments({ + text: options.filter, + offset: start, + count: end + start, + }); + + return { + items: departments.map((department: any) => { + department._updatedAt = new Date(department._updatedAt); + department.label = department.name; + department.value = { value: department._id, label: department.name }; + return department; + }), + itemCount: total, + }; + }, + [getDepartments, options.filter], + ); + + const { loadMoreItems, initialItemCount } = useScrollableRecordList(itemsList, fetchData, 25); + + return { + reload, + itemsList, + loadMoreItems, + initialItemCount, + }; +}; diff --git a/client/views/hooks/useMonitorsList.ts b/client/views/hooks/useMonitorsList.ts new file mode 100644 index 0000000000000..9b842c76c9a77 --- /dev/null +++ b/client/views/hooks/useMonitorsList.ts @@ -0,0 +1,60 @@ +import { useCallback, useState } from 'react'; + +import { ILivechatMonitorRecord } from '../../../definition/ILivechatMonitorRecord'; +import { useEndpoint } from '../../contexts/ServerContext'; +import { useScrollableRecordList } from '../../hooks/lists/useScrollableRecordList'; +import { useComponentDidUpdate } from '../../hooks/useComponentDidUpdate'; +import { RecordList } from '../../lib/lists/RecordList'; + +type MonitorsListOptions = { + filter: string; +}; + +export const useMonitorsList = ( + options: MonitorsListOptions, +): { + itemsList: RecordList; + initialItemCount: number; + reload: () => void; + loadMoreItems: (start: number, end: number) => void; +} => { + const [itemsList, setItemsList] = useState(() => new RecordList()); + const reload = useCallback(() => setItemsList(new RecordList()), []); + + const endpoint = 'livechat/monitors.list'; + + const getMonitors = useEndpoint('GET', endpoint); + + useComponentDidUpdate(() => { + options && reload(); + }, [options, reload]); + + const fetchData = useCallback( + async (start, end) => { + const { monitors, total } = await getMonitors({ + text: options.filter, + offset: start, + count: end + start, + }); + + return { + items: monitors.map((members: any) => { + members._updatedAt = new Date(members._updatedAt); + members.label = members.username; + members.value = { value: members._id, label: members.username }; + return members; + }), + itemCount: total, + }; + }, + [getMonitors, options.filter], + ); + + const { loadMoreItems, initialItemCount } = useScrollableRecordList(itemsList, fetchData, 25); + return { + reload, + itemsList, + loadMoreItems, + initialItemCount, + }; +}; diff --git a/definition/ILivechatDepartmentRecord.ts b/definition/ILivechatDepartmentRecord.ts new file mode 100644 index 0000000000000..9b2ff1d5100be --- /dev/null +++ b/definition/ILivechatDepartmentRecord.ts @@ -0,0 +1,17 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + + +export interface ILivechatDepartmentRecord extends IRocketChatRecord { + _id: string; + name: string; + enabled: boolean; + description: string; + showOnRegistration: boolean; + showOnOfflineForm: boolean; + requestTagBeforeClosingChat: boolean; + email: string; + chatClosingTags: string[]; + offlineMessageChannelName: string; + numAgents: number; + businessHourId?: string; +} diff --git a/definition/ILivechatMonitor.ts b/definition/ILivechatMonitor.ts new file mode 100644 index 0000000000000..231e4c855647c --- /dev/null +++ b/definition/ILivechatMonitor.ts @@ -0,0 +1,8 @@ +export interface ILivechatMonitor { + _id: string; + name: string; + enabled: boolean; + numMonitors: number; + type: string; + visibility: string; +} diff --git a/definition/ILivechatMonitorRecord.ts b/definition/ILivechatMonitorRecord.ts new file mode 100644 index 0000000000000..c9bb46302a80d --- /dev/null +++ b/definition/ILivechatMonitorRecord.ts @@ -0,0 +1,11 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + + +export interface ILivechatMonitorRecord extends IRocketChatRecord { + _id: string; + name: string; + enabled: boolean; + numMonitors: number; + type: string; + visibility: string; +} diff --git a/ee/app/livechat-enterprise/server/api/departments.js b/ee/app/livechat-enterprise/server/api/departments.js index e9a40f8119f9c..0472925afe333 100644 --- a/ee/app/livechat-enterprise/server/api/departments.js +++ b/ee/app/livechat-enterprise/server/api/departments.js @@ -12,6 +12,10 @@ import { findPercentageOfAbandonedRooms, findAllAverageOfChatDurationTime, } from '../../../../../app/livechat/server/lib/analytics/departments'; +import { + findAllDepartmentsAvailable, + findAllDepartmentsByUnit, +} from '../lib/Department'; API.v1.addRoute('livechat/analytics/departments/amount-of-chats', { authRequired: true }, { get() { @@ -318,3 +322,43 @@ API.v1.addRoute('livechat/analytics/departments/percentage-abandoned-chats', { a }); }, }); + +API.v1.addRoute('livechat/departments.available-by-unit/:unitId', { authRequired: true }, { + get() { + check(this.urlParams, { + unitId: Match.Maybe(String), + }); + const { offset, count } = this.getPaginationItems(); + const { unitId } = this.urlParams; + const { text } = this.queryParams; + + + const { departments, total } = Promise.await(findAllDepartmentsAvailable(unitId, offset, count, text)); + + return API.v1.success({ + departments, + count: departments.length, + offset, + total, + }); + }, +}); + +API.v1.addRoute('livechat/departments.by-unit/:unitId', { authRequired: true }, { + get() { + check(this.urlParams, { + unitId: String, + }); + const { offset, count } = this.getPaginationItems(); + const { unitId } = this.urlParams; + + const { departments, total } = Promise.await(findAllDepartmentsByUnit(unitId, offset, count)); + + return API.v1.success({ + departments, + count: departments.length, + offset, + total, + }); + }, +}); diff --git a/ee/app/livechat-enterprise/server/lib/Department.js b/ee/app/livechat-enterprise/server/lib/Department.js new file mode 100644 index 0000000000000..c15ec344df4bd --- /dev/null +++ b/ee/app/livechat-enterprise/server/lib/Department.js @@ -0,0 +1,32 @@ +import { escapeRegExp } from '@rocket.chat/string-helpers'; + +import { + LivechatDepartment, +} from '../../../../../app/models/server/raw'; + +export const findAllDepartmentsAvailable = async (unitId, offset, count, text) => { + const filterReg = new RegExp(escapeRegExp(text), 'i'); + + const cursor = LivechatDepartment.find({ + $or: [{ ancestors: { $in: [unitId] } }, { ancestors: { $exists: false } }], + ...text && { name: filterReg }, + + }, { limit: count, offset }); + + const departments = await cursor.toArray(); + const total = await cursor.count(); + const departmentsFiltered = departments.filter((department) => !department.ancestors?.length); + + return { departments: departmentsFiltered, total }; +}; + +export const findAllDepartmentsByUnit = async (unitId, offset, count) => { + const cursor = LivechatDepartment.find({ + ancestors: { $in: [unitId] }, + }, { limit: count, offset }); + + const total = await cursor.count(); + const departments = await cursor.toArray(); + + return { departments, total }; +}; diff --git a/ee/client/omnichannel/units/UnitEdit.js b/ee/client/omnichannel/units/UnitEdit.js index 38d836924ab1d..12146b727dc43 100644 --- a/ee/client/omnichannel/units/UnitEdit.js +++ b/ee/client/omnichannel/units/UnitEdit.js @@ -1,47 +1,59 @@ -import { Field, TextInput, Button, Box, MultiSelect, Select, Margins } from '@rocket.chat/fuselage'; +import { + Field, + TextInput, + Button, + Box, + PaginatedMultiSelectFiltered, + Select, + Margins, +} from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import VerticalBar from '../../../../client/components/VerticalBar'; import { useRoute } from '../../../../client/contexts/RouterContext'; import { useMethod } from '../../../../client/contexts/ServerContext'; import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext'; import { useTranslation } from '../../../../client/contexts/TranslationContext'; +import { useRecordList } from '../../../../client/hooks/lists/useRecordList'; +import { AsyncStatePhase } from '../../../../client/hooks/useAsyncState'; import { useForm } from '../../../../client/hooks/useForm'; +import { useDepartmentsList } from '../../../../client/views/hooks/useDepartmentsList'; +import { useMonitorsList } from '../../../../client/views/hooks/useMonitorsList'; -function UnitEdit({ - data, - unitId, - isNew, - availableDepartments, - availableMonitors, - unitMonitors, - reload, - ...props -}) { +function UnitEdit({ data, unitId, isNew, unitMonitors, unitDepartments, reload, ...props }) { const t = useTranslation(); const unitsRoute = useRoute('omnichannel-units'); + const [monitorsFilter, setMonitorsFilter] = useState(''); + const [departmentsFilter, setDepartmentsFilter] = useState(''); - const unit = data || {}; + const { itemsList: monitorsList, loadMoreItems: loadMoreMonitors } = useMonitorsList( + useMemo(() => ({ limit: 50, filter: monitorsFilter }), [monitorsFilter]), + ); - const depOptions = useMemo( - () => - availableDepartments && availableDepartments.departments - ? availableDepartments.departments.map(({ _id, name }) => [_id, name || _id]) - : [], - [availableDepartments], + const { phase: monitorsPhase, items: monitorsItems, itemCount: monitorsTotal } = useRecordList( + monitorsList, ); - const monOptions = useMemo( - () => - availableMonitors && availableMonitors.monitors - ? availableMonitors.monitors.map(({ _id, name }) => [_id, name || _id]) - : [], - [availableMonitors], + + const { itemsList: departmentsList, loadMoreItems: loadMoreDepartments } = useDepartmentsList( + useMemo(() => ({ limit: 50, filter: departmentsFilter }), [departmentsFilter]), ); + + const { + phase: departmentsPhase, + items: departmentsItems, + itemCount: departmentsTotal, + } = useRecordList(departmentsList); + + const unit = data || {}; + const currUnitMonitors = useMemo( () => unitMonitors && unitMonitors.monitors - ? unitMonitors.monitors.map(({ monitorId }) => monitorId) + ? unitMonitors.monitors.map(({ monitorId, username }) => ({ + value: monitorId, + label: username, + })) : [], [unitMonitors], ); @@ -49,26 +61,22 @@ function UnitEdit({ ['public', t('Public')], ['private', t('Private')], ]; - const unitDepartments = useMemo( + + const currUnitDepartments = useMemo( () => - availableDepartments && availableDepartments.departments && unitId - ? availableDepartments.departments - .map((department) => { - let result; - if (department.parentId === unitId) { - result = department._id; - } - return result; - }) - .filter((department) => !!department) + unitDepartments && unitDepartments.departments && unitId + ? unitDepartments.departments.map(({ _id, name }) => ({ + value: _id, + label: name, + })) : [], - [availableDepartments, unitId], + [unitDepartments, unitId], ); const { values, handlers, hasUnsavedChanges } = useForm({ name: unit.name, visibility: unit.visibility, - departments: unitDepartments, + departments: currUnitDepartments, monitors: currUnitMonitors, }); @@ -114,11 +122,11 @@ function UnitEdit({ const handleSave = useMutableCallback(async () => { const unitData = { name, visibility }; - const departmentsData = departments.map((department) => ({ departmentId: department })); - const monitorsData = monitors.map((monitor) => { - const monitorInfo = monOptions.find((el) => el[0] === monitor); - return { monitorId: monitorInfo[0], username: monitorInfo[1] }; - }); + const departmentsData = departments.map((department) => ({ departmentId: department.value })); + const monitorsData = monitors.map((monitor) => ({ + monitorId: monitor.value, + username: monitor.label, + })); if (!canSave) { return dispatchToastMessage({ type: 'error', message: t('The_field_is_required') }); @@ -164,28 +172,42 @@ function UnitEdit({ {t('Departments')}* - {} + : (start) => loadMoreDepartments(start, Math.min(50, departmentsTotal)) + } /> {t('Monitors')}* - {} + : (start) => loadMoreMonitors(start, Math.min(50, monitorsTotal)) + } /> diff --git a/ee/client/omnichannel/units/UnitEditWithData.js b/ee/client/omnichannel/units/UnitEditWithData.js index 6ac9af6391e62..4efd68507d79f 100644 --- a/ee/client/omnichannel/units/UnitEditWithData.js +++ b/ee/client/omnichannel/units/UnitEditWithData.js @@ -7,36 +7,28 @@ import { AsyncStatePhase } from '../../../../client/hooks/useAsyncState'; import { useEndpointData } from '../../../../client/hooks/useEndpointData'; import UnitEdit from './UnitEdit'; -function UnitEditWithData({ unitId, reload, allUnits }) { +function UnitEditWithData({ unitId, reload }) { const query = useMemo(() => ({ unitId }), [unitId]); const { value: data, phase: state, error } = useEndpointData('livechat/units.getOne', query); - const { - value: availableDepartments, - phase: availableDepartmentsState, - error: availableDepartmentsError, - } = useEndpointData('livechat/department'); - const { - value: availableMonitors, - phase: availableMonitorsState, - error: availableMonitorsError, - } = useEndpointData('livechat/monitors.list'); const { value: unitMonitors, phase: unitMonitorsState, error: unitMonitorsError, } = useEndpointData('livechat/unitMonitors.list', query); + const { + value: unitDepartments, + phase: unitDepartmentsState, + error: unitDepartmentsError, + } = useEndpointData(`livechat/departments.by-unit/${unitId}`); + const t = useTranslation(); - if ( - [state, availableDepartmentsState, availableMonitorsState, unitMonitorsState].includes( - AsyncStatePhase.LOADING, - ) - ) { + if ([state, unitMonitorsState, unitDepartmentsState].includes(AsyncStatePhase.LOADING)) { return ; } - if (error || availableDepartmentsError || availableMonitorsError || unitMonitorsError) { + if (error || unitMonitorsError || unitDepartmentsError) { return ( {t('Not_Available')} @@ -44,24 +36,12 @@ function UnitEditWithData({ unitId, reload, allUnits }) { ); } - const filteredDepartments = { - departments: availableDepartments.departments.filter( - (department) => - !allUnits || - !allUnits.units || - !department.ancestors || - department.ancestors[0] === unitId || - !allUnits.units.find((unit) => unit._id === department.ancestors[0]), - ), - }; - return ( );