[IMPROVE] Rewrite Omnichannel Queue Page to React(#24176)

Co-authored-by: Kevin Aleman <kevin.aleman@rocket.chat>
pull/23818/head^2
Tiago Evangelista Pinto 4 years ago committed by GitHub
parent c09cf332e5
commit 08a087e28e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      app/livechat/client/route.js
  2. 23
      app/livechat/client/views/app/livechatAutocompleteUser.html
  3. 118
      app/livechat/client/views/app/livechatAutocompleteUser.js
  4. 114
      app/livechat/client/views/app/livechatQueue.html
  5. 125
      app/livechat/client/views/app/livechatQueue.js
  6. 2
      app/livechat/client/views/regular.js
  7. 2
      client/contexts/OmnichannelContext.ts
  8. 22
      client/sidebar/sections/Omnichannel.js
  9. 10
      client/startup/routes.tsx
  10. 66
      client/views/omnichannel/queueList/QueueListFilter.tsx
  11. 48
      client/views/omnichannel/queueList/QueueListPage.tsx
  12. 54
      client/views/omnichannel/queueList/hooks/useQuery.ts
  13. 107
      client/views/omnichannel/queueList/index.tsx
  14. 21
      definition/rest/v1/omnichannel.ts

@ -59,14 +59,3 @@ AccountBox.addRoute(
livechatManagerRoutes,
load,
);
AccountBox.addRoute(
{
name: 'livechat-queue',
path: '/livechat-queue',
i18nPageTitle: 'Livechat_Queue',
pageTemplate: 'livechatQueue',
},
null,
load,
);

@ -1,23 +0,0 @@
<template name="livechatAutocompleteUser">
<div class="rc-input" id='search-{{name}}' {{disabled}}>
<label class="rc-input__label">
{{#if label}}
<div class="rc-input__title">{{_ label}}</div>
{{/if}}
<div class="rc-input__wrapper">
{{# if icon}}
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon=icon}}
</div>
{{/if}}
<div class="rc-tags{{# unless icon}} rc-tags--no-icon{{/unless}}">
{{#each item in list}} {{> tag item}} {{/each}}
<input type="text" id="{{name}}" class="rc-tags__input" placeholder="{{_ placeholder}}" name="{{name}}" autocomplete="off"
{{disabled}} />
</div>
</div>
{{#with config}} {{#if autocomplete 'isShowing'}} {{#if autocomplete 'isLoaded'}} {{> popupList data=config items=items}}
{{/if}} {{/if}} {{/with}}
</label>
</div>
</template>

@ -1,118 +0,0 @@
import { Blaze } from 'meteor/blaze';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { AutoComplete } from '../../../../meteor-autocomplete/client';
import './livechatAutocompleteUser.html';
Template.livechatAutocompleteUser.helpers({
list() {
return this.list;
},
items() {
return Template.instance().ac.filteredList();
},
config() {
const { filter } = Template.instance();
const { noMatchTemplate, templateItem, modifier } = Template.instance().data;
return {
filter: filter.get(),
template_item: templateItem,
noMatchTemplate,
modifier(text) {
return modifier(filter, text);
},
};
},
label() {
const instance = Template.instance();
return instance.showLabel && instance.label;
},
autocomplete(key) {
const instance = Template.instance();
const param = instance.ac[key];
return typeof param === 'function' ? param.apply(instance.ac) : param;
},
});
Template.livechatAutocompleteUser.events({
'input input'(e, t) {
const input = e.target;
const position = input.selectionEnd || input.selectionStart;
const { length } = input.value;
document.activeElement === input && e && /input/i.test(e.type) && (input.selectionEnd = position + input.value.length - length);
t.filter.set(input.value);
},
'click .rc-popup-list__item'(e, t) {
t.ac.onItemClick(this, e);
},
'keydown input'(e, t) {
t.ac.onKeyDown(e);
if ([8, 46].includes(e.keyCode) && e.target.value === '') {
const { deleteLastItem } = t;
return deleteLastItem && deleteLastItem();
}
},
'keyup input'(e, t) {
t.ac.onKeyUp(e);
},
'focus input'(e, t) {
t.ac.onFocus(e);
},
'blur input'(e, t) {
t.ac.onBlur(e);
},
'click .rc-tags__tag'({ target }, t) {
const { onClickTag } = t;
return onClickTag && onClickTag(Blaze.getData(target));
},
});
Template.livechatAutocompleteUser.onRendered(function () {
const { name } = this.data;
this.ac.element = this.firstNode.querySelector(`[name=${name}]`);
this.ac.$element = $(this.ac.element);
this.deleteLastItem = this.data.deleteLastItem;
});
Template.livechatAutocompleteUser.onCreated(function () {
this.filter = new ReactiveVar('');
this.selected = new ReactiveVar([]);
this.onClickTag = this.data.onClickTag;
this.showLabel = this.data.showLabel;
this.label = this.data.label;
const filter = {};
this.autorun(() => {
const { exceptions, conditions } = Template.currentData();
filter.exceptions = exceptions;
filter.conditions = conditions;
});
const { collection, endpoint, field, sort, onSelect, selector = (match) => ({ term: match }) } = this.data;
this.ac = new AutoComplete({
selector: {
anchor: '.rc-input__label',
item: '.rc-popup-list__item',
container: '.rc-popup-list__list',
},
onSelect,
position: 'fixed',
limit: 10,
inputDelay: 300,
rules: [
{
filter,
collection,
endpoint,
field,
matchAll: true,
doNotChangeWidth: false,
selector,
sort,
},
],
});
this.ac.tmplInst = this;
});

@ -1,114 +0,0 @@
<template name="livechatQueue">
{{#if hasPermission}}
<fieldset>
<form class="form-inline" method="post">
<div class="livechat-group-filters-wrapper">
<div class="livechat-group-filters-container">
<div class="livechat-queue-standard-filters">
<div class="form-group">
{{> livechatAutocompleteUser
onClickTag=onClickTagAgent
list=selectedAgents
onSelect=onSelectAgents
collection='UserAndRoom'
endpoint='users.autocomplete'
field='username'
sort='username'
label="Served_By"
placeholder="Served_By"
name="agent"
icon="at"
noMatchTemplate="userSearchEmpty"
templateItem="popupList_item_default"
modifier=agentModifier
showLabel=true
}}
</div>
<div class="form-group">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Status"}}</div>
<div class="rc-select">
<select class="rc-select__element" name="agentStatus">
<option class="rc-select__option" value="">{{_ "Online"}}</option>
<option class="rc-select__option" value="offline">{{_ "Include_Offline_Agents"}}</option>
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
</div>
<div class="form-group">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Department"}}</div>
<div class="rc-select">
<select class="rc-select__element" name="department">
<option class="rc-select__option" value="">{{_ "Select_a_department"}}</option>
{{#each departments}}
<option class="rc-select__option" value="{{_id}}">{{name}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
</div>
</div>
</div>
<div class="livechat-group-filters-buttons">
<div class="rc-button__group">
<button class="rc-button rc-button--primary">{{_ "Filter"}}</button>
</div>
</div>
</div>
</form>
</fieldset>
<div class="rc-table-content">
{{#table fixed='true' onScroll=onTableScroll onResize=onTableResize onSort=onTableSort}}
<thead>
<tr>
<th width="25%">
<div class="table-fake-th">{{_ "Served_By"}}</div>
</th>
<th width="25%">
<div class="table-fake-th">{{_ "Department"}}</div>
</th>
<th width="25%">
<div class="table-fake-th">{{_ "Total"}}</div>
</th>
<th width="25%">
<div class="table-fake-th">{{_ "Status"}}</div>
</th>
</tr>
</thead>
<tbody>
{{#each queue}}
<tr class="rc-table-tr manage row-link" data-name="{{user.username}}">
<td>
<div class="rc-table-wrapper">
<div class="rc-table-avatar">{{> avatar username=user.username}}</div>
<div class="rc-table-info">
<span class="rc-table-title">{{user.username}}</span>
</div>
</div>
</td>
<td>{{department.name}}</td>
<td>{{chats}}</td>
<td>{{user.status}}</td>
</tr>
{{/each}}
{{#if isLoading}}
<tr class="table-no-click">
<td colspan="5" class="table-loading-td">{{> loading}}</td>
</tr>
{{/if}}
</tbody>
{{/table}}
</div>
{{#if hasMore}}
<div class="rc-button__group">
<button class="rc-button rc-button--primary js-load-more">{{_ "Load_more"}}</button>
</div>
{{/if}}
{{else}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{/if}}
</template>

@ -1,125 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { settings } from '../../../../settings';
import { hasPermission } from '../../../../authorization';
import { Users } from '../../../../models';
import './livechatQueue.html';
import { APIClient } from '../../../../utils/client';
const QUEUE_COUNT = 50;
Template.livechatQueue.helpers({
departments() {
return Template.instance()
.departments.get()
.filter((department) => department.enabled === true);
},
onSelectAgents() {
return Template.instance().onSelectAgents;
},
agentModifier() {
return (filter, text = '') => {
const f = filter.get();
return `@${f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${part}</strong>`)}`;
};
},
selectedAgents() {
return Template.instance().selectedAgents.get();
},
onClickTagAgent() {
return Template.instance().onClickTagAgent;
},
queue() {
return Template.instance().queue.get();
},
isLoading() {
return Template.instance().isLoading.get();
},
hasPermission() {
const user = Users.findOne(Meteor.userId(), { fields: { statusLivechat: 1 } });
return (
hasPermission(Meteor.userId(), 'view-livechat-queue') ||
(user.statusLivechat === 'available' && settings.get('Livechat_show_queue_list_link'))
);
},
hasMore() {
const instance = Template.instance();
const queue = instance.queue.get();
return instance.total.get() > queue.length;
},
});
Template.livechatQueue.events({
'click .show-offline'(event, instance) {
const showOffline = instance.showOffline.get();
showOffline[this._id] = event.currentTarget.checked;
instance.showOffline.set(showOffline);
},
'submit form'(event, instance) {
event.preventDefault();
instance.queue.set([]);
instance.offset.set(0);
const filter = {};
$(':input', event.currentTarget).each(function () {
if (!this.name) {
return;
}
const value = $(this).val();
filter[this.name] = value;
});
const agents = instance.selectedAgents.get();
if (agents && agents.length > 0) {
filter.agent = agents[0]._id;
}
instance.filter.set(filter);
},
'click .js-load-more'(event, instance) {
instance.offset.set(instance.offset.get() + QUEUE_COUNT);
},
});
Template.livechatQueue.onCreated(async function () {
this.selectedAgents = new ReactiveVar([]);
this.departments = new ReactiveVar([]);
this.limit = new ReactiveVar(20);
this.filter = new ReactiveVar({});
this.queue = new ReactiveVar([]);
this.isLoading = new ReactiveVar(true);
this.offset = new ReactiveVar(0);
this.total = new ReactiveVar(0);
this.onSelectAgents = ({ item: agent }) => {
this.selectedAgents.set([agent]);
};
this.onClickTagAgent = ({ username }) => {
this.selectedAgents.set(this.selectedAgents.get().filter((user) => user.username !== username));
};
this.autorun(async () => {
this.isLoading.set(true);
const filter = this.filter.get();
const offset = this.offset.get();
let query = `includeOfflineAgents=${filter.agentStatus === 'offline'}`;
if (filter.agent) {
query += `&agentId=${filter.agent}`;
}
if (filter.department) {
query += `&departmentId=${filter.department}`;
}
const { queue, total } = await APIClient.v1.get(`livechat/queue?${query}&count=${QUEUE_COUNT}&offset=${offset}`);
this.total.set(total);
this.queue.set(this.queue.get().concat(queue));
this.isLoading.set(false);
});
const { departments } = await APIClient.v1.get('livechat/department?sort={"name": 1}');
this.departments.set(departments);
});

@ -1,5 +1,3 @@
import './app/livechatAutocompleteUser';
import './app/livechatQueue';
import './app/livechatReadOnly';
import './app/livechatNotSubscribed.html';
import './app/livechatRoomTagSelector';

@ -22,6 +22,4 @@ export const useOmnichannelShowQueueLink = (): boolean => useOmnichannel().showO
export const useOmnichannelRouteConfig = (): OmichannelRoutingConfig | undefined => useOmnichannel().routeConfig;
export const useOmnichannelAgentAvailable = (): boolean => useOmnichannel().agentAvailable;
export const useQueuedInquiries = (): Inquiries => useOmnichannel().inquiries;
export const useOmnichannelQueueLink = (): string => '/livechat-queue';
export const useOmnichannelDirectoryLink = (): string => '/omnichannel-directory';
export const useOmnichannelEnabled = (): boolean => useOmnichannel().enabled;

@ -4,7 +4,7 @@ import React, { memo } from 'react';
import { hasPermission } from '../../../app/authorization/client';
import { useLayout } from '../../contexts/LayoutContext';
import { useOmnichannelShowQueueLink, useOmnichannelAgentAvailable, useOmnichannelQueueLink } from '../../contexts/OmnichannelContext';
import { useOmnichannelShowQueueLink, useOmnichannelAgentAvailable } from '../../contexts/OmnichannelContext';
import { useRoute } from '../../contexts/RouterContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
@ -15,9 +15,9 @@ const OmnichannelSection = (props) => {
const t = useTranslation();
const agentAvailable = useOmnichannelAgentAvailable();
const showOmnichannelQueueLink = useOmnichannelShowQueueLink();
const queueLink = useOmnichannelQueueLink();
const { sidebar } = useLayout();
const directoryRoute = useRoute('omnichannel-directory');
const queueListRoute = useRoute('livechat-queue');
const dispatchToastMessage = useToastMessageDispatch();
const icon = {
@ -40,18 +40,28 @@ const OmnichannelSection = (props) => {
}
});
const handleDirectory = useMutableCallback(() => {
const handleRoute = useMutableCallback((route) => {
sidebar.toggle();
directoryRoute.push({});
switch (route) {
case 'directory':
directoryRoute.push({});
break;
case 'queue':
queueListRoute.push({});
break;
}
});
return (
<Sidebar.TopBar.ToolBox {...props}>
<Sidebar.TopBar.Title>{t('Omnichannel')}</Sidebar.TopBar.Title>
<Sidebar.TopBar.Actions>
{showOmnichannelQueueLink && <Sidebar.TopBar.Action icon='queue' title={t('Queue')} is='a' href={queueLink} />}
{showOmnichannelQueueLink && <Sidebar.TopBar.Action icon='queue' title={t('Queue')} onClick={() => handleRoute('queue')} />}
<Sidebar.TopBar.Action {...icon} onClick={handleStatusChange} />
{hasPermission(['view-omnichannel-contact-center']) && <Sidebar.TopBar.Action {...directoryIcon} onClick={handleDirectory} />}
{hasPermission(['view-omnichannel-contact-center']) && (
<Sidebar.TopBar.Action {...directoryIcon} onClick={() => handleRoute('directory')} />
)}
</Sidebar.TopBar.Actions>
</Sidebar.TopBar.ToolBox>
);

@ -136,6 +136,16 @@ FlowRouter.route('/omnichannel-directory/:page?/:bar?/:id?/:tab?/:context?', {
},
});
FlowRouter.route('/livechat-queue', {
name: 'livechat-queue',
action: () => {
const OmnichannelQueueList = createTemplateForComponent('QueueList', () => import('../views/omnichannel/queueList'), {
attachment: 'at-parent',
});
appLayout.renderMainLayout({ center: OmnichannelQueueList });
},
});
FlowRouter.route('/account/:group?', {
name: 'account',
action: () => {

@ -0,0 +1,66 @@
import { Box, Select, Label } from '@rocket.chat/fuselage';
import { useMutableCallback, useLocalStorage } from '@rocket.chat/fuselage-hooks';
import React, { Dispatch, FC, SetStateAction, useEffect } from 'react';
import AutoCompleteAgent from '../../../components/AutoCompleteAgent';
import AutoCompleteDepartment from '../../../components/AutoCompleteDepartment';
import { useTranslation } from '../../../contexts/TranslationContext';
type QueueListFilterPropsType = FC<{
setFilter: Dispatch<SetStateAction<any>>;
}>;
export const QueueListFilter: QueueListFilterPropsType = ({ setFilter, ...props }) => {
const t = useTranslation();
const statusOptions: [string, string][] = [
['online', t('Online')],
['offline', t('Include_Offline_Agents')],
];
const [servedBy, setServedBy] = useLocalStorage('servedBy', 'all');
const [status, setStatus] = useLocalStorage('status', 'online');
const [department, setDepartment] = useLocalStorage<{ label: string; value: string }>('department', { value: 'all', label: t('All') });
const handleServedBy = useMutableCallback((e) => setServedBy(e));
const handleStatus = useMutableCallback((e) => setStatus(e));
const handleDepartment = useMutableCallback((e) => setDepartment(e));
const onSubmit = useMutableCallback((e) => e.preventDefault());
useEffect(() => {
const filters = { status } as {
status: string;
servedBy?: string;
departmentId?: string;
};
if (servedBy !== 'all') {
filters.servedBy = servedBy;
}
if (department?.value && department.value !== 'all') {
filters.departmentId = department.value;
}
setFilter(filters);
}, [setFilter, servedBy, status, department]);
return (
<Box mb='x16' is='form' onSubmit={onSubmit} display='flex' flexDirection='column' {...props}>
<Box display='flex' flexDirection='row' flexWrap='wrap' {...props}>
<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'>
<Label mb='x4'>{t('Served_By')}</Label>
<AutoCompleteAgent haveAll value={servedBy} onChange={handleServedBy} />
</Box>
<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'>
<Label mb='x4'>{t('Status')}</Label>
<Select flexShrink={0} options={statusOptions} value={status} onChange={handleStatus} placeholder={t('Status')} />
</Box>
<Box display='flex' mie='x8' flexGrow={1} flexDirection='column'>
<Label mb='x4'>{t('Department')}</Label>
<AutoCompleteDepartment haveAll value={department} onChange={handleDepartment} label={t('All')} onlyMyDepartments />
</Box>
</Box>
</Box>
);
};

@ -0,0 +1,48 @@
import React, { Dispatch, Key, ReactElement, ReactNode, SetStateAction } from 'react';
import GenericTable from '../../../components/GenericTable';
import Page from '../../../components/Page';
import { QueueListFilter } from './QueueListFilter';
type QueueListPagePropsParamsType = {
servedBy: string;
status: string;
departmentId: string;
itemsPerPage: number;
current: number;
};
type QueueListPagePropsType = {
title: string;
header: ReactNode;
data?: {
queue: {
chats: number;
department: { _id: string; name: string };
user: { _id: string; username: string; status: string };
}[];
count: number;
offset: number;
total: number;
};
params: QueueListPagePropsParamsType;
setParams: Dispatch<SetStateAction<QueueListPagePropsParamsType>>;
renderRow: (props: { _id?: Key }) => ReactElement;
};
export const QueueListPage = ({ title, header, data, renderRow, params, setParams }: QueueListPagePropsType): ReactElement => (
<Page>
<Page.Header title={title} />
<Page.Content>
<GenericTable
header={header}
renderFilter={({ onChange, ...props }: any): ReactElement => <QueueListFilter setFilter={onChange} {...props} />}
renderRow={renderRow}
results={data?.queue}
total={data?.total}
params={params}
setParams={setParams}
/>
</Page.Content>
</Page>
);

@ -0,0 +1,54 @@
import { useMemo } from 'react';
import { ILivechatAgent } from '../../../../../definition/ILivechatAgent';
import { ILivechatDepartment } from '../../../../../definition/ILivechatDepartment';
type useQueryType = (
debouncedParams: {
servedBy: string;
status: string;
departmentId: ILivechatDepartment['_id'];
itemsPerPage: number;
current: number;
},
debouncedSort: [string, 'asc' | 'desc'],
) => {
agentId?: ILivechatAgent['_id'];
includeOfflineAgents?: boolean;
departmentId?: ILivechatAgent['_id'];
offset: number;
count: number;
sort: string;
};
const sortDir = (sortDir: string): number => (sortDir === 'asc' ? 1 : -1);
export const useQuery: useQueryType = ({ servedBy, status, departmentId, itemsPerPage, current }, [column, direction]) =>
useMemo(() => {
const query: {
agentId?: string;
includeOflineAgents?: boolean;
departmentId?: string;
sort: string;
count: number;
offset: number;
} = {
sort: JSON.stringify({
[column]: sortDir(direction),
}),
count: itemsPerPage,
offset: current,
};
if (status !== 'online') {
query.includeOflineAgents = true;
}
if (servedBy) {
query.agentId = servedBy;
}
if (departmentId) {
query.departmentId = departmentId;
}
return query;
}, [column, direction, itemsPerPage, current, status, servedBy, departmentId]);

@ -0,0 +1,107 @@
import { Box, Table } from '@rocket.chat/fuselage';
import { useDebouncedValue, useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { ReactElement, useCallback, useMemo, useState } from 'react';
import GenericTable from '../../../components/GenericTable';
import UserAvatar from '../../../components/avatar/UserAvatar';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointData } from '../../../hooks/useEndpointData';
import { QueueListPage } from './QueueListPage';
import { useQuery } from './hooks/useQuery';
const QueueList = (): ReactElement => {
const t = useTranslation();
const [sort, setSort] = useState<[string, 'asc' | 'desc']>(['servedBy', 'desc']);
const onHeaderClick = useMutableCallback((id) => {
const [sortBy, sortDirection] = sort;
if (sortBy === id) {
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']);
return;
}
setSort([id, 'asc']);
});
const mediaQuery = useMediaQuery('(min-width: 1024px)');
const header = useMemo(
() =>
[
mediaQuery && (
<GenericTable.HeaderCell
key={'servedBy'}
direction={sort[1]}
active={sort[0] === 'servedBy'}
onClick={onHeaderClick}
sort='servedBy'
>
{t('Served_By')}
</GenericTable.HeaderCell>
),
<GenericTable.HeaderCell
key={'department'}
direction={sort[1]}
active={sort[0] === 'departmend'}
onClick={onHeaderClick}
sort='department'
>
{t('Department')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'total'} direction={sort[1]} active={sort[0] === 'total'} onClick={onHeaderClick} sort='total'>
{t('Total')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'status'} direction={sort[1]} active={sort[0] === 'status'} onClick={onHeaderClick} sort='status'>
{t('Status')}
</GenericTable.HeaderCell>,
].filter(Boolean),
[mediaQuery, sort, onHeaderClick, t],
);
const renderRow = useCallback(
({ user, department, chats }) => {
const getStatusText = (): string => {
if (user.status === 'online') {
return t('Online');
}
return t('Offline');
};
return (
<Table.Row key={user._id} tabIndex={0}>
<Table.Cell withTruncatedText>
<Box display='flex' alignItems='center' mb='5px'>
<UserAvatar size={mediaQuery ? 'x28' : 'x40'} username={user.username} />
<Box display='flex' mi='x8'>
{user.username}
</Box>
</Box>
</Table.Cell>
<Table.Cell withTruncatedText>{department ? department.name : ''}</Table.Cell>
<Table.Cell withTruncatedText>{chats}</Table.Cell>
<Table.Cell withTruncatedText>{getStatusText()}</Table.Cell>
</Table.Row>
);
},
[mediaQuery, t],
);
const [params, setParams] = useState({
servedBy: '',
status: '',
departmentId: '',
itemsPerPage: 25,
current: 0,
});
const debouncedParams = useDebouncedValue(params, 500);
const debouncedSort = useDebouncedValue(sort, 500);
const query = useQuery(debouncedParams, debouncedSort);
const { value: data } = useEndpointData('livechat/queue', query);
return (
<QueueListPage title={t('Livechat_Queue')} header={header} data={data} renderRow={renderRow} params={params} setParams={setParams} />
);
};
export default QueueList;

@ -1,3 +1,4 @@
import { ILivechatAgent } from '../../ILivechatAgent';
import { ILivechatDepartment } from '../../ILivechatDepartment';
import { ILivechatDepartmentAgents } from '../../ILivechatDepartmentAgents';
import { ILivechatMonitor } from '../../ILivechatMonitor';
@ -165,4 +166,24 @@ export type OmnichannelEndpoints = {
'livechat/visitor.status': {
POST: (params: { token: string; status: string }) => { token: string; status: string };
};
'livechat/queue': {
GET: (params: {
agentId?: ILivechatAgent['_id'];
includeOfflineAgents?: boolean;
departmentId?: ILivechatAgent['_id'];
offset: number;
count: number;
sort: string;
}) => {
queue: {
chats: number;
department: { _id: string; name: string };
user: { _id: string; username: string; status: string };
}[];
count: number;
offset: number;
total: number;
};
};
};

Loading…
Cancel
Save