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')}
+ }
+ />
+
+
+ {t('Department')}
+
+ (
+
+ )}
+ />
+
+
+
+ {t('Tags')}
+
+ }
+ />
+
+
+ {canViewCustomFields &&
+ contactCustomFields?.map((customField) => {
+ if (customField.type === 'select') {
+ return (
+
+ {customField.label}
+
+ (
+
+
+ );
+ }
+
+ 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;