RocketChat/Rocket.Chat

View on GitHub
apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx

Summary

Maintainability
D
2 days
Test Coverage
import { Callout, Pagination } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import type { GETLivechatRoomsParams } from '@rocket.chat/rest-typings';
import { usePermission, useTranslation } from '@rocket.chat/ui-contexts';
import { hashQueryKey } from '@tanstack/react-query';
import moment from 'moment';
import type { ComponentProps, ReactElement } from 'react';
import React, { memo, useCallback, useMemo, useState } from 'react';

import GenericNoResults from '../../../components/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 { Page, PageHeader, PageContent } from '../../../components/Page';
import { useIsOverMacLimit } from '../../../hooks/omnichannel/useIsOverMacLimit';
import { RoomActivityIcon } from '../../../omnichannel/components/RoomActivityIcon';
import { useOmnichannelPriorities } from '../../../omnichannel/hooks/useOmnichannelPriorities';
import { PriorityIcon } from '../../../omnichannel/priorities/PriorityIcon';
import CustomFieldsList from './CustomFieldsList';
import FilterByText from './FilterByText';
import RemoveChatButton from './RemoveChatButton';
import { useAllCustomFields } from './hooks/useAllCustomFields';
import { useCurrentChats } from './hooks/useCurrentChats';

type DebouncedParams = {
    fname: string;
    guest: string;
    servedBy: string;
    department: string;
    status: string;
    from: string;
    to: string;
    tags: any[];
};

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;
};

type useQueryType = (
    debouncedParams: DebouncedParams,
    customFields: { [key: string]: string } | undefined,
    [column, direction]: [string, 'asc' | 'desc'],
    current: number,
    itemsPerPage: 25 | 50 | 100,
) => GETLivechatRoomsParams;

const sortDir = (sortDir: 'asc' | 'desc'): 1 | -1 => (sortDir === 'asc' ? 1 : -1);

const currentChatQuery: 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 (servedBy && servedBy !== 'all') {
        query.agents = [servedBy];
    }
    if (department && department !== 'all') {
        query.departmentId = department;
    }

    if (tags && tags.length > 0) {
        query.tags = tags;
    }

    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;
};

