From 4b9288473980a45937d0aa592541ee71eb85034f Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Sun, 9 Aug 2026 23:01:01 +0530 Subject: [PATCH] fix(cc-task): align consult/transfer tabs and lists with agent desktop (CAI-8354) --- .../consult-transfer-popover-hooks.ts | 2 +- .../consult-transfer-popover.tsx | 58 ++- .../consult-transfer-tab.utils.ts | 51 ++ .../task/CallControl/call-control.tsx | 9 + .../src/components/task/task.types.ts | 30 ++ ...consult-transfer-popover.snapshot.tsx.snap | 438 ++++++++++++++---- .../consult-transfer-popover.snapshot.tsx | 4 +- .../consult-transfer-popover.tsx | 30 +- .../consult-transfer-tab.utils.ts | 81 ++++ .../store/ai-docs/store-spec.md | 6 +- packages/contact-center/store/src/store.ts | 11 + .../contact-center/store/src/store.types.ts | 3 + .../store/src/storeEventsWrapper.ts | 26 +- .../store/tests/storeEventsWrapper.ts | 38 +- .../contact-center/task/ai-docs/task-spec.md | 2 +- .../task/src/CallControl/index.tsx | 30 ++ .../task/src/CallControlCAD/index.tsx | 30 ++ 17 files changed, 730 insertions(+), 119 deletions(-) create mode 100644 packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-tab.utils.ts create mode 100644 packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-tab.utils.ts diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts index c827bc1de..70fc7a691 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts @@ -30,7 +30,7 @@ import {DEFAULT_PAGE_SIZE} from '../../constants'; * @param logger - Optional logger instance for diagnostics. * @returns An object containing the transformed data (U[]), pagination state and helpers. */ -export const usePaginatedData = ( +export const usePaginatedData = ( fetchFunction: FetchPaginatedList | undefined, transformFunction: TransformPaginatedData, categoryName: string, diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 32e1ab744..fbd622da5 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -11,6 +11,12 @@ import { getAgentsForDisplay, } from './call-control-custom.utils'; import {useConsultTransferPopover} from './consult-transfer-popover-hooks'; +import { + isAgentsTabVisible, + isEntryPointTabVisible, + isQueuesTabVisible, + ConsultTransferAction, +} from './consult-transfer-tab.utils'; import { SEARCH_PLACEHOLDER, @@ -34,12 +40,26 @@ const ConsultTransferPopoverComponent: React.FC { const {showDialNumberTab = true, showEntryPointTab = true} = consultTransferOptions || {}; - const isEntryPointTabVisible = showEntryPointTab && heading === 'Consult'; + const action: ConsultTransferAction = heading === 'Transfer' ? 'Transfer' : 'Consult'; + const isQueueTabVisible = isQueuesTabVisible( + action, + allowConsultToQueue, + accessQueue, + interactionContext ?? {}, + isTelephony + ); + const isAgentsTabVisibleFlag = isAgentsTabVisible(accessBuddyTeam); + const isEntryPointTabVisibleFlag = isEntryPointTabVisible(showEntryPointTab, accessEntryPoint, isTelephony); const { selectedCategory, searchQuery, @@ -61,7 +81,7 @@ const ConsultTransferPopoverComponent: React.FC ); - const noQueues = !allowConsultToQueue || queuesData.length === 0; + const noQueues = !isQueueTabVisible || queuesData.length === 0; const noDialNumbers = !showDialNumberTab || dialNumbers.length === 0; - const noEntryPoints = !isEntryPointTabVisible || entryPoints.length === 0; + const noEntryPoints = !isEntryPointTabVisibleFlag || entryPoints.length === 0; const consultTransferManualAction = shouldAddConsultTransferAction( selectedCategory, - isEntryPointTabVisible, + isEntryPointTabVisibleFlag, allowParticipantsToInteract, searchQuery, entryPoints, @@ -180,17 +200,19 @@ const ConsultTransferPopoverComponent: React.FC
- - {allowConsultToQueue && ( + {isAgentsTabVisibleFlag && ( + + )} + {isQueueTabVisible && ( +
+
+
+
`; -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with allowConsultToQueue false 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with allowConsultToQueue false for Consult 1`] = `
@@ -1411,7 +1483,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tagname="h3" type="body-large-bold" > - Select an Agent + Consult
+
+
+
+
+
+
+
+
+
+
+
{ expect(container).toMatchSnapshot(); }); - it('should render with allowConsultToQueue false', async () => { - const noQueueConsultProps = {...defaultProps, allowConsultToQueue: false}; + it('should render with allowConsultToQueue false for Consult', async () => { + const noQueueConsultProps = {...defaultProps, heading: 'Consult', allowConsultToQueue: false}; let screen; await act(async () => { screen = render(); 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 375b76089..1c4453450 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 @@ -223,9 +223,10 @@ describe('ConsultTransferPopoverComponent', () => { expect(screen.container.querySelectorAll('.call-control-list-item').length).toBe(0); }); - it('hides queue tab when allowConsultToQueue is false', async () => { + it('hides queue tab when allowConsultToQueue is false for Consult', async () => { const propsWithoutQueue = { ...baseProps, + heading: 'Consult', allowConsultToQueue: false, }; @@ -234,6 +235,33 @@ describe('ConsultTransferPopoverComponent', () => { expect(maybeQueuesButton).toBeNull(); }); + it('shows queue tab for Transfer inbound when consultToQueue is off and accessQueue is SPECIFIC (AVERA)', async () => { + const averaProps = { + ...baseProps, + heading: 'Transfer', + allowConsultToQueue: false, + accessQueue: 'SPECIFIC', + interactionContext: {contactDirectionType: 'INBOUND', mediaType: 'telephony'}, + isTelephony: true, + }; + + const screen = await render(); + expect(screen.getByRole('button', {name: 'Queues'})).toBeInTheDocument(); + }); + + it('shows entry point tab on Transfer when accessEntryPoint allows it', async () => { + const transferProps = { + ...baseProps, + heading: 'Transfer', + accessEntryPoint: 'SPECIFIC', + isTelephony: true, + consultTransferOptions: {showEntryPointTab: true}, + }; + + const screen = await render(); + expect(screen.getByRole('button', {name: 'Entry Point'})).toBeInTheDocument(); + }); + it('covers edge case for empty items in renderList (line 50)', async () => { const propsWithEmptyAgents = { ...baseProps, diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-tab.utils.ts b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-tab.utils.ts new file mode 100644 index 000000000..dd38f1256 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-tab.utils.ts @@ -0,0 +1,81 @@ +import { + ConsultTransferInteractionContext, + isAgentsTabVisible, + isCollaborationAccessEnabled, + isEntryPointTabVisible, + isQueueEnabled, + isQueuesTabVisible, +} from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-tab.utils'; + +describe('consult-transfer-tab.utils', () => { + const inboundVoice: ConsultTransferInteractionContext = { + contactDirectionType: 'INBOUND', + mediaType: 'telephony', + }; + + const outboundVoiceTransferDisabled: ConsultTransferInteractionContext = { + contactDirectionType: 'OUTBOUND', + outdialTransferToQueueEnabled: false, + mediaType: 'telephony', + }; + + const outboundVoiceTransferEnabled: ConsultTransferInteractionContext = { + contactDirectionType: 'OUTBOUND', + outdialTransferToQueueEnabled: true, + mediaType: 'telephony', + }; + + describe('isCollaborationAccessEnabled', () => { + it('returns false when access is NONE (case-insensitive)', () => { + expect(isCollaborationAccessEnabled('NONE')).toBe(false); + expect(isCollaborationAccessEnabled('none')).toBe(false); + }); + + it('returns true for ALL, SPECIFIC, or undefined', () => { + expect(isCollaborationAccessEnabled('ALL')).toBe(true); + expect(isCollaborationAccessEnabled('SPECIFIC')).toBe(true); + expect(isCollaborationAccessEnabled(undefined)).toBe(true); + }); + }); + + describe('isQueueEnabled', () => { + it('requires allowConsultToQueue for Consult on voice', () => { + expect(isQueueEnabled('Consult', false, inboundVoice, true)).toBe(false); + expect(isQueueEnabled('Consult', true, inboundVoice, true)).toBe(true); + }); + + it('shows queues for Transfer on inbound voice even when consultToQueue is off (AVERA case)', () => { + expect(isQueueEnabled('Transfer', false, inboundVoice, true)).toBe(true); + }); + + it('gates Transfer outbound voice on outdialTransferToQueueEnabled', () => { + expect(isQueueEnabled('Transfer', false, outboundVoiceTransferDisabled, true)).toBe(false); + expect(isQueueEnabled('Transfer', false, outboundVoiceTransferEnabled, true)).toBe(true); + }); + + it('returns true for non-voice media', () => { + expect(isQueueEnabled('Consult', false, {mediaType: 'chat'}, false)).toBe(true); + }); + }); + + describe('tab visibility helpers', () => { + it('hides agents tab when accessBuddyTeam is NONE', () => { + expect(isAgentsTabVisible('NONE')).toBe(false); + expect(isAgentsTabVisible('SPECIFIC')).toBe(true); + }); + + it('shows queue tab for Transfer inbound when accessQueue is SPECIFIC and consultToQueue is off', () => { + expect(isQueuesTabVisible('Transfer', false, 'SPECIFIC', inboundVoice, true)).toBe(true); + }); + + it('hides queue tab when accessQueue is NONE even if queue transfer is otherwise enabled', () => { + expect(isQueuesTabVisible('Transfer', false, 'NONE', inboundVoice, true)).toBe(false); + }); + + it('shows entry point tab on voice when showEntryPointTab and accessEntryPoint allow it', () => { + expect(isEntryPointTabVisible(true, 'SPECIFIC', true)).toBe(true); + expect(isEntryPointTabVisible(true, 'NONE', true)).toBe(false); + expect(isEntryPointTabVisible(true, 'SPECIFIC', false)).toBe(false); + }); + }); +}); diff --git a/packages/contact-center/store/ai-docs/store-spec.md b/packages/contact-center/store/ai-docs/store-spec.md index e0a3dbd84..713552968 100644 --- a/packages/contact-center/store/ai-docs/store-spec.md +++ b/packages/contact-center/store/ai-docs/store-spec.md @@ -281,14 +281,16 @@ classDiagram - **UC-3 Observe agent/session state in React:** Widget wraps in `observer()` and reads `store.agentId`, `store.isAgentLoggedIn`, `store.deviceType`, `store.currentState` → re-renders on mutation. Evidence: `src/storeEventsWrapper.ts:56-187`, `_archive/.../AGENTS.md` usage. - **UC-4 Handle an incoming task through to wrap-up:** SDK `task:incoming` → listeners registered + `onIncomingTask` fired → `task:assigned` sets ENGAGED/current → `task:end` + `handleTaskRemove` cleans up. Evidence: `src/storeEventsWrapper.ts:585-762`, `tests/storeEventsWrapper.ts` ("events reactions"). - **UC-5 Campaign-preview accept flow:** `task:campaignPreviewReservation` puts a preview in RESERVED; preview stays out of `currentTask` until accepted (`acceptedCampaignIds`), then transitions to ENGAGED. Evidence: `src/storeEventsWrapper.ts:243-283,537-583,772-795`, `tests/storeEventsWrapper.ts` ("campaign preview task lifecycle"). -- **UC-6 Fetch a domain list for a widget dropdown:** Transfer/Consult widget calls `getBuddyAgents()`/`getQueues()`; Outdial calls `getEntryPoints()`/`getAddressBookEntries()` → store proxies the SDK, transforms/filters, returns. Evidence: `src/storeEventsWrapper.ts:924-1001`, `tests/storeEventsWrapper.ts`. +- **UC-6 Fetch a domain list for a widget dropdown:** Transfer/Consult widget calls `getBuddyAgents()`/`getQueues()`; Outdial calls `getEntryPoints()`/`getAddressBookEntries()` → store proxies the SDK, transforms/filters, returns. `getQueues`/`getEntryPoints` merge `desktopProfileFilter: true`. Evidence: `src/storeEventsWrapper.ts:924-1001`, `tests/storeEventsWrapper.ts`. ## State Model The store is a single MobX `makeAutoObservable` instance. Observable slices (all in `src/store.ts:23-56`): - **Session / profile:** `agentId`, `agentProfile`, `isAgentLoggedIn`, `deviceType`, `dialNumber`, `teamId`, `teams`, `loginOptions`, `idleCodes`, `wrapupCodes`, `featureFlags`, `dataCenter`. - **Agent state:** `currentState`, `customState`, `lastStateChangeTimestamp`, `lastIdleCodeChangeTimestamp`, `showMultipleLoginAlert`. - **Tasks:** `taskList` (`Record`), `currentTask`, `acceptedCampaignIds` (`Set`), `realtimeTranscriptionData`. -- **Call/consult control:** `isMuted`, `callControlAudio`, `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`, `isDeclineButtonEnabled`, `isEndConsultEnabled`, `allowConsultToQueue`, `isDigitalChannelsInitialized`. +- **Call/consult control:** `isMuted`, `callControlAudio`, `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`, `isDeclineButtonEnabled`, `isEndConsultEnabled`, `allowConsultToQueue`, `accessQueue`, `accessEntryPoint`, `accessBuddyTeam`, `isDigitalChannelsInitialized`. +- **`getQueues` / `getEntryPoints`:** merge `desktopProfileFilter: true` into SDK search params. List sort deferred pending team decision on CMS/BFF sort param parity. +- **`getBuddyAgents`:** returns agents sorted by `agentName` ascending (client-side; full list, no pagination). - **Misc:** `currentTheme`, `cc` (`observable.ref` — not deeply observed), `isAddressBookEnabled`. Transition triggers: SDK CC/task events drive the session/agent/task slices via the wrapper's handlers (`handleStateChange`, `handleTaskAssigned`, `refreshTaskList`, `cleanUpStore`, campaign-preview handlers). Widget-initiated mutators (`setDeviceType`, `setDialNumber`, `setTeamId`, `setState`, `setCurrentTheme`, etc.) drive UI-local slices. All writes pass through `runInAction`. diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 52654bbd7..6434ec3e5 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -50,6 +50,9 @@ class Store implements IStore { isEndConsultEnabled: boolean = false; isAddressBookEnabled: boolean = false; allowConsultToQueue: boolean = false; + accessQueue?: string; + accessEntryPoint?: string; + accessBuddyTeam?: string; agentProfile: AgentLoginProfile = {}; isMuted: boolean = false; isDigitalChannelsInitialized: boolean = false; @@ -118,6 +121,14 @@ class Store implements IStore { // TODO: Remove this once SDK performs the validation this.isAddressBookEnabled = Boolean(response.addressBookId); this.allowConsultToQueue = response.allowConsultToQueue; + const collaborationProfile = response as Profile & { + accessQueue?: string; + accessEntryPoint?: string; + accessBuddyTeam?: string; + }; + this.accessQueue = collaborationProfile.accessQueue; + this.accessEntryPoint = collaborationProfile.accessEntryPoint; + this.accessBuddyTeam = collaborationProfile.accessBuddyTeam; this.agentProfile.agentName = response.agentName; this.agentProfile.isTimeoutDesktopInactivityEnabled = response.isTimeoutDesktopInactivityEnabled; this.agentProfile.timeoutDesktopInactivityMins = response.timeoutDesktopInactivityMins; diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 61dac9e40..a45aa836b 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -196,6 +196,9 @@ interface IStore { callControlAudio: MediaStream | null; isEndConsultEnabled: boolean; allowConsultToQueue: boolean; + accessQueue?: string; + accessEntryPoint?: string; + accessBuddyTeam?: string; agentProfile: AgentLoginProfile; isMuted: boolean; isAddressBookEnabled: boolean; diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index efa3cf9ce..3cffead5f 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -188,6 +188,18 @@ class StoreWrapper implements IStoreWrapper { return this.store.allowConsultToQueue; } + get accessQueue() { + return this.store.accessQueue; + } + + get accessEntryPoint() { + return this.store.accessEntryPoint; + } + + get accessBuddyTeam() { + return this.store.accessBuddyTeam; + } + get agentProfile() { return this.store.agentProfile; } @@ -1095,7 +1107,10 @@ class StoreWrapper implements IStoreWrapper { }> => { try { const upperMediaType = mediaType.toUpperCase(); - const response = await this.store.cc.getQueues(params); + const response = await this.store.cc.getQueues({ + ...(params ?? {}), + desktopProfileFilter: true, + } as ContactServiceQueueSearchParams); const data = Array.isArray(response) ? response : response.data; const filtered = data.filter((queue) => queue.channelType === upperMediaType); const page = Array.isArray(response) ? 0 : (response.meta?.page ?? 0); @@ -1113,8 +1128,10 @@ class StoreWrapper implements IStoreWrapper { getEntryPoints = async (params?: EntryPointSearchParams): Promise => { try { - const response: EntryPointListResponse = await this.store.cc.getEntryPoints(params); - return response; + return await this.store.cc.getEntryPoints({ + ...(params ?? {}), + desktopProfileFilter: true, + } as EntryPointSearchParams); } catch (error) { this.store.logger.error('Error fetching entry points:', error); throw error; @@ -1126,8 +1143,7 @@ class StoreWrapper implements IStoreWrapper { if (!this.store.isAddressBookEnabled) { return {data: [], meta: {page: 0, totalPages: 0}}; } - const response: AddressBookEntriesResponse = await this.store.cc.addressBook.getEntries(params ?? {}); - return response; + return await this.store.cc.addressBook.getEntries(params ?? {}); } catch (error) { this.store.logger.error('Error fetching address book entries:', error); throw error; diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 62d624aab..919ab78d4 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -998,7 +998,11 @@ describe('storeEventsWrapper', () => { }); it('should return buddy agents list', async () => { - const buddyAgents = [{name: 'agent1'}, {name: 'agent2'}]; + const buddyAgents = [ + {agentName: 'Zeta Agent', agentId: '3'}, + {agentName: 'Alpha Agent', agentId: '1'}, + {agentName: 'Beta Agent', agentId: '2'}, + ]; storeWrapper['store'].cc.getBuddyAgents = jest.fn().mockResolvedValue({data: {agentList: buddyAgents}}); const result = await storeWrapper.getBuddyAgents('telephony'); expect(result).toEqual(buddyAgents); @@ -1023,7 +1027,22 @@ describe('storeEventsWrapper', () => { {id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}, {id: 'queue2', name: 'Queue 2', channelType: 'TELEPHONY'}, ]); - expect(storeWrapper['store'].cc.getQueues).toHaveBeenCalled(); + expect(storeWrapper['store'].cc.getQueues).toHaveBeenCalledWith({ + desktopProfileFilter: true, + }); + }); + + it('should pass desktopProfileFilter when getQueues is called with params', async () => { + const queueList = [{id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}]; + storeWrapper['store'].cc.getQueues = jest.fn().mockResolvedValue(queueList); + + await storeWrapper.getQueues('telephony', {page: 1, pageSize: 25}); + + expect(storeWrapper['store'].cc.getQueues).toHaveBeenCalledWith({ + page: 1, + pageSize: 25, + desktopProfileFilter: true, + }); }); it('should handle error in getQueues and throw error', async () => { @@ -1044,7 +1063,9 @@ describe('storeEventsWrapper', () => { const result = await storeWrapper.getQueues('telephony'); expect(result.data).toEqual([{...mockQueueDetails[0], channelType: 'TELEPHONY'}]); - expect(storeWrapper['store'].cc.getQueues).toHaveBeenCalled(); + expect(storeWrapper['store'].cc.getQueues).toHaveBeenCalledWith({ + desktopProfileFilter: true, + }); }); it('should handle consultQueueCancelled event', () => { @@ -1062,7 +1083,11 @@ describe('storeEventsWrapper', () => { storeWrapper['store'].cc.getEntryPoints = jest.fn().mockResolvedValue(mockEntryPointsResponse); const result = await storeWrapper.getEntryPoints({page: 0, pageSize: 25}); - expect(storeWrapper['store'].cc.getEntryPoints).toHaveBeenCalledWith({page: 0, pageSize: 25}); + expect(storeWrapper['store'].cc.getEntryPoints).toHaveBeenCalledWith({ + page: 0, + pageSize: 25, + desktopProfileFilter: true, + }); expect(result).toEqual(mockEntryPointsResponse); }); @@ -1076,7 +1101,10 @@ describe('storeEventsWrapper', () => { jest.spyOn(storeWrapper['store'].cc.addressBook, 'getEntries').mockResolvedValue(mockAddressBookEntriesResponse); const result = await storeWrapper.getAddressBookEntries({page: 0, pageSize: 25}); - expect(storeWrapper['store'].cc.addressBook.getEntries).toHaveBeenCalledWith({page: 0, pageSize: 25}); + expect(storeWrapper['store'].cc.addressBook.getEntries).toHaveBeenCalledWith({ + page: 0, + pageSize: 25, + }); expect(result).toEqual(mockAddressBookEntriesResponse); }); diff --git a/packages/contact-center/task/ai-docs/task-spec.md b/packages/contact-center/task/ai-docs/task-spec.md index 060f6e128..f70160edb 100644 --- a/packages/contact-center/task/ai-docs/task-spec.md +++ b/packages/contact-center/task/ai-docs/task-spec.md @@ -392,7 +392,7 @@ The six widget shells are siblings that each bind to exactly one hook and one pr - **UC-3 Hold / resume active call (CallControl):** Agent clicks Hold → `toggleHold(true)` → `currentTask.hold()` → `TASK_HOLD` → hold timer starts via `useHoldTimer`, `onHoldResume({isHeld:true})`. Evidence: `src/helper.ts`, `src/Utils/useHoldTimer.ts`, `tests/helper.ts` (hold/resume cases). UI flow: Hold button toggles to Resume; "Hold" elapsed timer shown. - **UC-4 Toggle recording (CallControl):** Agent clicks record toggle → `toggleRecording()` → `pauseRecording()`/`resumeRecording()` → `TASK_RECORDING_PAUSED/RESUMED` → `isRecording` flips, `onRecordingToggle`. Evidence: `src/helper.ts`, `tests/helper.ts` (recording cases). - **UC-5 Mute / unmute (CallControl):** Agent clicks mute → `toggleMute()` (gated by `controlVisibility.muteUnmute`) → `currentTask.toggleMute()` → `store.setIsMuted`, `onToggleMute`. Evidence: `src/helper.ts`, `tests/helper.ts` ("should successfully toggle mute…", "rapid toggleMute"). UI flow: shows error-safe revert on failure. -- **UC-6 Consult an agent/queue/EP-DN then transfer or conference (CallControl):** Agent opens consult modal → `consultCall(dest,type,allow)` → on completion `consultTransfer()` (or `transferConference` in conference) or `endConsultCall()`. Evidence: `src/helper.ts`, `tests/helper.ts` (consult/transfer/conference cases), `tests/utils/task-util.ts` (EP-DN end-button cases). UI flow: consult controls (switch/merge/end) appear; consult timer label `Consult Requested`/`Consulting`/`Consult on Hold`. +- **UC-6 Consult an agent/queue/EP-DN then transfer or conference (CallControl):** Agent opens consult modal → `consultCall(dest,type,allow)` → on completion `consultTransfer()` (or `transferConference` in conference) or `endConsultCall()`. Consult/Transfer popover tab visibility follows Agent Desktop: `accessQueue`/`accessEntryPoint`/`accessBuddyTeam` from store plus interaction direction (`contactDirection`, `outdialTransferToQueueEnabled`) for queue tab gating. Evidence: `src/helper.ts`, `packages/contact-center/cc-components/.../consult-transfer-tab.utils.ts`, `task/src/CallControl/index.tsx`. - **UC-7 Blind transfer (CallControl):** Agent picks destination → `transferCall(to,type)` → `currentTask.transfer(...)`; failure re-thrown to the modal. Evidence: `src/helper.ts`, `tests/helper.ts` ("should call transferCall successfully"). - **UC-8 Wrap up a call (CallControl):** Call ends → wrap-up codes shown → Agent selects code → `wrapupCall(reason, auxCodeId)` → `currentTask.wrapup` → next task promoted to `currentTask`, agent → ENGAGED. Evidence: `src/helper.ts`, `tests/helper.ts` ("should call wrapupCall"). Auto-wrap-up: countdown shown with Cancel. UI flow: wrap-up dropdown + optional auto-wrap-up countdown. - **UC-9 Place an outbound call (OutdialCall):** Agent enters number, selects ANI → dial → `startOutdial(destination, origin)`; empty number alerts and aborts; disabled while a telephony task is active. Evidence: `src/helper.ts` (`useOutdialCall`), `tests/OutdialCall/index.tsx`. UI flow: dialpad with validation, ANI dropdown, address book toggled by `isAddressBookEnabled`. diff --git a/packages/contact-center/task/src/CallControl/index.tsx b/packages/contact-center/task/src/CallControl/index.tsx index 443072076..c9ef6870a 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -7,6 +7,29 @@ import {useCallControl} from '../helper'; import {CallControlProps} from '../task.types'; import {CallControlComponent} from '@webex/cc-components'; import {isUnacceptedCampaignPreview} from '../Utils/task-util'; +import {ITask} from '@webex/contact-center'; + +type ConsultTransferInteractionContext = { + contactDirectionType?: string; + outdialTransferToQueueEnabled?: boolean; + mediaType?: string; +}; + +const buildConsultTransferInteractionContext = (currentTask?: ITask): ConsultTransferInteractionContext => { + const interaction = currentTask?.data?.interaction as + | { + contactDirection?: {type?: string}; + outdialTransferToQueueEnabled?: boolean; + mediaType?: string; + } + | undefined; + + return { + contactDirectionType: interaction?.contactDirection?.type, + outdialTransferToQueueEnabled: interaction?.outdialTransferToQueueEnabled, + mediaType: interaction?.mediaType, + }; +}; const CallControlInternal: React.FunctionComponent = observer( ({onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute, consultTransferOptions, conferenceEnabled}) => { @@ -17,6 +40,9 @@ const CallControlInternal: React.FunctionComponent = observer( consultStartTimeStamp, callControlAudio, allowConsultToQueue, + accessQueue, + accessEntryPoint, + accessBuddyTeam, isMuted, agentId, acceptedCampaignIds, @@ -48,6 +74,10 @@ const CallControlInternal: React.FunctionComponent = observer( consultStartTimeStamp, callControlAudio, allowConsultToQueue, + accessQueue, + accessEntryPoint, + accessBuddyTeam, + interactionContext: buildConsultTransferInteractionContext(currentTask), logger, consultTransferOptions, }; diff --git a/packages/contact-center/task/src/CallControlCAD/index.tsx b/packages/contact-center/task/src/CallControlCAD/index.tsx index 1426d19da..ab27c18a4 100644 --- a/packages/contact-center/task/src/CallControlCAD/index.tsx +++ b/packages/contact-center/task/src/CallControlCAD/index.tsx @@ -7,6 +7,29 @@ import {useCallControl} from '../helper'; import {CallControlProps} from '../task.types'; import {CallControlCADComponent} from '@webex/cc-components'; import {isUnacceptedCampaignPreview} from '../Utils/task-util'; +import {ITask} from '@webex/contact-center'; + +type ConsultTransferInteractionContext = { + contactDirectionType?: string; + outdialTransferToQueueEnabled?: boolean; + mediaType?: string; +}; + +const buildConsultTransferInteractionContext = (currentTask?: ITask): ConsultTransferInteractionContext => { + const interaction = currentTask?.data?.interaction as + | { + contactDirection?: {type?: string}; + outdialTransferToQueueEnabled?: boolean; + mediaType?: string; + } + | undefined; + + return { + contactDirectionType: interaction?.contactDirection?.type, + outdialTransferToQueueEnabled: interaction?.outdialTransferToQueueEnabled, + mediaType: interaction?.mediaType, + }; +}; const CallControlCADInternal: React.FunctionComponent = observer( ({ @@ -27,6 +50,9 @@ const CallControlCADInternal: React.FunctionComponent = observ consultStartTimeStamp, callControlAudio, allowConsultToQueue, + accessQueue, + accessEntryPoint, + accessBuddyTeam, isMuted, agentId, acceptedCampaignIds, @@ -57,6 +83,10 @@ const CallControlCADInternal: React.FunctionComponent = observ callControlClassName, callControlConsultClassName, allowConsultToQueue, + accessQueue, + accessEntryPoint, + accessBuddyTeam, + interactionContext: buildConsultTransferInteractionContext(currentTask), logger, consultTransferOptions, };