Skip to content

Commit

Permalink
feat(wxcc): added agent outbound dial feature (#4086)
Browse files Browse the repository at this point in the history
Co-authored-by: parv_gour <parv_gour@PAGOUR-M-D8B2>
  • Loading branch information
pagour98 and parv_gour authored Feb 18, 2025
1 parent dd98dc6 commit f5a1ab2
Show file tree
Hide file tree
Showing 14 changed files with 479 additions and 5 deletions.
48 changes: 47 additions & 1 deletion docs/samples/contact-center/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ let taskId;
let wrapupCodes = []; // Add this to store wrapup codes
let isConsultOptionsShown = false;
let isTransferOptionsShown = false; // Add this variable to track the state of transfer options
let entryPointId = '';
let stateTimer;

const authTypeElm = document.querySelector('#auth-type');
Expand Down Expand Up @@ -382,6 +383,50 @@ async function endConsult() {
}
}

// Function to start an outdial call.
async function startOutdial() {

const destination = document.getElementById('outBoundDialNumber').value;

if (!destination || !destination.trim()) {
alert('Destination number is required');
return;
}

if (!entryPointId) {
alert('Entry point ID is not configured');
return;
}

const dialerPayload = {
entryPointId: entryPointId,
destination: destination,
direction: 'OUTBOUND',
attributes: {},
mediaType: 'telephony',
outboundType: 'OUTDIAL',
};

try {
console.log('Making an outdial call');
await webex.cc.startOutdial(dialerPayload);
console.log('Outdial call initiated successfully');
} catch (error) {
console.error('Failed to initiate outdial call', error);
alert('Failed to initiate outdial call');
}
}

// Function to press a key during an active call
function pressKey(value) {
// Allow only digits, #, *, and +
if (!/^[\d#*+]$/.test(value)) {
console.warn('Invalid keypad input:', value);
return;
}
document.getElementById('outBoundDialNumber').value += value;
}

// Enable consult button after task is accepted
function enableConsultControls() {
consultTabBtn.disabled = false;
Expand Down Expand Up @@ -476,7 +521,7 @@ function registerTaskListeners(task) {
}
});
task.on('task:rejected', (reason) => {
console.info('Adhwaith', reason);
console.info('Task is rejected with reason:', reason);
if (reason === 'RONA_TIMER_EXPIRED') {
answerElm.disabled = true;
declineElm.disabled = true;
Expand Down Expand Up @@ -606,6 +651,7 @@ function register() {
idleCodesDropdown.add(option);
}
});
entryPointId = agentProfile.outDialEp;
}).catch((error) => {
console.error('Event subscription failed', error);
})
Expand Down
23 changes: 23 additions & 0 deletions docs/samples/contact-center/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,29 @@ <h2 class="collapsible">
</div>
</fieldset>
</div>
<div id="outBoundCallDialer">
<fieldset>
<legend>Outdial Call</legend>
<div class="keypad">
<input id="outBoundDialNumber" placeholder="Enter number to dial">
<div class="keys">
<div class="key" onclick="pressKey('1')">1</div>
<div class="key" onclick="pressKey('2')">2</div>
<div class="key" onclick="pressKey('3')">3</div>
<div class="key" onclick="pressKey('4')">4</div>
<div class="key" onclick="pressKey('5')">5</div>
<div class="key" onclick="pressKey('6')">6</div>
<div class="key" onclick="pressKey('7')">7</div>
<div class="key" onclick="pressKey('8')">8</div>
<div class="key" onclick="pressKey('9')">9</div>
<div class="key" onclick="pressKey('*')">*</div>
<div class="key" onclick="pressKey('0')">0</div>
<div class="key" onclick="pressKey('#')">#</div>
</div>
<button class="call-btn" onclick="startOutdial()"><i class="fa fa-phone"></i></button>
</div>
</fieldset>
</div>
</section>
</div>
</div>
Expand Down
45 changes: 45 additions & 0 deletions docs/samples/contact-center/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -445,4 +445,49 @@ legend {
background-color:lightgrey;
border-radius:5px;
padding: 0 2px;
}

