Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(widgets): added-blind-transfer #422

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/contact-center/store/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Store implements IStore {
lastStateChangeTimestamp?: number;
lastIdleCodeChangeTimestamp?: number;
showMultipleLoginAlert: boolean = false;
isAgentTransferred: boolean = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State so we can check when the agent has been transferred.


constructor() {
makeAutoObservable(this, {
Expand Down
7 changes: 5 additions & 2 deletions packages/contact-center/store/src/store.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {AgentLogin, IContactCenter, Profile, Team, LogContext} from '@webex/plugin-cc';
import {ITask} from '@webex/plugin-cc';
import {ITask, AgentLogin, IContactCenter, Profile, Team, LogContext, BuddyDetails} from '@webex/plugin-cc';

type ILogger = {
log: (message: string, context?: LogContext) => void;
Expand Down Expand Up @@ -42,6 +41,7 @@ interface IStore {
isAgentLoggedIn: boolean;
deviceType: string;
wrapupRequired: boolean;
isAgentTransferred: boolean;
currentState: string;
lastStateChangeTimestamp?: number;
lastIdleCodeChangeTimestamp?: number;
Expand All @@ -67,6 +67,7 @@ interface IStoreWrapper extends IStore {
setIsAgentLoggedIn(value: boolean): void;
setWrapupCodes(wrapupCodes: IWrapupCode[]): void;
setState(state: IdleCode | ICustomState): void;
setIsAgentTransferred(value: boolean): void;
}

interface IWrapupCode {
Expand All @@ -93,6 +94,7 @@ enum TASK_EVENTS {
CONTACT_RECORDING_PAUSED = 'ContactRecordingPaused',
CONTACT_RECORDING_RESUMED = 'ContactRecordingResumed',
AGENT_WRAPPEDUP = 'AgentWrappedUp',
AGENT_BLIND_TRANSFERRED = 'AgentBlindTransferred',
} // TODO: remove this once cc sdk exports this enum
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, we will update above state on this event.


// Events that are received on the contact center SDK
Expand Down Expand Up @@ -130,6 +132,7 @@ export type {
IWrapupCode,
IStoreWrapper,
ICustomState,
BuddyDetails,
};

export {CC_EVENTS, TASK_EVENTS};
41 changes: 38 additions & 3 deletions packages/contact-center/store/src/storeEventsWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
IdleCode,
IContactCenter,
ITask,
BuddyDetails,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this for the budyd agents details.

} from './store.types';
import Store from './store';
import {runInAction} from 'mobx';
Expand Down Expand Up @@ -84,6 +85,10 @@ class StoreWrapper implements IStoreWrapper {
return this.store.showMultipleLoginAlert;
}

get isAgentTransferred() {
return this.store.isAgentTransferred;
}

get currentTheme() {
return this.store.currentTheme;
}
Expand Down Expand Up @@ -131,6 +136,10 @@ class StoreWrapper implements IStoreWrapper {
this.store.wrapupRequired = value;
};

setIsAgentTransferred = (value: boolean): void => {
this.store.isAgentTransferred = value;
};

setCurrentTask = (task: ITask): void => {
runInAction(() => {
this.store.currentTask = task;
Expand Down Expand Up @@ -234,14 +243,17 @@ class StoreWrapper implements IStoreWrapper {
});
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
handleAgentBlindTransferred = (event) => {
this.setIsAgentTransferred(true);
};

handleTaskEnd = (event) => {
// If the call is ended by agent we get the task object in event.data
// If the call is ended by customer we get the task object directly

const task = event.data ? event.data : event;
// TODO -- SDK needs to send only 1 event on end : https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-615785

if (task.interaction.state === 'connected') {
if (task.interaction.state === 'connected' || this.store.isAgentTransferred) {
this.setWrapupRequired(true);
return;
} else if (task.interaction.state !== 'connected' && this.store.wrapupRequired !== true) {
Expand All @@ -264,6 +276,7 @@ class StoreWrapper implements IStoreWrapper {
handleTaskWrapUp = (event) => {
const task = event;
this.setWrapupRequired(false);
this.setIsAgentTransferred(false);
this.handleTaskRemove(task.interactionId);
};

Expand All @@ -286,6 +299,9 @@ class StoreWrapper implements IStoreWrapper {

task.on(TASK_EVENTS.AGENT_WRAPPEDUP, this.handleTaskWrapUp);

// Listen for AGENT_BLIND_TRANSFERRED event
task.on(TASK_EVENTS.AGENT_BLIND_TRANSFERRED, () => this.handleAgentBlindTransferred(task));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we listening for this event? I don't see SDK emitting any event for trasnfer. Complete dependency is there on promise resolve for transfer

Copy link
Contributor Author

@adhmenon adhmenon Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit:
I did some testing... and it was working with just promise based... I was in the wrong here.. thought it might cause some async issues and that end call would arrive before we set the state in store...


this.setIncomingTask(task);
this.setTaskList([...this.store.taskList, task]);
};
Expand Down Expand Up @@ -319,6 +335,9 @@ class StoreWrapper implements IStoreWrapper {

task.on(TASK_EVENTS.AGENT_WRAPPEDUP, this.handleTaskWrapUp);

// Listen for AGENT_BLIND_TRANSFERRED event
task.on(TASK_EVENTS.AGENT_BLIND_TRANSFERRED, () => this.handleAgentBlindTransferred(task));

if (!this.store.taskList.some((t) => t.data.interactionId === task.data.interactionId)) {
this.setTaskList([...this.store.taskList, task]);
}
Expand Down Expand Up @@ -418,6 +437,22 @@ class StoreWrapper implements IStoreWrapper {
});
});
};

getBuddyAgents = async (): Promise<Array<BuddyDetails>> => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method so we can extract buddy agents - queues will also be similar to this.

try {
const response = await this.store.cc.getBuddyAgents({
mediaType: 'telephony',
state: 'Available',
});
return response?.data?.agentList || [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have ? after response? In case of success, response will always be there and have data right. And even agentList is mandatory field but it can be empty array sometime.
And failure should go to catch block

} catch (error) {
this.store.logger.error(`Error fetching buddy agents: ${error}`, {
module: 'cc-store#storeEventsWrapper.ts',
method: 'getBuddyAgents',
});
return [];
}
};
}

// Create and export a single instance of the wrapper
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import {ListItemBase, ListItemBaseSection, AvatarNext, Text, ButtonCircle} from '@momentum-ui/react-collaboration';
import {Icon} from '@momentum-design/components/dist/react';
import classnames from 'classnames';

export interface CallControlListItemPresentationalProps {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire class is for the custom list item, right now all it consists of is an avatar, title, subtitle and a button which are all highly customisable.
Open to any suggestions to make it better.

title: string;
subtitle?: string;
buttonIcon: string;
onButtonPress: () => void;
className?: string;
}

const CallControlListItemPresentational: React.FC<CallControlListItemPresentationalProps> = (props) => {
const {title, subtitle, buttonIcon, onButtonPress, className} = props;
const initials = title
.split(' ')
.map((word) => word[0])
.join('')
.slice(0, 2)
.toUpperCase();

return (
<ListItemBase className={classnames('call-control-list-item', className)} size={50} isPadded aria-label={title}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Size is 50 by default.

<ListItemBaseSection position="start">
<AvatarNext size={32} initials={initials} title={title} />
</ListItemBaseSection>
<ListItemBaseSection
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have used sections here as that is what the code on the web client was using.

position="middle"
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
marginLeft: '8px',
minWidth: 0,
overflow: 'hidden',
}}
>
<Text tagName="p" type="body-primary" style={{margin: 0, lineHeight: '1.2'}}>
{title}
</Text>
{subtitle && (
<Text tagName="p" type="body-secondary" style={{margin: 0, lineHeight: '1.2'}}>
{subtitle}
</Text>
)}
</ListItemBaseSection>
<ListItemBaseSection position="end">
<div className="hover-button">
<ButtonCircle onPress={onButtonPress} size={28} color="join">
<Icon name={buttonIcon} />
</ButtonCircle>
</div>
</ListItemBaseSection>
</ListItemBase>
);
};

export default CallControlListItemPresentational;
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, {useState} from 'react';
import {Text, TabListNext, TabNext, ListNext} from '@momentum-ui/react-collaboration';
import CallControlListItemPresentational from './call-control-list-item.presentational';

export interface CallControlPopoverPresentationalProps {
heading: string;
buttonIcon: string;
buddyAgents: Array<{agentId: string; agentName: string; dn: string}>;
onAgentSelect: (agentId: string) => void;
}

const CallControlPopoverPresentational: React.FC<CallControlPopoverPresentationalProps> = ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for the internal list inside the popover (ie heading, tab list and the list of agents)
We will move it once the component PR goes in as well.
It will simply render everything - it is also customisable based on trasnfer or consult.

heading,
buttonIcon,
buddyAgents,
onAgentSelect,
}) => {
const [selectedTab, setSelectedTab] = useState('Agents');
const filteredAgents = buddyAgents;

return (
<div className="agent-popover-content">
<Text tagName="h3" className="agent-popover-title" type="body-large-bold" style={{margin: '0 0 0 0'}}>
{heading}
</Text>
<TabListNext
aria-label="Tabs"
className="agent-tablist"
hasBackground={false}
style={{marginTop: '0'}}
onTabSelection={(key) => setSelectedTab(key as string)}
>
<TabNext key="Agents" className="agent-tab" active={selectedTab === 'Agents'}>
Agents
</TabNext>
</TabListNext>
<ListNext listSize={filteredAgents.length} className="agent-agent-list">
{filteredAgents.map((agent) => (
<div
key={agent.agentId}
onMouseDown={(e) => e.stopPropagation()}
style={{cursor: 'pointer', pointerEvents: 'auto'}}
>
<CallControlListItemPresentational
title={agent.agentName}
buttonIcon={buttonIcon}
onButtonPress={() => onAgentSelect(agent.agentId)}
/>
</div>
))}
</ListNext>
{filteredAgents.length === 0 && (
<Text tagName="small" type="body-secondary">
No agents found
</Text>
)}
</div>
);
};

export default CallControlPopoverPresentational;
Loading
Loading