diff --git a/.changeset/giant-singers-melt.md b/.changeset/giant-singers-melt.md new file mode 100644 index 000000000000..266c18bb13c8 --- /dev/null +++ b/.changeset/giant-singers-melt.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds filters options to the omnichannel directory chats tab diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx index f0ea3c5b40ed..614231b003ef 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx @@ -1,7 +1,7 @@ import { Callout, Pagination } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import type { GETLivechatRoomsParams } from '@rocket.chat/rest-typings'; -import { usePermission } from '@rocket.chat/ui-contexts'; +import { usePermission, useRouter } from '@rocket.chat/ui-contexts'; import { hashQueryKey } from '@tanstack/react-query'; import moment from 'moment'; import type { ComponentProps, ReactElement } from 'react'; @@ -131,6 +131,7 @@ const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: s const [customFields, setCustomFields] = useState<{ [key: string]: string }>(); const { t } = useTranslation(); + const directoryPath = useRouter().buildRoutePath('/omnichannel-directory'); const canRemoveClosedChats = usePermission('remove-closed-livechat-room'); const { enabled: isPriorityEnabled } = useOmnichannelPriorities(); @@ -204,7 +205,11 @@ const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: s {getStatusText(open, onHold, !!servedBy?.username)} - {canRemoveClosedChats && !open && } + {canRemoveClosedChats && ( + + {!open && } + + )} ); }, @@ -304,10 +309,7 @@ const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: s Manage conversations in the - - contact center - - . + contact center. {((isSuccess && data?.rooms.length > 0) || queryHasChanged) && ( diff --git a/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx b/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx index f487eed3599f..796028d024b0 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx @@ -1,27 +1,27 @@ import { IconButton } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import GenericModal from '../../../components/GenericModal'; -import { GenericTableCell } from '../../../components/GenericTable'; import { useRemoveCurrentChatMutation } from './hooks/useRemoveCurrentChatMutation'; type RemoveChatButtonProps = { _id: string }; const RemoveChatButton = ({ _id }: RemoveChatButtonProps) => { - const removeCurrentChatMutation = useRemoveCurrentChatMutation(); + const t = useTranslation(); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); - const t = useTranslation(); - const handleRemoveClick = useMutableCallback(async () => { + const removeCurrentChatMutation = useRemoveCurrentChatMutation(); + + const handleRemoveClick = useEffectEvent(async () => { removeCurrentChatMutation.mutate(_id); }); - const handleDelete = useMutableCallback((e) => { + const handleDelete = useEffectEvent((e) => { e.stopPropagation(); - const onDeleteAgent = async (): Promise => { + const onDeleteAgent = async () => { try { await handleRemoveClick(); dispatchToastMessage({ type: 'success', message: t('Chat_removed') }); @@ -31,27 +31,18 @@ const RemoveChatButton = ({ _id }: RemoveChatButtonProps) => { setModal(null); }; - const handleClose = (): void => { - setModal(null); - }; - setModal( setModal(null)} confirmText={t('Delete')} />, ); }); - return ( - - - - ); + return ; }; export default RemoveChatButton; diff --git a/apps/meteor/client/views/omnichannel/directory/ChatsContextualBar.tsx b/apps/meteor/client/views/omnichannel/directory/ChatsContextualBar.tsx index 7493a72becb2..8be6e809f6c2 100644 --- a/apps/meteor/client/views/omnichannel/directory/ChatsContextualBar.tsx +++ b/apps/meteor/client/views/omnichannel/directory/ChatsContextualBar.tsx @@ -2,6 +2,7 @@ import { useRouteParameter, useRouter } from '@rocket.chat/ui-contexts'; import React from 'react'; import ContactHistoryMessagesList from '../contactHistory/MessageList/ContactHistoryMessagesList'; +import ChatFiltersContextualBar from './chats/ChatFiltersContextualBar'; const ChatsContextualBar = () => { const router = useRouter(); @@ -11,6 +12,10 @@ const ChatsContextualBar = () => { const handleOpenRoom = () => id && router.navigate(`/live/${id}`); const handleClose = () => router.navigate('/omnichannel-directory/chats'); + if (context === 'filters') { + return ; + } + if (context === 'info' && id) { return ; } diff --git a/apps/meteor/client/views/omnichannel/directory/OmnichannelDirectoryPage.tsx b/apps/meteor/client/views/omnichannel/directory/OmnichannelDirectoryPage.tsx index 34d52bbfb399..faf2ed82eee8 100644 --- a/apps/meteor/client/views/omnichannel/directory/OmnichannelDirectoryPage.tsx +++ b/apps/meteor/client/views/omnichannel/directory/OmnichannelDirectoryPage.tsx @@ -9,7 +9,7 @@ import CallTab from './calls/CallTab'; import ChatTab from './chats/ChatTab'; import ContactTab from './contacts/ContactTab'; -const DEFAULT_TAB = 'contacts'; +const DEFAULT_TAB = 'chats'; const OmnichannelDirectoryPage = () => { const t = useTranslation(); @@ -39,19 +39,19 @@ const OmnichannelDirectoryPage = () => { - handleTabClick('contacts')}> - {t('Contacts')} - handleTabClick('chats')}> {t('Chats')} + handleTabClick('contacts')}> + {t('Contacts')} + handleTabClick('calls')}> {t('Calls')} - {tab === 'contacts' && } {tab === 'chats' && } + {tab === 'contacts' && } {tab === 'calls' && } diff --git a/apps/meteor/client/views/omnichannel/directory/chats/ChatFilterByText.tsx b/apps/meteor/client/views/omnichannel/directory/chats/ChatFilterByText.tsx new file mode 100644 index 000000000000..cd03a70c015f --- /dev/null +++ b/apps/meteor/client/views/omnichannel/directory/chats/ChatFilterByText.tsx @@ -0,0 +1,93 @@ +import { Box, Button, Chip } from '@rocket.chat/fuselage'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { GenericMenu } from '@rocket.chat/ui-client'; +import { useMethod, useRoute, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; +import React from 'react'; + +import FilterByText from '../../../../components/FilterByText'; +import GenericModal from '../../../../components/GenericModal'; +import { useChatsFilters } from './useChatsFilters'; + +const ChatFilterByText = () => { + const t = useTranslation(); + const setModal = useSetModal(); + const dispatchToastMessage = useToastMessageDispatch(); + const directoryRoute = useRoute('omnichannel-directory'); + const removeClosedChats = useMethod('livechat:removeAllClosedRooms'); + const queryClient = useQueryClient(); + + const { displayFilters, setFiltersQuery, removeFilter } = useChatsFilters(); + + const handleRemoveAllClosed = useEffectEvent(async () => { + const onDeleteAll = async () => { + try { + await removeClosedChats(); + queryClient.invalidateQueries(['current-chats']); + dispatchToastMessage({ type: 'success', message: t('Chat_removed') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + setModal(null); + } + }; + + setModal( + setModal(null)} + confirmText={t('Delete')} + />, + ); + }); + + const menuItems = [ + { + items: [ + { + id: 'delete-all-closed-chats', + variant: 'danger', + icon: 'trash', + content: t('Delete_all_closed_chats'), + onClick: handleRemoveAllClosed, + } as const, + ], + }, + ]; + + return ( + <> + setFiltersQuery((prevState) => ({ ...prevState, guest: text }))}> + + + + + {Object.entries(displayFilters).map(([value, label], index) => { + if (!label) { + return null; + } + + return ( + removeFilter(value)}> + {label} + + ); + })} + + + ); +}; + +export default ChatFilterByText; diff --git a/apps/meteor/client/views/omnichannel/directory/chats/ChatFiltersContextualBar.tsx b/apps/meteor/client/views/omnichannel/directory/chats/ChatFiltersContextualBar.tsx new file mode 100644 index 000000000000..0fb0e9917505 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/directory/chats/ChatFiltersContextualBar.tsx @@ -0,0 +1,186 @@ +import { Button, ButtonGroup, Field, FieldLabel, FieldRow, InputBox, Select, TextInput } from '@rocket.chat/fuselage'; +import { useEndpoint, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { format } from 'date-fns'; +import React from 'react'; +import { Controller, useForm } from 'react-hook-form'; + +import AutoCompleteAgent from '../../../../components/AutoCompleteAgent'; +import AutoCompleteDepartment from '../../../../components/AutoCompleteDepartment'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, + ContextualbarFooter, +} from '../../../../components/Contextualbar'; +import { CurrentChatTags } from '../../additionalForms'; +import type { ChatsFiltersQuery } from './useChatsFilters'; +import { useChatsFilters } from './useChatsFilters'; + +type ChatFiltersContextualBarProps = { + onClose: () => void; +}; + +const ChatFiltersContextualBar = ({ onClose }: ChatFiltersContextualBarProps) => { + const t = useTranslation(); + const canViewLivechatRooms = usePermission('view-livechat-rooms'); + const canViewCustomFields = usePermission('view-livechat-room-customfields'); + + const allCustomFields = useEndpoint('GET', '/v1/livechat/custom-fields'); + const { data } = useQuery(['livechat/custom-fields'], async () => allCustomFields()); + const contactCustomFields = data?.customFields.filter((customField) => customField.scope !== 'visitor'); + + const { filtersQuery, setFiltersQuery, resetFiltersQuery } = useChatsFilters(); + const queryClient = useQueryClient(); + + const { + formState: { isDirty }, + handleSubmit, + control, + reset, + } = useForm({ + values: filtersQuery, + }); + + const statusOptions: [string, string][] = [ + ['all', t('All')], + ['closed', t('Closed')], + ['opened', t('Room_Status_Open')], + ['onhold', t('On_Hold_Chats')], + ['queued', t('Queued')], + ]; + + const handleSubmitFilters = (data: ChatsFiltersQuery) => { + setFiltersQuery(({ guest }) => ({ ...data, guest })); + queryClient.invalidateQueries(['current-chats']); + }; + + const handleResetFilters = () => { + resetFiltersQuery(); + reset(); + }; + + return ( + <> + + + {t('Filters')} + + + + + {t('From')} + + } + /> + + + + {t('To')} + + } + /> + + + {canViewLivechatRooms && ( + + {t('Served_By')} + + } + /> + + + )} + + {t('Status')} + [item, item])} + /> + )} + /> + + + ); + } + + return ( + + {customField.label} + + } + /> + + + ); + })} + + + + + + + + + ); +}; + +export default ChatFiltersContextualBar; diff --git a/apps/meteor/client/views/omnichannel/directory/chats/ChatTable.tsx b/apps/meteor/client/views/omnichannel/directory/chats/ChatTable.tsx index 46b5b6784ce0..f24f4779bee3 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/ChatTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/chats/ChatTable.tsx @@ -1,139 +1,93 @@ -import { Tag, Box, Pagination, States, StatesIcon, StatesTitle, StatesActions, StatesAction } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useRoute, useTranslation, useUserId } from '@rocket.chat/ui-contexts'; +import { Pagination, States, StatesIcon, StatesTitle, StatesActions, StatesAction } from '@rocket.chat/fuselage'; +import { usePermission, useTranslation } from '@rocket.chat/ui-contexts'; import { hashQueryKey } from '@tanstack/react-query'; -import moment from 'moment'; -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo } from 'react'; -import FilterByText from '../../../../components/FilterByText'; import GenericNoResults from '../../../../components/GenericNoResults/GenericNoResults'; import { GenericTable, GenericTableBody, - GenericTableCell, GenericTableHeader, GenericTableHeaderCell, GenericTableLoadingTable, - GenericTableRow, } from '../../../../components/GenericTable'; import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../../components/GenericTable/hooks/useSort'; +import { useOmnichannelPriorities } from '../../../../omnichannel/hooks/useOmnichannelPriorities'; import { useCurrentChats } from '../../currentChats/hooks/useCurrentChats'; +import ChatFilterByText from './ChatFilterByText'; +import ChatTableRow from './ChatTableRow'; +import { useChatsFilters } from './useChatsFilters'; +import { useChatsQuery } from './useChatsQuery'; const ChatTable = () => { const t = useTranslation(); - const [text, setText] = useState(''); - const userIdLoggedIn = useUserId(); - const directoryRoute = useRoute('omnichannel-directory'); + const canRemoveClosedChats = usePermission('remove-closed-livechat-room'); + + const { enabled: isPriorityEnabled } = useOmnichannelPriorities(); + const { filtersQuery: filters } = useChatsFilters(); + const chatsQuery = useChatsQuery(); const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); - const { sortBy, sortDirection, setSort } = useSort<'fname' | 'department' | 'ts' | 'chatDuration' | 'closedAt'>('fname'); + const { sortBy, sortDirection, setSort } = useSort<'fname' | 'priorityWeight' | 'department.name' | 'servedBy' | 'ts' | 'lm' | 'status'>( + 'fname', + ); const query = useMemo( - () => ({ - sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, - roomName: text || '', - agents: userIdLoggedIn ? [userIdLoggedIn] : [], - ...(itemsPerPage && { count: itemsPerPage }), - ...(current && { offset: current }), - }), - [sortBy, current, sortDirection, itemsPerPage, userIdLoggedIn, text], + () => chatsQuery(filters, [sortBy, sortDirection], current, itemsPerPage), + [itemsPerPage, filters, sortBy, sortDirection, current, chatsQuery], ); - const onRowClick = useMutableCallback((id) => - directoryRoute.push({ - tab: 'chats', - context: 'info', - id, - }), - ); + const { data, isLoading, isSuccess, isError, refetch } = useCurrentChats(query); + + const [defaultQuery] = useState(hashQueryKey([query])); + const queryHasChanged = defaultQuery !== hashQueryKey([query]); const headers = ( <> - + {t('Contact_Name')} + {isPriorityEnabled && ( + + {t('Priority')} + + )} {t('Department')} - + + {t('Served_By')} + + {t('Started_At')} - - {t('Chat_Duration')} + + {t('Last_Message')} - - {t('Closed_At')} + + {t('Status')} + {canRemoveClosedChats && } ); - const { data, isLoading, isSuccess, isError, refetch } = useCurrentChats(query); - - const [defaultQuery] = useState(hashQueryKey([query])); - const queryHasChanged = defaultQuery !== hashQueryKey([query]); - - const renderRow = useCallback( - ({ _id, fname, ts, closedAt, department, tags }) => ( - onRowClick(_id)} action qa-user-id={_id}> - - - {fname} - {tags && ( - - {tags.map((tag: string) => ( - 10 ? 'hidden' : 'visible', - textOverflow: 'ellipsis', - }} - key={tag} - mie={4} - > - - {tag} - - - ))} - - )} - - - {department ? department.name : ''} - {moment(ts).format('L LTS')} - {moment(closedAt).from(moment(ts), true)} - {moment(closedAt).format('L LTS')} - - ), - [onRowClick], - ); - return ( <> - {((isSuccess && data?.rooms.length > 0) || queryHasChanged) && } + {isLoading && ( {headers} @@ -156,7 +110,11 @@ const ChatTable = () => { <> {headers} - {data?.rooms.map((room) => renderRow(room))} + + {data?.rooms.map((room) => ( + + ))} + { + const t = useTranslation(); + const { _id, fname, tags, servedBy, ts, lm, department, open, onHold, priorityWeight } = room; + const { enabled: isPriorityEnabled } = useOmnichannelPriorities(); + + const canRemoveClosedChats = usePermission('remove-closed-livechat-room'); + + const directoryRoute = useRoute('omnichannel-directory'); + + const getStatusText = (open = false, onHold = false): string => { + if (!open) { + return t('Closed'); + } + + if (open && !servedBy) { + return t('Queued'); + } + + return onHold ? t('On_Hold_Chats') : t('Room_Status_Open'); + }; + + const onRowClick = useEffectEvent((id) => + directoryRoute.push({ + tab: 'chats', + context: 'info', + id, + }), + ); + + return ( + onRowClick(_id)} action qa-user-id={_id}> + + + {fname} + {tags && ( + + {tags.map((tag: string) => ( + 10 ? 'hidden' : 'visible', + textOverflow: 'ellipsis', + }} + key={tag} + mie={4} + > + + {tag} + + + ))} + + )} + + + {isPriorityEnabled && ( + + + + )} + {department?.name} + {servedBy?.username} + {moment(ts).format('L LTS')} + {moment(lm).format('L LTS')} + + + {getStatusText(open, onHold)} + + {canRemoveClosedChats && {!open && }} + + ); +}; + +export default ChatTableRow; diff --git a/apps/meteor/client/views/omnichannel/directory/chats/useChatsFilters.ts b/apps/meteor/client/views/omnichannel/directory/chats/useChatsFilters.ts new file mode 100644 index 000000000000..0727e08abf98 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/directory/chats/useChatsFilters.ts @@ -0,0 +1,86 @@ +import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +import { useFormatDate } from '../../../../hooks/useFormatDate'; + +export type ChatsFiltersQuery = { + guest: string; + servedBy: string; + status: string; + department: string; + from: string; + to: string; + tags: { _id: string; label: string; value: string }[]; + [key: string]: unknown; +}; + +const statusTextMap: { [key: string]: string } = { + all: 'All', + closed: 'Closed', + opened: 'Room_Status_Open', + onhold: 'On_Hold_Chats', + queued: 'Queued', +}; + +const initialValues: ChatsFiltersQuery = { + guest: '', + servedBy: 'all', + status: 'all', + department: 'all', + from: '', + to: '', + tags: [], +}; + +const useDisplayFilters = (filtersQuery: ChatsFiltersQuery) => { + const t = useTranslation(); + const formatDate = useFormatDate(); + + const { guest, servedBy, status, department, from, to, tags, ...customFields } = filtersQuery; + + const getDepartment = useEndpoint('GET', '/v1/livechat/department/:_id', { _id: department }); + const getAgent = useEndpoint('GET', '/v1/livechat/users/agent/:_id', { _id: servedBy }); + + const { data: departmentData } = useQuery(['getDepartmentDataForFilter', department], () => getDepartment({})); + const { data: agentData } = useQuery(['getAgentDataForFilter', servedBy], () => getAgent()); + + const displayCustomFields = Object.entries(customFields).reduce((acc, [key, value]) => { + acc[key] = value ? `${key}: ${value}` : undefined; + return acc; + }, {} as { [key: string]: string | undefined }); + + return { + from: from !== '' ? `${t('From')}: ${formatDate(from)}` : undefined, + to: to !== '' ? `${t('To')}: ${formatDate(to)}` : undefined, + guest: guest !== '' ? `${t('Text')}: ${guest}` : undefined, + servedBy: servedBy !== 'all' ? `${t('Served_By')}: ${agentData?.user.name}` : undefined, + department: department !== 'all' ? `${t('Department')}: ${departmentData?.department.name}` : undefined, + status: status !== 'all' ? `${t('Status')}: ${t(statusTextMap[status] as TranslationKey)}` : undefined, + tags: tags.length > 0 ? tags.map((tag) => `${t('Tag')}: ${tag.label}`) : undefined, + ...displayCustomFields, + }; +}; + +export const useChatsFilters = () => { + const [filtersQuery, setFiltersQuery] = useLocalStorage('conversationsQuery', initialValues); + const displayFilters = useDisplayFilters(filtersQuery); + + const resetFiltersQuery = () => + setFiltersQuery((prevState) => { + const customFields = Object.keys(prevState).filter((item) => !Object.keys(initialValues).includes(item)); + + const initialCustomFields = customFields.reduce((acc, cv) => { + acc[cv] = ''; + return acc; + }, {} as { [key: string]: string }); + + return { ...initialValues, ...initialCustomFields }; + }); + + const removeFilter = (filter: keyof typeof initialValues) => + setFiltersQuery((prevState) => ({ ...prevState, [filter]: initialValues[filter] })); + + return { filtersQuery, setFiltersQuery, resetFiltersQuery, displayFilters, removeFilter }; +}; diff --git a/apps/meteor/client/views/omnichannel/directory/chats/useChatsQuery.ts b/apps/meteor/client/views/omnichannel/directory/chats/useChatsQuery.ts new file mode 100644 index 000000000000..193fd6d72aaa --- /dev/null +++ b/apps/meteor/client/views/omnichannel/directory/chats/useChatsQuery.ts @@ -0,0 +1,94 @@ +import type { GETLivechatRoomsParams } from '@rocket.chat/rest-typings'; +import { usePermission, useUserId } from '@rocket.chat/ui-contexts'; +import moment from 'moment'; + +import type { ChatsFiltersQuery } from './useChatsFilters'; + +type useQueryType = ( + debouncedParams: ChatsFiltersQuery, + [column, direction]: [string, 'asc' | 'desc'], + current: number, + itemsPerPage: 25 | 50 | 100, +) => GETLivechatRoomsParams; + +type CurrentChatQuery = { + agents?: string[]; + offset?: number; + roomName?: string; + departmentId?: string; + open?: boolean; + createdAt?: string; + closedAt?: string; + tags?: string[]; + onhold?: boolean; + customFields?: string; + sort: string; + count?: number; + queued?: boolean; +}; + +const sortDir = (sortDir: 'asc' | 'desc'): 1 | -1 => (sortDir === 'asc' ? 1 : -1); + +export const useChatsQuery = () => { + const userIdLoggedIn = useUserId(); + const canViewLivechatRooms = usePermission('view-livechat-rooms'); + + const chatsQuery: useQueryType = ( + { guest, servedBy, department, status, from, to, tags, ...customFields }, + [column, direction], + current, + itemsPerPage, + ) => { + const query: CurrentChatQuery = { + ...(guest && { roomName: guest }), + sort: JSON.stringify({ + [column]: sortDir(direction), + ts: column === 'ts' ? sortDir(direction) : undefined, + }), + ...(itemsPerPage && { count: itemsPerPage }), + ...(current && { offset: current }), + }; + + if (from || to) { + query.createdAt = JSON.stringify({ + ...(from && { + start: moment(new Date(from)).set({ hour: 0, minutes: 0, seconds: 0 }).toISOString(), + }), + ...(to && { + end: moment(new Date(to)).set({ hour: 23, minutes: 59, seconds: 59 }).toISOString(), + }), + }); + } + + if (status !== 'all') { + query.open = status === 'opened' || status === 'onhold' || status === 'queued'; + query.onhold = status === 'onhold'; + query.queued = status === 'queued'; + } + + if (canViewLivechatRooms && servedBy && servedBy !== 'all') { + query.agents = [servedBy]; + } else { + query.agents = userIdLoggedIn ? [userIdLoggedIn] : []; + } + + if (department && department !== 'all') { + query.departmentId = department; + } + + if (tags && tags.length > 0) { + query.tags = tags.map((tag) => tag.value); + } + + if (customFields && Object.keys(customFields).length > 0) { + const customFieldsQuery = Object.fromEntries(Object.entries(customFields).filter((item) => item[1] !== undefined && item[1] !== '')); + if (Object.keys(customFieldsQuery).length > 0) { + query.customFields = JSON.stringify(customFieldsQuery); + } + } + + return query; + }; + + return chatsQuery; +}; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center.spec.ts index a9745288f967..7c3a15c21c38 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center.spec.ts @@ -91,6 +91,7 @@ test.describe('Omnichannel Contact Center', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); await poOmniSection.btnContactCenter.click(); + await poOmniSection.tabContacts.click(); await page.waitForURL(URL.contactCenter); }); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts index 87a34a66356e..e1f2cfb23de2 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts @@ -22,4 +22,8 @@ export class OmnichannelSection { get btnContactCenter(): Locator { return this.page.locator('role=button[name="Contact Center"]'); } + + get tabContacts(): Locator { + return this.page.locator('role=tab[name="Contacts"]'); + } } diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index cba7fbede924..16cfa0142d9a 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -1,3 +1,4 @@ +import type { ILivechatDepartment } from './ILivechatDepartment'; import type { ILivechatPriority } from './ILivechatPriority'; import type { ILivechatVisitor } from './ILivechatVisitor'; import type { IMessage, MessageTypesValues } from './IMessage'; @@ -354,6 +355,8 @@ export type IOmnichannelRoomClosingInfo = Pick): room is IOmnichannelRoom & IRoom => room.t === 'l'; export const isVoipRoom = (room: IRoom): room is IVoipRoom & IRoom => room.t === 'v'; diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 8b7fd7162934..d28e11ef3e97 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -9,6 +9,7 @@ import type { ILivechatVisitorDTO, IMessage, IOmnichannelRoom, + IOmnichannelRoomWithDepartment, IRoom, ISetting, ILivechatAgentActivity, @@ -3941,7 +3942,7 @@ export type OmnichannelEndpoints = { DELETE: () => void; }; '/v1/livechat/rooms': { - GET: (params: GETLivechatRoomsParams) => PaginatedResult<{ rooms: IOmnichannelRoom[] }>; + GET: (params: GETLivechatRoomsParams) => PaginatedResult<{ rooms: IOmnichannelRoomWithDepartment[] }>; }; '/v1/livechat/room/:rid/priority': { POST: (params: POSTLivechatRoomPriorityParams) => void;