+
+
+ ) : noQueues ? (
) : (
@@ -223,9 +262,9 @@ const ConsultTransferPopoverComponent: React.FC
{loadingQueues ? (
-
- {LOADING_MORE_QUEUES}
-
+
+
+
) : (
{SCROLL_TO_LOAD_MORE}
@@ -238,7 +277,11 @@ const ConsultTransferPopoverComponent: React.FC
+
+
+ ) : noDialNumbers ? (
) : (
@@ -253,9 +296,9 @@ const ConsultTransferPopoverComponent: React.FC
{loadingDialNumbers ? (
-
- {LOADING_MORE_DIAL_NUMBERS}
-
+
+
+
) : (
{SCROLL_TO_LOAD_MORE}
@@ -268,7 +311,11 @@ const ConsultTransferPopoverComponent: React.FC
+
+
+ ) : noEntryPoints ? (
) : (
@@ -281,9 +328,9 @@ const ConsultTransferPopoverComponent: React.FC
{loadingEntryPoints ? (
-
- {LOADING_MORE_ENTRY_POINTS}
-
+
+
+
) : (
{SCROLL_TO_LOAD_MORE}
diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss
index 1b1df41b4..58b02d01f 100644
--- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss
+++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss
@@ -230,10 +230,24 @@
min-width: 0;
}
+ .consult-action-buttons {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex-shrink: 0;
+ }
+
+ .consult-reload-button,
.consult-quick-action-button {
flex: 0 0 2rem; /* 32px */
}
+ .consult-reload-button {
+ mdc-icon {
+ --mdc-icon-fill-color: var(--mds-color-theme-text-primary-normal);
+ }
+ }
+
.consult-category-buttons {
margin: 0.5rem 0;
display: flex;
@@ -271,6 +285,15 @@
align-items: center;
}
+ .consult-loading-spinner {
+ // Centering the spinner in the loading area
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 10rem;
+ width: 100%;
+ }
+
.consult-list-container {
flex: 1;
overflow-y: auto;
diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx
index 9eb5c8f0d..7dfb1b137 100644
--- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx
+++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx
@@ -45,6 +45,7 @@ function CallControlComponent(props: CallControlComponentProps) {
isRecording,
setIsRecording,
buddyAgents,
+ loadingBuddyAgents,
loadBuddyAgents,
transferCall,
consultCall,
@@ -221,6 +222,8 @@ function CallControlComponent(props: CallControlComponentProps) {
heading={button.menuType}
buttonIcon={button.icon}
buddyAgents={buddyAgents}
+ loadingBuddyAgents={loadingBuddyAgents}
+ loadBuddyAgents={loadBuddyAgents}
getAddressBookEntries={getAddressBookEntries}
getEntryPoints={getEntryPoints}
getQueues={getQueuesFetcher}
diff --git a/packages/contact-center/cc-components/src/components/task/constants.ts b/packages/contact-center/cc-components/src/components/task/constants.ts
index 633dbff01..8308c8b9b 100644
--- a/packages/contact-center/cc-components/src/components/task/constants.ts
+++ b/packages/contact-center/cc-components/src/components/task/constants.ts
@@ -22,9 +22,6 @@ export const QUEUES = 'Queues';
export const SEARCH_PLACEHOLDER = 'Search...';
export const CLEAR_SEARCH = 'Clear search';
export const SCROLL_TO_LOAD_MORE = 'Scroll to load more';
-export const LOADING_MORE_QUEUES = 'Loading more queues...';
-export const LOADING_MORE_DIAL_NUMBERS = 'Loading more dial numbers...';
-export const LOADING_MORE_ENTRY_POINTS = 'Loading more entry points...';
export const NO_DATA_AVAILABLE_CONSULT_TRANSFER = 'No data available for consult transfer.';
export const VIA_SEARCH_SUFFIX = ' via search';
// Pagination
diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts
index 7a0a2c65c..aac63d7ca 100644
--- a/packages/contact-center/cc-components/src/components/task/task.types.ts
+++ b/packages/contact-center/cc-components/src/components/task/task.types.ts
@@ -282,6 +282,11 @@ export interface ControlProps {
*/
buddyAgents: BuddyDetails[];
+ /**
+ * Flag to indicate if buddy agents are being loaded
+ */
+ loadingBuddyAgents: boolean;
+
/**
* Function to load buddy agents
*/
@@ -480,6 +485,7 @@ export type CallControlComponentProps = Pick<
| 'isRecording'
| 'setIsRecording'
| 'buddyAgents'
+ | 'loadingBuddyAgents'
| 'loadBuddyAgents'
| 'transferCall'
| 'consultCall'
@@ -612,6 +618,8 @@ export interface ConsultTransferPopoverComponentProps {
heading: string;
buttonIcon: string;
buddyAgents: BuddyDetails[];
+ loadingBuddyAgents: boolean;
+ loadBuddyAgents?: () => Promise;
getAddressBookEntries?: FetchPaginatedList;
getEntryPoints?: FetchPaginatedList;
getQueues?: FetchPaginatedList;
diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap
index f661e7b40..b48bb3cba 100644
--- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap
+++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/consult-transfer-popover.snapshot.tsx.snap
@@ -36,6 +36,41 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co
style="clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute; width: 1px; white-space: nowrap;"
/>
+
+
+
+
+
+
+
+
+
+
@@ -2099,6 +2439,41 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem
style="clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute; width: 1px; white-space: nowrap;"
/>
+
+
+
+
+
+
{
onDialNumberSelect: jest.fn(),
onEntryPointSelect: jest.fn(),
allowConsultToQueue: true,
+ loadingBuddyAgents: false,
logger: mockLogger,
};
diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx
index eb6491f86..375b76089 100644
--- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx
+++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import {render, fireEvent, waitFor, act} from '@testing-library/react';
import '@testing-library/jest-dom';
import ConsultTransferPopoverComponent from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-popover';
-import {ContactServiceQueue} from '@webex/cc-store';
+import {ContactServiceQueue, EntryPointRecord, AddressBookEntry} from '@webex/cc-store';
import {DEFAULT_PAGE_SIZE} from '../../../../../src/components/task/constants';
const loggerMock = {
@@ -60,6 +60,7 @@ describe('ConsultTransferPopoverComponent', () => {
onDialNumberSelect: jest.fn(),
onEntryPointSelect: jest.fn(),
allowConsultToQueue: true,
+ loadingBuddyAgents: false,
logger: loggerMock,
};
@@ -307,4 +308,256 @@ describe('ConsultTransferPopoverComponent', () => {
expect(getQueuesMock).not.toHaveBeenCalled();
});
});
+
+ describe('Reload button functionality', () => {
+ it('renders reload button with correct attributes', async () => {
+ const screen = await render();
+
+ const reloadButton = screen.getByTestId('consult-reload-button');
+ expect(reloadButton).toBeInTheDocument();
+ expect(reloadButton).toHaveAttribute('aria-label', 'Reload Agents');
+
+ // Check icon is present
+ const icon = reloadButton.querySelector('mdc-icon[name="refresh-bold"]');
+ expect(icon).toBeInTheDocument();
+ });
+
+ it('calls loadBuddyAgents when reload button clicked on Agents tab', async () => {
+ const mockLoadBuddyAgents = jest.fn().mockResolvedValue(undefined);
+ const screen = await render(
+
+ );
+
+ // Default tab is Agents
+ const reloadButton = screen.getByTestId('consult-reload-button');
+ fireEvent.click(reloadButton);
+
+ expect(mockLoadBuddyAgents).toHaveBeenCalledTimes(1);
+ });
+
+ it('reloads queues when reload button clicked on Queues tab', async () => {
+ const getQueuesMock = jest.fn().mockResolvedValue({
+ data: [
+ {id: 'queue1', name: 'Queue One'} as ContactServiceQueue,
+ {id: 'queue2', name: 'Queue Two'} as ContactServiceQueue,
+ ],
+ meta: {page: 0, totalPages: 1},
+ });
+
+ const screen = await render();
+
+ // Switch to Queues tab
+ fireEvent.click(screen.getByText('Queues'));
+
+ await waitFor(() => {
+ expect(getQueuesMock).toHaveBeenCalledTimes(1);
+ });
+
+ // Click reload button
+ const reloadButton = screen.getByTestId('consult-reload-button');
+ fireEvent.click(reloadButton);
+
+ await waitFor(() => {
+ expect(getQueuesMock).toHaveBeenCalledTimes(2);
+ expect(loggerMock.info).toHaveBeenCalledWith('CC-Components: Reloading Queues data', {
+ module: 'cc-components#consult-transfer-popover-hooks.ts',
+ method: 'useConsultTransferPopover#handleReload',
+ });
+ });
+ });
+
+ it('disables reload button when loadingBuddyAgents is true', async () => {
+ const screen = await render();
+
+ const reloadButton = screen.getByTestId('consult-reload-button');
+ expect(reloadButton).toBeDisabled();
+ });
+
+ it('updates aria-label when switching tabs', async () => {
+ // Add getEntryPoints and getAddressBookEntries to enable all tabs
+ // Note: Entry Point tab only shows when heading is 'Consult'
+ const propsWithAllTabs = {
+ ...baseProps,
+ heading: 'Consult', // Required for Entry Point tab to be visible
+ getAddressBookEntries: async () => ({
+ data: [
+ {
+ id: 'dn1',
+ name: 'Dial Number One',
+ number: '12345',
+ type: 'DN',
+ } as AddressBookEntry,
+ ],
+ meta: {page: 0, totalPages: 1},
+ }),
+ getEntryPoints: async () => ({
+ data: [
+ {
+ id: 'ep1',
+ name: 'Entry Point One',
+ type: 'EP',
+ isActive: true,
+ orgId: 'org1',
+ } as EntryPointRecord,
+ ],
+ meta: {page: 0, totalPages: 1},
+ }),
+ };
+
+ const screen = await render();
+
+ // Default is Agents
+ let reloadButton = screen.getByTestId('consult-reload-button');
+ expect(reloadButton).toHaveAttribute('aria-label', 'Reload Agents');
+
+ // Switch to Queues
+ fireEvent.click(screen.getByText('Queues'));
+ reloadButton = screen.getByTestId('consult-reload-button');
+ expect(reloadButton).toHaveAttribute('aria-label', 'Reload Queues');
+
+ // Switch to Dial Number
+ fireEvent.click(screen.getByText('Dial Number'));
+ reloadButton = screen.getByTestId('consult-reload-button');
+ expect(reloadButton).toHaveAttribute('aria-label', 'Reload Dial Number');
+
+ // Switch to Entry Point
+ fireEvent.click(screen.getByText('Entry Point'));
+ reloadButton = screen.getByTestId('consult-reload-button');
+ expect(reloadButton).toHaveAttribute('aria-label', 'Reload Entry Point');
+ });
+ });
+
+ describe('Loading states', () => {
+ it('shows spinner when loadingBuddyAgents is true and no agents', async () => {
+ const screen = await render(
+
+ );
+
+ const spinner = screen.container.querySelector('.consult-loading-spinner mdc-spinner');
+ expect(spinner).toBeInTheDocument();
+ });
+
+ it('shows agents list when loadingBuddyAgents is false', async () => {
+ const screen = await render();
+
+ const agentList = screen.container.querySelector('.agent-list');
+ expect(agentList).toBeInTheDocument();
+
+ const listItems = screen.container.querySelectorAll('.call-control-list-item');
+ expect(listItems).toHaveLength(2);
+ });
+
+ it('shows spinner for queues when loading and no data', async () => {
+ const getQueuesMock = jest.fn().mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ setTimeout(() => resolve({data: [], meta: {page: 0, totalPages: 0}}), 100);
+ })
+ );
+
+ const screen = await render();
+
+ // Switch to Queues tab
+ fireEvent.click(screen.getByText('Queues'));
+
+ // Should show spinner while loading
+ await waitFor(() => {
+ const spinner = screen.container.querySelector('.consult-loading-spinner mdc-spinner');
+ expect(spinner).toBeInTheDocument();
+ });
+ });
+
+ it('shows spinner in load more area when loading more queues', async () => {
+ const getQueuesMock = jest.fn().mockResolvedValue({
+ data: [
+ {id: 'queue1', name: 'Queue One'} as ContactServiceQueue,
+ {id: 'queue2', name: 'Queue Two'} as ContactServiceQueue,
+ ],
+ meta: {page: 0, totalPages: 2},
+ });
+
+ const screen = await render();
+
+ // Switch to Queues tab
+ fireEvent.click(screen.getByText('Queues'));
+
+ await waitFor(() => {
+ const listItems = screen.container.querySelectorAll('.call-control-list-item');
+ expect(listItems.length).toBeGreaterThan(0);
+ });
+
+ // Should have a load more area
+ const loadMoreArea = screen.container.querySelector('.consult-load-more');
+ expect(loadMoreArea).toBeInTheDocument();
+ });
+
+ it('shows empty state instead of spinner when not loading', async () => {
+ const screen = await render(
+
+ );
+
+ const spinner = screen.container.querySelector('.consult-loading-spinner mdc-spinner');
+ expect(spinner).not.toBeInTheDocument();
+
+ const emptyState = screen.container.querySelector('.consult-empty-state');
+ expect(emptyState).toBeInTheDocument();
+ });
+ });
+
+ describe('Reload with search query', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('reloads with current search query on Queues tab', async () => {
+ const getQueuesMock = jest.fn().mockResolvedValue({
+ data: [{id: 'queue1', name: 'Queue One'} as ContactServiceQueue],
+ meta: {page: 0, totalPages: 1},
+ });
+
+ const screen = await render();
+
+ // Switch to Queues tab
+ fireEvent.click(screen.getByText('Queues'));
+
+ await waitFor(() => {
+ expect(getQueuesMock).toHaveBeenCalledTimes(1);
+ });
+
+ // Enter search query
+ const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
+ fireEvent.change(input, {target: {value: 'test query'}});
+
+ await act(async () => {
+ jest.advanceTimersByTime(500);
+ });
+
+ // Should have called with search query
+ expect(getQueuesMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ page: 0,
+ pageSize: DEFAULT_PAGE_SIZE,
+ search: 'test query',
+ })
+ );
+
+ // Click reload - should reload with the same search query
+ const reloadButton = screen.getByTestId('consult-reload-button');
+ fireEvent.click(reloadButton);
+
+ await waitFor(() => {
+ expect(getQueuesMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ page: 0,
+ pageSize: DEFAULT_PAGE_SIZE,
+ search: 'test query',
+ })
+ );
+ });
+ });
+ });
});
diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx
index 949b6e22b..78a5324ec 100644
--- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx
+++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx
@@ -83,6 +83,7 @@ describe('CallControlComponent Snapshots', () => {
setIsRecording: jest.fn(),
buddyAgents: mockBuddyAgents,
loadBuddyAgents: jest.fn(),
+ loadingBuddyAgents: false,
transferCall: jest.fn(),
consultCall: jest.fn(),
endConsultCall: jest.fn(),
diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx
index 7037ec59f..ae6ca7221 100644
--- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx
+++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx
@@ -105,6 +105,7 @@ describe('CallControlComponent', () => {
setIsRecording: jest.fn(),
buddyAgents: mockBuddyAgents,
loadBuddyAgents: jest.fn(),
+ loadingBuddyAgents: false,
transferCall: jest.fn(),
consultCall: jest.fn(),
endConsultCall: jest.fn(),
diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx
index 0b4277f88..77197a228 100644
--- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx
+++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx
@@ -115,6 +115,7 @@ describe('CallControlCADComponent Snapshots', () => {
setIsRecording: jest.fn(),
buddyAgents: mockBuddyAgents,
loadBuddyAgents: jest.fn(),
+ loadingBuddyAgents: false,
transferCall: jest.fn(),
consultCall: jest.fn(),
endConsultCall: jest.fn(),
diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx
index 95af309f8..693209a74 100644
--- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx
+++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx
@@ -125,6 +125,7 @@ describe('CallControlCADComponent', () => {
setIsRecording: jest.fn(),
buddyAgents: mockBuddyAgents,
loadBuddyAgents: jest.fn(),
+ loadingBuddyAgents: false,
transferCall: jest.fn(),
consultCall: jest.fn(),
endConsultCall: jest.fn(),
diff --git a/packages/contact-center/task/src/Utils/task-util.ts b/packages/contact-center/task/src/Utils/task-util.ts
index fe87c4eff..430130ab4 100644
--- a/packages/contact-center/task/src/Utils/task-util.ts
+++ b/packages/contact-center/task/src/Utils/task-util.ts
@@ -41,6 +41,22 @@ function isTelephonySupported(deviceType: string, webRtcEnabled: boolean): boole
return (isBrowser && webRtcEnabled) || isAgentDN || isExtension;
}
+/**
+ * Check if consulting with an EP_DN agent (matches Agent Desktop's isEPorEPDN)
+ */
+function isConsultingWithEpDnAgent(task: ITask): boolean {
+ if (!task?.data?.interaction) {
+ return false;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const destAgentType = (task.data.interaction as any).destAgentType;
+
+ return (
+ destAgentType === 'EpDn' || destAgentType === 'EPDN' || destAgentType === 'EntryPoint' || destAgentType === 'EP'
+ );
+}
+
export function findHoldTimestamp(interaction: Interaction, mType = 'mainCall'): number | null {
if (interaction?.media) {
const media = Object.values(interaction.media).find((m) => m.mType === mType);
@@ -77,7 +93,7 @@ export function getDeclineButtonVisibility(isBrowser: boolean, webRtcEnabled: bo
}
/**
- * Get visibility for End button
+ * Get visibility for End button (matches Agent Desktop behavior)
*/
export function getEndButtonVisibility(
isBrowser: boolean,
@@ -87,10 +103,30 @@ export function getEndButtonVisibility(
isConferenceInProgress: boolean,
isConsultCompleted: boolean,
isHeld: boolean,
- consultCallHeld: boolean
+ consultCallHeld: boolean,
+ task?: ITask,
+ agentId?: string
): Visibility {
const isVisible = isBrowser || (isEndCallEnabled && isCall) || !isCall;
- // Disable if: held (except when in conference and consult not completed) OR consult in progress (unless consult call is held - meaning we're back on main)
+ const isEpDnConsult = task && agentId ? isConsultingWithEpDnAgent(task) : false;
+
+ // EP_DN consult: End button enabled unless main call is held
+ if (isEpDnConsult && isConsultInitiatedOrAcceptedOrBeingConsulted) {
+ const isEnabled = !isHeld || (isConferenceInProgress && !isConsultCompleted);
+ return {isVisible, isEnabled};
+ }
+
+ // Agent-to-agent consult during conference: End button enabled when switched back to main call
+ if (isConsultInitiatedOrAcceptedOrBeingConsulted && isConferenceInProgress) {
+ return {isVisible, isEnabled: consultCallHeld};
+ }
+
+ // Regular consult without conference: End button enabled only when on main call
+ if (isConsultInitiatedOrAcceptedOrBeingConsulted && !isConferenceInProgress) {
+ return {isVisible, isEnabled: consultCallHeld};
+ }
+
+ // Default logic for other states
const isEnabled =
(!isHeld || (isConferenceInProgress && !isConsultCompleted)) &&
(!isConsultInitiatedOrAcceptedOrBeingConsulted || consultCallHeld);
@@ -444,7 +480,9 @@ export function getControlsVisibility(
isConferenceInProgress,
isConsultCompleted,
isHeld,
- consultCallHeld
+ consultCallHeld,
+ task,
+ agentId
),
muteUnmute: getMuteUnmuteButtonVisibility(isBrowser, webRtcEnabled, isCall, isBeingConsulted),
holdResume: getHoldResumeButtonVisibility(
diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts
index ac1144099..c3e2b6129 100644
--- a/packages/contact-center/task/src/helper.ts
+++ b/packages/contact-center/task/src/helper.ts
@@ -298,6 +298,7 @@ export const useCallControl = (props: useCallControlProps) => {
} = props;
const [isRecording, setIsRecording] = useState(true);
const [buddyAgents, setBuddyAgents] = useState([]);
+ const [loadingBuddyAgents, setLoadingBuddyAgents] = useState(false);
const [consultAgentName, setConsultAgentName] = useState('Consult Agent');
const [startTimestamp, setStartTimestamp] = useState(0);
const [secondsUntilAutoWrapup, setsecondsUntilAutoWrapup] = useState(null);
@@ -466,6 +467,7 @@ export const useCallControl = (props: useCallControlProps) => {
const loadBuddyAgents = useCallback(async () => {
try {
+ setLoadingBuddyAgents(true);
const agents = await store.getBuddyAgents();
logger.info(`Loaded ${agents.length} buddy agents`, {module: 'helper.ts', method: 'loadBuddyAgents'});
setBuddyAgents(agents);
@@ -475,6 +477,8 @@ export const useCallControl = (props: useCallControlProps) => {
method: 'loadBuddyAgents',
});
setBuddyAgents([]);
+ } finally {
+ setLoadingBuddyAgents(false);
}
}, [logger]);
@@ -990,6 +994,7 @@ export const useCallControl = (props: useCallControlProps) => {
isRecording,
setIsRecording,
buddyAgents,
+ loadingBuddyAgents,
loadBuddyAgents,
transferCall,
consultCall,
diff --git a/packages/contact-center/task/tests/CallControl/index.tsx b/packages/contact-center/task/tests/CallControl/index.tsx
index aee5e3723..5ec67c9fe 100644
--- a/packages/contact-center/task/tests/CallControl/index.tsx
+++ b/packages/contact-center/task/tests/CallControl/index.tsx
@@ -39,6 +39,7 @@ describe('CallControl Component', () => {
setIsRecording: jest.fn(),
buddyAgents: [],
loadBuddyAgents: jest.fn(),
+ loadingBuddyAgents: false,
transferCall: jest.fn(),
consultCall: jest.fn(),
endConsultCall: jest.fn(),
diff --git a/packages/contact-center/task/tests/CallControlCAD/index.tsx b/packages/contact-center/task/tests/CallControlCAD/index.tsx
index 60ae4f7df..1bd6758a5 100644
--- a/packages/contact-center/task/tests/CallControlCAD/index.tsx
+++ b/packages/contact-center/task/tests/CallControlCAD/index.tsx
@@ -35,6 +35,7 @@ describe('CallControlCAD Component', () => {
setIsRecording: jest.fn(),
buddyAgents: [],
loadBuddyAgents: jest.fn(),
+ loadingBuddyAgents: false,
transferCall: jest.fn(),
consultCall: jest.fn(),
endConsultCall: jest.fn(),
@@ -132,6 +133,7 @@ describe('CallControlCAD Component', () => {
setIsRecording: jest.fn(),
buddyAgents: [],
loadBuddyAgents: jest.fn(),
+ loadingBuddyAgents: false,
transferCall: jest.fn(),
consultCall: jest.fn(),
endConsultCall: jest.fn(),
@@ -210,6 +212,7 @@ describe('CallControlCAD Component', () => {
setIsRecording: jest.fn(),
buddyAgents: [],
loadBuddyAgents: jest.fn(),
+ loadingBuddyAgents: false,
transferCall: jest.fn(),
consultCall: jest.fn(),
endConsultCall: jest.fn(),
@@ -290,6 +293,7 @@ describe('CallControlCAD Component', () => {
setIsRecording: jest.fn(),
buddyAgents: [],
loadBuddyAgents: jest.fn(),
+ loadingBuddyAgents: false,
transferCall: jest.fn(),
consultCall: jest.fn(),
endConsultCall: jest.fn(),
diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts
index 0087026ee..a0c63d574 100644
--- a/packages/contact-center/task/tests/helper.ts
+++ b/packages/contact-center/task/tests/helper.ts
@@ -1361,6 +1361,112 @@ describe('useCallControl', () => {
getBuddyAgentsSpy.mockRestore();
});
+ it('should set loadingBuddyAgents to false initially', () => {
+ const {result} = renderHook(() =>
+ useCallControl({
+ currentTask: mockCurrentTask,
+ onHoldResume: mockOnHoldResume,
+ onEnd: mockOnEnd,
+ onWrapUp: mockOnWrapUp,
+ logger: mockLogger,
+ featureFlags: store.featureFlags,
+ deviceType: store.deviceType,
+ isMuted: false,
+ conferenceEnabled: true,
+ agentId: 'test-agent-id',
+ })
+ );
+ expect(result.current.loadingBuddyAgents).toBe(false);
+ });
+
+ it('should set loadingBuddyAgents to true during loading and false after success', async () => {
+ // Create a promise that we can control
+ let resolveGetBuddyAgents: (value: typeof mockAgents) => void;
+ const getBuddyAgentsPromise = new Promise((resolve) => {
+ resolveGetBuddyAgents = resolve;
+ });
+
+ const getBuddyAgentsSpy = jest.spyOn(store, 'getBuddyAgents').mockReturnValue(getBuddyAgentsPromise);
+
+ const {result} = renderHook(() =>
+ useCallControl({
+ currentTask: mockCurrentTask,
+ onHoldResume: mockOnHoldResume,
+ onEnd: mockOnEnd,
+ onWrapUp: mockOnWrapUp,
+ logger: mockLogger,
+ featureFlags: store.featureFlags,
+ deviceType: store.deviceType,
+ isMuted: false,
+ conferenceEnabled: true,
+ agentId: 'test-agent-id',
+ })
+ );
+
+ // Initially false
+ expect(result.current.loadingBuddyAgents).toBe(false);
+
+ // Start loading
+ let loadPromise: Promise;
+ act(() => {
+ loadPromise = result.current.loadBuddyAgents();
+ });
+
+ // Should be true while loading
+ await waitFor(() => {
+ expect(result.current.loadingBuddyAgents).toBe(true);
+ });
+
+ // Resolve the promise
+ act(() => {
+ resolveGetBuddyAgents!(mockAgents);
+ });
+
+ // Wait for the load to complete
+ await act(async () => {
+ await loadPromise!;
+ });
+
+ // Should be false after loading completes
+ expect(result.current.loadingBuddyAgents).toBe(false);
+ expect(result.current.buddyAgents).toEqual(mockAgents);
+
+ getBuddyAgentsSpy.mockRestore();
+ });
+
+ it('should set loadingBuddyAgents to false after error', async () => {
+ const getBuddyAgentsSpy = jest.spyOn(store, 'getBuddyAgents').mockRejectedValue(new Error('Load failed'));
+
+ const {result} = renderHook(() =>
+ useCallControl({
+ currentTask: mockCurrentTask,
+ onHoldResume: mockOnHoldResume,
+ onEnd: mockOnEnd,
+ onWrapUp: mockOnWrapUp,
+ logger: mockLogger,
+ featureFlags: store.featureFlags,
+ deviceType: store.deviceType,
+ isMuted: false,
+ conferenceEnabled: true,
+ agentId: 'test-agent-id',
+ })
+ );
+
+ // Initially false
+ expect(result.current.loadingBuddyAgents).toBe(false);
+
+ // Load and handle error
+ await act(async () => {
+ await result.current.loadBuddyAgents();
+ });
+
+ // Should be false after error
+ expect(result.current.loadingBuddyAgents).toBe(false);
+ expect(result.current.buddyAgents).toEqual([]);
+
+ getBuddyAgentsSpy.mockRestore();
+ });
+
it('should handle rejection when transferring call', async () => {
const transferError = new Error('Transfer failed');
const transferSpy = jest.fn().mockRejectedValue(transferError);
diff --git a/packages/contact-center/task/tests/utils/task-util.ts b/packages/contact-center/task/tests/utils/task-util.ts
index 67a8f01b3..2a1935b54 100644
--- a/packages/contact-center/task/tests/utils/task-util.ts
+++ b/packages/contact-center/task/tests/utils/task-util.ts
@@ -628,6 +628,299 @@ describe('getControlsVisibility', () => {
});
});
+describe('getEndButtonVisibility - EP_DN consult scenarios', () => {
+ const deviceType = 'BROWSER';
+ const featureFlags = {
+ isEndCallEnabled: true,
+ isEndConsultEnabled: true,
+ webRtcEnabled: true,
+ };
+
+ it('should enable end button during EP_DN consult when main call is active (not held)', () => {
+ // Mock a task with EP_DN consult - switching back to main call (consult on hold)
+ const task = {
+ ...mockTask,
+ data: {
+ ...mockTask.data,
+ consultMediaResourceId: 'consult',
+ interaction: {
+ ...mockTask.data.interaction,
+ mediaType: 'telephony',
+ destAgentType: 'EpDn',
+ state: 'consulting',
+ media: {
+ main: {
+ mediaResourceId: 'main',
+ mType: 'mainCall',
+ isHold: false, // Main call is active - switched back to main
+ participants: ['agent1', 'customer1'],
+ },
+ consult: {
+ mediaResourceId: 'consult',
+ mType: 'consult',
+ isHold: true, // Consult is on hold - we're on main call
+ participants: ['agent1', 'epdn-agent'],
+ },
+ },
+ participants: {
+ agent1: {
+ id: 'agent1',
+ pType: 'Agent',
+ name: 'Agent One',
+ consultState: 'Initiated',
+ isConsulted: false,
+ hasLeft: false,
+ },
+ customer1: {
+ id: 'customer1',
+ pType: 'Customer',
+ name: 'Customer',
+ hasLeft: false,
+ },
+ },
+ },
+ },
+ } as ITask;
+
+ const result = getControlsVisibility(deviceType, featureFlags, task, 'agent1', false);
+
+ // EP_DN consult: End button should be enabled when on main call
+ expect(result.end.isVisible).toBe(true);
+ expect(result.end.isEnabled).toBe(true);
+ });
+
+ it('should disable end button during EP_DN consult when switched to EP_DN agent (main call held)', () => {
+ // Mock a task with EP_DN consult - switched to EP_DN agent (main call on hold)
+ const task = {
+ ...mockTask,
+ data: {
+ ...mockTask.data,
+ consultMediaResourceId: 'consult',
+ interaction: {
+ ...mockTask.data.interaction,
+ mediaType: 'telephony',
+ destAgentType: 'EPDN',
+ state: 'consulting',
+ media: {
+ main: {
+ mediaResourceId: 'main',
+ mType: 'mainCall',
+ isHold: true, // Main call is held - switched to EP_DN consult
+ participants: ['agent1', 'customer1'],
+ },
+ consult: {
+ mediaResourceId: 'consult',
+ mType: 'consult',
+ isHold: false, // Consult is active - talking to EP_DN
+ participants: ['agent1', 'epdn-agent'],
+ },
+ },
+ participants: {
+ agent1: {
+ id: 'agent1',
+ pType: 'Agent',
+ name: 'Agent One',
+ consultState: 'Initiated',
+ isConsulted: false,
+ hasLeft: false,
+ },
+ customer1: {
+ id: 'customer1',
+ pType: 'Customer',
+ name: 'Customer',
+ hasLeft: false,
+ },
+ },
+ },
+ },
+ } as ITask;
+
+ const result = getControlsVisibility(deviceType, featureFlags, task, 'agent1', false);
+
+ // EP_DN consult: End button should be disabled when main call is held (talking to EP_DN)
+ expect(result.end.isVisible).toBe(true);
+ expect(result.end.isEnabled).toBe(false);
+ });
+
+ it('should enable end button during EP_DN consult conference when main call is held but conference in progress', () => {
+ // Mock a task with EP_DN consult in conference state
+ const task = {
+ ...mockTask,
+ data: {
+ ...mockTask.data,
+ isConferenceInProgress: true,
+ consultMediaResourceId: 'consult',
+ interaction: {
+ ...mockTask.data.interaction,
+ mediaType: 'telephony',
+ destAgentType: 'EntryPoint',
+ state: 'conferencing',
+ media: {
+ main: {
+ mediaResourceId: 'main',
+ mType: 'mainCall',
+ isHold: true, // Main call is held during conference
+ participants: ['agent1', 'customer1', 'epdn-agent'],
+ },
+ },
+ participants: {
+ agent1: {
+ id: 'agent1',
+ pType: 'Agent',
+ name: 'Agent One',
+ consultState: 'Conferencing',
+ isConsulted: false,
+ hasLeft: false,
+ },
+ customer1: {
+ id: 'customer1',
+ pType: 'Customer',
+ name: 'Customer',
+ hasLeft: false,
+ },
+ 'epdn-agent': {
+ id: 'epdn-agent',
+ pType: 'Agent',
+ name: 'EP DN Agent',
+ hasLeft: false,
+ },
+ },
+ },
+ },
+ } as ITask;
+
+ const result = getControlsVisibility(deviceType, featureFlags, task, 'agent1', true);
+
+ expect(result.end.isVisible).toBe(true);
+ expect(result.end.isEnabled).toBe(true);
+ });
+
+ it('should recognize EP destAgentType variant', () => {
+ const task = {
+ ...mockTask,
+ data: {
+ ...mockTask.data,
+ consultMediaResourceId: 'consult',
+ interaction: {
+ ...mockTask.data.interaction,
+ destAgentType: 'EP',
+ mediaType: 'telephony',
+ state: 'consulting',
+ media: {
+ main: {
+ mediaResourceId: 'main',
+ mType: 'mainCall',
+ isHold: false, // Main call active - we're on main call
+ participants: ['agent1', 'customer1'],
+ },
+ consult: {
+ mediaResourceId: 'consult',
+ mType: 'consult',
+ isHold: true, // Consult on hold
+ participants: ['agent1', 'ep-agent'],
+ },
+ },
+ participants: {
+ agent1: {
+ id: 'agent1',
+ pType: 'Agent',
+ name: 'Agent One',
+ consultState: 'Initiated',
+ isConsulted: false,
+ hasLeft: false,
+ },
+ customer1: {
+ id: 'customer1',
+ pType: 'Customer',
+ name: 'Customer',
+ hasLeft: false,
+ },
+ },
+ },
+ },
+ } as ITask;
+
+ const result = getControlsVisibility(deviceType, featureFlags, task, 'agent1', false);
+
+ // EP_DN consult: End button should be enabled when on main call
+ expect(result.end.isVisible).toBe(true);
+ expect(result.end.isEnabled).toBe(true);
+ });
+
+ it('should handle missing destAgentType as non-EP_DN consult', () => {
+ const task = {
+ ...mockTask,
+ data: {
+ ...mockTask.data,
+ consultMediaResourceId: 'consult',
+ interaction: {
+ ...mockTask.data.interaction,
+ mediaType: 'telephony',
+ state: 'consulting',
+ media: {
+ main: {
+ mediaResourceId: 'main',
+ mType: 'mainCall',
+ isHold: true,
+ participants: ['agent1', 'customer1'],
+ },
+ consult: {
+ mediaResourceId: 'consult',
+ mType: 'consult',
+ isHold: false,
+ participants: ['agent1', 'agent2'],
+ },
+ },
+ participants: {
+ agent1: {
+ id: 'agent1',
+ pType: 'Agent',
+ consultState: 'Initiated',
+ hasLeft: false,
+ },
+ },
+ },
+ },
+ } as ITask;
+
+ const result = getControlsVisibility(deviceType, featureFlags, task, 'agent1', false);
+
+ // Should follow regular consult logic (disabled when on consult call)
+ expect(result.end.isVisible).toBe(true);
+ expect(result.end.isEnabled).toBe(false);
+ });
+
+ it('should handle missing task data gracefully', () => {
+ const task = {
+ ...mockTask,
+ data: undefined,
+ } as unknown as ITask;
+
+ const result = getControlsVisibility(deviceType, featureFlags, task, 'agent1', false);
+
+ // Should still return valid visibility structure
+ expect(result.end).toBeDefined();
+ expect(result.end.isVisible).toBeDefined();
+ expect(result.end.isEnabled).toBeDefined();
+ });
+
+ it('should handle missing interaction data gracefully', () => {
+ const task = {
+ ...mockTask,
+ data: {
+ ...mockTask.data,
+ interaction: undefined,
+ },
+ } as unknown as ITask;
+
+ const result = getControlsVisibility(deviceType, featureFlags, task, 'agent1', false);
+
+ expect(result.end).toBeDefined();
+ expect(result.end.isVisible).toBeDefined();
+ expect(result.end.isEnabled).toBeDefined();
+ });
+});
+
describe('findHoldTimestamp', () => {
it('returns the holdTimestamp for the correct mType', () => {
const interaction = {