const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: string) => void }): ReactElement => {
    const isWorkspaceOverMacLimit = useIsOverMacLimit();
    const { sortBy, sortDirection, setSort } = useSort<'fname' | 'departmentId' | 'servedBy' | 'priorityWeight' | 'ts' | 'lm' | 'open'>(
        'ts',
        'desc',
    );
    const [customFields, setCustomFields] = useState<{ [key: string]: string }>();

    const t = useTranslation();

    const canRemoveClosedChats = usePermission('remove-closed-livechat-room');
    const { enabled: isPriorityEnabled } = useOmnichannelPriorities();

    const { data: allCustomFields } = useAllCustomFields();

    const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination();

    const [params, setParams] = useState({
        guest: '',
        fname: '',
        servedBy: '',
        status: 'all',
        department: '',
        from: '',
        to: '',
        tags: [] as string[],
    });

    const hasCustomFields = useMemo(
        () => !!allCustomFields?.customFields?.find((customField) => customField.scope === 'room'),
        [allCustomFields],
    );

    const query = useMemo(
        () => currentChatQuery(params, customFields, [sortBy, sortDirection], current, itemsPerPage),
        [customFields, itemsPerPage, params, sortBy, sortDirection, current],
    );

    const { data, isLoading, isSuccess } = useCurrentChats(query);

    const [defaultQuery] = useState(hashQueryKey([query]));
    const queryHasChanged = defaultQuery !== hashQueryKey([query]);

    const onFilter = useMutableCallback((params: DebouncedParams): void => {
        setParams(params);
        setCurrent(0);
    });

    const renderRow = useCallback(
        (room) => {
            const { _id, fname, servedBy, ts, lm, department, open, onHold, priorityWeight } = room;
            const getStatusText = (open: boolean, onHold: boolean, servedBy: boolean): string => {
                if (!open) return t('Closed');
                if (open && !servedBy) return t('Queued');
                return onHold ? t('On_Hold_Chats') : t('Room_Status_Open');
            };

            return (
                <GenericTableRow key={_id} onClick={(): void => onRowClick(_id)} action data-qa-id={fname}>
                    {isPriorityEnabled && (
                        <GenericTableCell withTruncatedText data-qa='current-chats-cell-priority'>
                            <PriorityIcon level={priorityWeight} />
                        </GenericTableCell>
                    )}
                    <GenericTableCell withTruncatedText data-qa='current-chats-cell-name'>
                        {fname}
                    </GenericTableCell>
                    <GenericTableCell withTruncatedText data-qa='current-chats-cell-department'>
                        {department ? department.name : ''}
                    </GenericTableCell>
                    <GenericTableCell withTruncatedText data-qa='current-chats-cell-servedBy'>
                        {servedBy?.username}
                    </GenericTableCell>
                    <GenericTableCell withTruncatedText data-qa='current-chats-cell-startedAt'>
                        {moment(ts).format('L LTS')}
                    </GenericTableCell>
                    <GenericTableCell withTruncatedText data-qa='current-chats-cell-lastMessage'>
                        {moment(lm).format('L LTS')}
                    </GenericTableCell>
                    <GenericTableCell withTruncatedText data-qa='current-chats-cell-status'>
                        <RoomActivityIcon room={room} /> {getStatusText(open, onHold, !!servedBy?.username)}
                    </GenericTableCell>
                    {canRemoveClosedChats && !open && <RemoveChatButton _id={_id} />}
                </GenericTableRow>
            );
        },
        [canRemoveClosedChats, onRowClick, isPriorityEnabled, t],
    );

    const headers = (
        <>
            {isPriorityEnabled && (
                <GenericTableHeaderCell
                    key='priorityWeight'
                    direction={sortDirection}
                    active={sortBy === 'priorityWeight'}
                    onClick={setSort}
                    sort='priorityWeight'
                    w='x100'
                    alignItems='center'
                >
                    {t('Priority')}
                </GenericTableHeaderCell>
            )}
            <GenericTableHeaderCell
                key='fname'
                direction={sortDirection}
                active={sortBy === 'fname'}
                onClick={setSort}
                sort='fname'
                data-qa='current-chats-header-name'
            >
                {t('Name')}
            </GenericTableHeaderCell>
            <GenericTableHeaderCell
                key='departmentId'
                direction={sortDirection}
                active={sortBy === 'departmentId'}
                onClick={setSort}
                sort='departmentId'
                data-qa='current-chats-header-department'
            >
                {t('Department')}
            </GenericTableHeaderCell>
            <GenericTableHeaderCell
                key='servedBy'
                direction={sortDirection}
                active={sortBy === 'servedBy'}
                onClick={setSort}
                sort='servedBy'
                data-qa='current-chats-header-servedBy'
            >
                {t('Served_By')}
            </GenericTableHeaderCell>
            <GenericTableHeaderCell
                key='ts'
                direction={sortDirection}
                active={sortBy === 'ts'}
                onClick={setSort}
                sort='ts'
                data-qa='current-chats-header-startedAt'
            >
                {t('Started_At')}
            </GenericTableHeaderCell>
            <GenericTableHeaderCell
                key='lm'
                direction={sortDirection}
                active={sortBy === 'lm'}
                onClick={setSort}
                sort='lm'
                data-qa='current-chats-header-lastMessage'
            >
                {t('Last_Message')}
            </GenericTableHeaderCell>
            <GenericTableHeaderCell
                key='open'
                direction={sortDirection}
                active={sortBy === 'open'}
                onClick={setSort}
                sort='open'
                w='x100'
                data-qa='current-chats-header-status'
            >
                {t('Status')}
            </GenericTableHeaderCell>
            {canRemoveClosedChats && (
                <GenericTableHeaderCell key='remove' w='x60' data-qa='current-chats-header-remove'>
                    {t('Remove')}
                </GenericTableHeaderCell>
            )}
        </>
    );

    // TODO: Missing error state
    return (
        <Page flexDirection='row'>
            <Page>
                <PageHeader title={t('Current_Chats')} />
                <PageContent>
                    {((isSuccess && data?.rooms.length > 0) || queryHasChanged) && (
                        <FilterByText
                            setFilter={onFilter as ComponentProps<typeof FilterByText>['setFilter']}
                            setCustomFields={setCustomFields}
                            customFields={customFields}
                            hasCustomFields={hasCustomFields}
                        />
                    )}
                    {isWorkspaceOverMacLimit && (
                        <Callout
                            type='danger'
                            icon='warning'
                            title={t('The_workspace_has_exceeded_the_monthly_limit_of_active_contacts')}
                            style={{ marginBlock: '2rem' }}
                        >
                            {t('Talk_to_your_workspace_admin_to_address_this_issue')}
                        </Callout>
                    )}
                    {isSuccess && data?.rooms.length === 0 && queryHasChanged && <GenericNoResults />}
                    {isSuccess && data?.rooms.length === 0 && !queryHasChanged && (
                        <GenericNoResults
                            icon='discussion'
                            title={t('No_chats_yet')}
                            description={t('No_chats_yet_description')}
                            linkHref='https://go.rocket.chat/i/omnichannel-docs'
                            linkText={t('Learn_more_about_current_chats')}
                        />
                    )}
                    {isLoading && (
                        <GenericTable>
                            <GenericTableHeader>{headers}</GenericTableHeader>
                            <GenericTableBody>
                                <GenericTableLoadingTable headerCells={6} />
                            </GenericTableBody>
                        </GenericTable>
                    )}
                    {isSuccess && data?.rooms.length > 0 && (
                        <>
                            <GenericTable>
                                <GenericTableHeader>{headers}</GenericTableHeader>
                                <GenericTableBody data-qa='GenericTableCurrentChatsBody'>
                                    {data.rooms.map((room) => renderRow({ ...room }))}
                                </GenericTableBody>
                            </GenericTable>
                            <Pagination
                                divider
                                current={current}
                                itemsPerPage={itemsPerPage}
                                count={data.total}
                                onSetItemsPerPage={setItemsPerPage}
                                onSetCurrent={setCurrent}
                                {...paginationProps}
                            />
                        </>
                    )}
                </PageContent>
            </Page>
            {id === 'custom-fields' && hasCustomFields && (
                <CustomFieldsList setCustomFields={setCustomFields} allCustomFields={allCustomFields?.customFields || []} />
            )}
        </Page>
    );
};

export default memo(CurrentChatsPage);