.keypad {
display: flex;
flex-direction: column;
align-items: center;
background: #222;
padding: 20px;
border-radius: 10px;
width: 250px;
}
.keypad input {
width: 100%;
padding: 10px;
text-align: center;
font-size: 18px;
margin-bottom: 10px;
}
.keys {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.key {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: #333;
color: white;
font-size: 20px;
border-radius: 50%;
cursor: pointer;
}
.call-btn {
margin-top: 10px;
background: green;
color: white;
padding: 10px 20px;
border-radius: 50%;
cursor: pointer;
}
.dropdown {
margin-top: 10px;
}
32 changes: 31 additions & 1 deletion packages/@webex/plugin-cc/src/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {AGENT_STATE_AVAILABLE, AGENT_STATE_AVAILABLE_ID} from './services/config
import {ConnectionLostDetails} from './services/core/websocket/types';
import TaskManager from './services/task/TaskManager';
import WebCallingService from './services/WebCallingService';
import {ITask, TASK_EVENTS} from './services/task/types';
import {ITask, TASK_EVENTS, DialerPayload, TaskResponse} from './services/task/types';

export default class ContactCenter extends WebexPlugin implements IContactCenter {
namespace = 'cc';
Expand Down Expand Up @@ -426,4 +426,34 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
this.webCallingService.setLoginOption(deviceType);
this.agentConfig.deviceType = deviceType;
}

/**
* This is used for making the outdial call.
* @param outDialPayload
* @returns Promise<TaskResponse>
* @throws Error
* @example
* ```typescript
* const outDialPayload = {
* entryPointId: entryPointId,
destination: destination,
direction: 'OUTBOUND',
attributes: {},
mediaType: 'telephony',
outboundType: 'OUTDIAL',
* }
* const result = await webex.cc.startOutdial(outDialPayload).then(()=>{}).catch(()=>{});
* ```
*/

public async startOutdial(outDialPayload: DialerPayload): Promise<TaskResponse> {
try {
const result = await this.services.dialer.startOutdial({data: outDialPayload});

return result;
} catch (error) {
const {error: detailedError} = getErrorDetails(error, 'startOutdial', CC_FILE);
throw detailedError;
}
}
}
1 change: 1 addition & 0 deletions packages/@webex/plugin-cc/src/services/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const CC_EVENTS = {
AGENT_WRAPUP: 'AgentWrapup',
AGENT_WRAPPEDUP: 'AgentWrappedUp',
AGENT_WRAPUP_FAILED: 'AgentWrapupFailed',
AGENT_OUTBOUND_FAILED: 'AgentOutboundFailed',
AGENT_CONTACT: 'AgentContact',
} as const;

Expand Down
1 change: 1 addition & 0 deletions packages/@webex/plugin-cc/src/services/core/Err.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type TaskErrorIds =
| {'Service.aqm.task.VteamListFailed': Failure}
| {'Service.aqm.task.pauseRecording': Failure}
| {'Service.aqm.task.resumeRecording': Failure}
| {'Service.aqm.dialer.startOutdial': Failure}
| {'Service.reqs.generic.failure': {trackingId: string}};

export type ReqError =
Expand Down
3 changes: 3 additions & 0 deletions packages/@webex/plugin-cc/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import AqmReqs from './core/aqm-reqs';
import {WebSocketManager} from './core/websocket/WebSocketManager';
import {ConnectionService} from './core/websocket/connection-service';
import {WebexSDK, SubscribeRequest} from '../types';
import aqmDialer from './task/dialer';

export default class Services {
public readonly agent: ReturnType<typeof routingAgent>;
public readonly config: AgentConfigService;
public readonly contact: ReturnType<typeof routingContact>;
public readonly dialer: ReturnType<typeof aqmDialer>;
public readonly webSocketManager: WebSocketManager;
public readonly connectionService: ConnectionService;
private static instance: Services;
Expand All @@ -21,6 +23,7 @@ export default class Services {
this.config = new AgentConfigService();
this.agent = routingAgent(aqmReq);
this.contact = routingContact(aqmReq);
this.dialer = aqmDialer(aqmReq);
this.connectionService = new ConnectionService({
webSocketManager: this.webSocketManager,
subscribeRequest: connectionConfig,
Expand Down
18 changes: 18 additions & 0 deletions packages/@webex/plugin-cc/src/services/task/TaskManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,24 @@ export default class TaskManager extends EventEmitter {
this.emit(TASK_EVENTS.TASK_INCOMING, this.currentTask);
}
break;
case CC_EVENTS.AGENT_OFFER_CONTACT:
// We don't have to emit any event here since this will be result of promise.
this.currentTask = this.currentTask.updateTaskData(payload.data);
LoggerProxy.log('Agent offer contact', {
module: TASK_MANAGER_FILE,
method: 'registerTaskListeners',
});
break;
case CC_EVENTS.AGENT_OUTBOUND_FAILED:
// We don't have to emit any event here since this will be result of promise.
if (this.currentTask.data) {
this.removeCurrentTaskFromCollection();
}
LoggerProxy.log('Agent outbound failed', {
module: TASK_MANAGER_FILE,
method: 'registerTaskListeners',
});
break;
case CC_EVENTS.AGENT_CONTACT_ASSIGNED:
this.currentTask = this.currentTask.updateTaskData(payload.data);
this.currentTask.emit(TASK_EVENTS.TASK_ASSIGNED, this.currentTask);
Expand Down
34 changes: 34 additions & 0 deletions packages/@webex/plugin-cc/src/services/task/dialer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {CC_EVENTS} from '../config/types';
import {WCC_API_GATEWAY} from '../constants';
import {createErrDetailsObject as err} from '../core/Utils';
import {TASK_MESSAGE_TYPE, TASK_API} from './constants';
import * as Contact from './types';
import AqmReqs from '../core/aqm-reqs';

export default function aqmDialer(aqm: AqmReqs) {
return {
/*
* Make outbound request.
*/
startOutdial: aqm.req((p: {data: Contact.DialerPayload}) => ({
url: `${TASK_API}`,
host: WCC_API_GATEWAY,
data: p.data,
err,
notifSuccess: {
bind: {
type: TASK_MESSAGE_TYPE,
data: {type: CC_EVENTS.AGENT_OFFER_CONTACT},
},
msg: {} as Contact.AgentContact,
},
notifFail: {
bind: {
type: TASK_MESSAGE_TYPE,
data: {type: CC_EVENTS.AGENT_OUTBOUND_FAILED},
},
errId: 'Service.aqm.dialer.startOutdial',
},
})),
};
}
15 changes: 14 additions & 1 deletion packages/@webex/plugin-cc/src/services/task/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,19 @@ export type WrapupPayLoad = {
auxCodeId: string;
};

/**
* Parameters to be passed for outbound dialer task
*/
export type DialerPayload = {
entryPointId: string;
destination: string;
direction: string;
origin?: string;
attributes: {[key: string]: string};
mediaType: string;
outboundType: string;
};

export type ContactCleanupData = {
type: string;
orgId: string;
Expand Down Expand Up @@ -388,7 +401,7 @@ export interface ITask extends EventEmitter {
*/
unregisterWebCallListeners(): void;
/**
* Used to update the task the data received on each event
* Used to update the task when the data received on each event
*/
updateTaskData(newData: TaskData): ITask;
/**
Expand Down
3 changes: 2 additions & 1 deletion packages/@webex/plugin-cc/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ export type RequestBody =
| Contact.TransferPayLoad
| Contact.ConsultTransferPayLoad
| Contact.cancelCtq
| Contact.WrapupPayLoad;
| Contact.WrapupPayLoad
| Contact.DialerPayload;

/**
* Represents the options to fetch buddy agents for the logged in agent.
Expand Down
Loading

0 comments on commit f5a1ab2

Please sign in to comment.