Skip to content

Commit

Permalink
feat(plugin-cc): add task sync on reload (#4089)
Browse files Browse the repository at this point in the history
  • Loading branch information
rarajes2 authored Feb 12, 2025
1 parent 28a4548 commit dd98dc6
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 56 deletions.
78 changes: 78 additions & 0 deletions docs/samples/contact-center/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ function updateButtonsPostEndCall() {
showConsultButton()
disableTransferControls();
consultTabBtn.disabled = true;
incomingDetailsElm.innerText = '';
pauseResumeRecordingElm.innerText = 'Pause Recording';
}

function showInitiateConsultDialog() {
Expand Down Expand Up @@ -614,6 +616,10 @@ function register() {
incomingCallListener.dispatchEvent(taskEvents);
});

webex.cc.on('task:hydrate', (currentTask) => {
handleTaskHydrate(currentTask);
});

webex.cc.on('agent:stateChange', (data) => {
if (data && typeof data === 'object' && data.type === 'AgentStateChangeSuccess') {
const DEFAULT_CODE = '0'; // Default code when no aux code is present
Expand All @@ -631,6 +637,78 @@ function register() {

}

function handleTaskHydrate(currentTask) {
task = currentTask;

if (!task || !task.data || !task.data.interaction) {
console.error('task:hydrate --> No task data found.');
alert('task:hydrate --> No task data found.');

return;
}

const { data, webCallingService } = task;
const { interaction, mediaResourceId, agentId } = data;
const {
state,
isTerminated,
media,
participants,
callAssociatedDetails,
callProcessingDetails
} = interaction;

if (isTerminated) {
// wrapup
if (state === 'wrapUp' && !participants[agentId].isWrappedUp) {
wrapupCodesDropdownElm.disabled = false;
wrapupElm.disabled = false;
}

return;
}

// answer & decline incoming calls
const callerDisplay = callAssociatedDetails?.ani;
if (webCallingService.loginOption === 'BROWSER') {
answerElm.disabled = false;
declineElm.disabled = false;

incomingDetailsElm.innerText = `Call from ${callerDisplay}`;
} else {
incomingDetailsElm.innerText = `Call from ${callerDisplay}...please answer on the endpoint where the agent's extension is registered`;
}

// end button
const hasParticipants = Object.keys(participants).length > 1;
endElm.disabled = !hasParticipants;

// hold/resume call
const isHold = media && media[mediaResourceId] && media[mediaResourceId].isHold;
holdResumeElm.disabled = isTerminated;
holdResumeElm.innerText = isHold ? 'Resume' : 'Hold';

if (callProcessingDetails) {
const { pauseResumeEnabled, isPaused } = callProcessingDetails;

// pause/resume recording
pauseResumeRecordingElm.disabled = !pauseResumeEnabled;
pauseResumeRecordingElm.innerText = isPaused === 'true' ? 'Resume Recording' : 'Pause Recording';
}

// end consult, consult transfer buttons
const { consultMediaResourceId, destAgentId, destinationType } = data;
if (consultMediaResourceId && destAgentId && destinationType) {
const destination = participants[destAgentId];
destinationTypeDropdown.value = destinationType;
consultDestinationInput.value = destination.dn;

consultTabBtn.style.display = 'none';
endConsultBtn.style.display = 'inline-block';
consultTransferBtn.style.display = 'inline-block';
}
}

function populateWrapupCodesDropdown() {
wrapupCodesDropdownElm.innerHTML = ''; // Clear previous options
wrapupCodes.forEach((code) => {
Expand Down
9 changes: 7 additions & 2 deletions packages/@webex/plugin-cc/src/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,17 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
this.trigger(TASK_EVENTS.TASK_INCOMING, task);
};

private handleTaskHydrate = (task: ITask) => {
// @ts-ignore
this.trigger(TASK_EVENTS.TASK_HYDRATE, task);
};

/**
* An Incoming Call listener.
*/
private incomingTaskListener() {
this.taskManager.on(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask);
this.taskManager.on(TASK_EVENTS.TASK_HYDRATE, this.handleTaskHydrate);
}

/**
Expand Down Expand Up @@ -197,7 +203,6 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter

this.services.webSocketManager.on('message', this.handleWebSocketMessage);
this.incomingTaskListener();
this.taskManager.registerIncomingCallEvent();

return loginResponse;
} catch (error) {
Expand Down Expand Up @@ -225,6 +230,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter

this.taskManager.unregisterIncomingCallEvent();
this.taskManager.off(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask);
this.taskManager.off(TASK_EVENTS.TASK_HYDRATE, this.handleTaskHydrate);
this.services.webSocketManager.off('message', this.handleWebSocketMessage);

return logoutResponse;
Expand Down Expand Up @@ -352,7 +358,6 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter

// To handle re-registration of event listeners on silent relogin
this.incomingTaskListener();
this.taskManager.registerIncomingCallEvent();

if (lastStateChangeReason === 'agent-wss-disconnect') {
LoggerProxy.info(
Expand Down
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_CONTACT: 'AgentContact',
} as const;

export type WelcomeEvent = {
Expand Down
5 changes: 5 additions & 0 deletions packages/@webex/plugin-cc/src/services/task/TaskManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export default class TaskManager extends EventEmitter {
const payload = JSON.parse(event);
if (payload.data) {
switch (payload.data.type) {
case CC_EVENTS.AGENT_CONTACT:
this.currentTask = new Task(this.contact, this.webCallingService, payload.data);
this.taskCollection[payload.data.interactionId] = this.currentTask;
this.emit(TASK_EVENTS.TASK_HYDRATE, this.currentTask);
break;
case CC_EVENTS.AGENT_CONTACT_RESERVED:
this.currentTask = new Task(this.contact, this.webCallingService, payload.data);
this.currentTask.data = {...this.currentTask.data, isConsulted: false}; // Ensure isConsulted prop exists
Expand Down
1 change: 1 addition & 0 deletions packages/@webex/plugin-cc/src/services/task/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const TASK_EVENTS = {
TASK_END: 'task:end',
TASK_WRAPUP: 'task:wrapup',
TASK_REJECT: 'task:rejected',
TASK_HYDRATE: 'task:hydrate',
} as const;

export type TASK_EVENTS = Enum<typeof TASK_EVENTS>;
Expand Down
71 changes: 38 additions & 33 deletions packages/@webex/plugin-cc/test/unit/spec/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ import {CC_FILE, AGENT_STATE_CHANGE, AGENT_MULTI_LOGIN} from '../../../src/const
import '../../../__mocks__/workerMock';
import {Profile} from '../../../src/services/config/types';
import TaskManager from '../../../src/services/task/TaskManager';
import { TASK_EVENTS } from '../../../src/services/task/types';

import {TASK_EVENTS} from '../../../src/services/task/types';

jest.mock('../../../src/logger-proxy', () => ({
__esModule: true,
Expand All @@ -47,7 +46,6 @@ describe('webex.cc', () => {
let mockTaskManager;
let mockWebSocketManager;


beforeEach(() => {
webex = MockWebex({
children: {
Expand Down Expand Up @@ -85,7 +83,7 @@ describe('webex.cc', () => {
end: jest.fn(),
wrapup: jest.fn(),
cancelTask: jest.fn(),
cancelCtq: jest.fn()
cancelCtq: jest.fn(),
};

// Mock Services instance
Expand All @@ -104,7 +102,7 @@ describe('webex.cc', () => {
connectionService: {
on: jest.fn(),
},
contact: mockContact
contact: mockContact,
};

mockTaskManager = {
Expand All @@ -121,8 +119,8 @@ describe('webex.cc', () => {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
unregisterIncomingCallEvent: jest.fn()
}
unregisterIncomingCallEvent: jest.fn(),
};

jest.spyOn(Services, 'getInstance').mockReturnValue(mockServicesInstance);
jest.spyOn(TaskManager, 'getTaskManager').mockReturnValue(mockTaskManager);
Expand Down Expand Up @@ -338,10 +336,10 @@ describe('webex.cc', () => {
},
});
expect(configSpy).toHaveBeenCalled();
expect(LoggerProxy.log).toHaveBeenCalledWith(
'agent config is fetched successfully',
{module: CC_FILE, method: 'mockConstructor'}
);
expect(LoggerProxy.log).toHaveBeenCalledWith('agent config is fetched successfully', {
module: CC_FILE,
method: 'mockConstructor',
});
expect(reloadSpy).not.toHaveBeenCalled();
expect(result).toEqual(mockAgentProfile);
});
Expand Down Expand Up @@ -410,32 +408,28 @@ describe('webex.cc', () => {

expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, mockTask);
// Verify message event listener
const messageCallback = mockWebSocketManager.on.mock.calls.find(call => call[0] === 'message')[1];
const messageCallback = mockWebSocketManager.on.mock.calls.find(
(call) => call[0] === 'message'
)[1];
const agentStateChangeEventData = {
type: CC_EVENTS.AGENT_STATE_CHANGE,
data: { some: 'data' },
data: {some: 'data'},
};

const agentMultiLoginEventData = {
type: CC_EVENTS.AGENT_MULTI_LOGIN,
data: {some: 'data'},
}
};

// Simulate receiving a message event
messageCallback(JSON.stringify(agentStateChangeEventData));

expect(ccEmitSpy).toHaveBeenCalledWith(
AGENT_STATE_CHANGE,
agentStateChangeEventData.data
);
expect(ccEmitSpy).toHaveBeenCalledWith(AGENT_STATE_CHANGE, agentStateChangeEventData.data);

// Simulate receiving a message event
messageCallback(JSON.stringify(agentMultiLoginEventData));

expect(ccEmitSpy).toHaveBeenCalledWith(
AGENT_MULTI_LOGIN,
agentMultiLoginEventData.data
)
expect(ccEmitSpy).toHaveBeenCalledWith(AGENT_MULTI_LOGIN, agentMultiLoginEventData.data);
});

it('should login successfully with other LoginOption', async () => {
Expand Down Expand Up @@ -507,7 +501,14 @@ describe('webex.cc', () => {

expect(stationLogoutMock).toHaveBeenCalledWith({data: data});
expect(mockTaskManager.unregisterIncomingCallEvent).toHaveBeenCalledWith();
expect(mockTaskManager.off).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.any(Function));
expect(mockTaskManager.off).toHaveBeenCalledWith(
TASK_EVENTS.TASK_INCOMING,
expect.any(Function)
);
expect(mockTaskManager.off).toHaveBeenCalledWith(
TASK_EVENTS.TASK_HYDRATE,
expect.any(Function)
);
expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
expect(result).toEqual(response);
});
Expand Down Expand Up @@ -567,6 +568,15 @@ describe('webex.cc', () => {
{module: CC_FILE, method: 'stationReLogin'}
);
});

it('should trigger TASK_HYDRATE event with the task', () => {
const task = {id: 'task1'};
const triggerSpy = jest.spyOn(webex.cc, 'trigger');

webex.cc['handleTaskHydrate'](task);

expect(triggerSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_HYDRATE, task);
});
});

describe('setAgentStatus', () => {
Expand Down Expand Up @@ -778,10 +788,6 @@ describe('webex.cc', () => {
webex.cc.webCallingService,
'registerWebCallingLine'
);
const registerIncomingCallEventSpy = jest.spyOn(
webex.cc.taskManager,
'registerIncomingCallEvent'
);
const incomingTaskListenerSpy = jest.spyOn(webex.cc, 'incomingTaskListener');
const webSocketManagerOnSpy = jest.spyOn(webex.cc.services.webSocketManager, 'on');
await webex.cc['silentRelogin']();
Expand All @@ -801,9 +807,12 @@ describe('webex.cc', () => {
expect(webex.cc.agentConfig.lastStateChangeTimestamp).toStrictEqual(date); // it should be updated with the new timestamp of setAgentState response
expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.BROWSER);
expect(registerWebCallingLineSpy).toHaveBeenCalled();
expect(registerIncomingCallEventSpy).toHaveBeenCalled();
expect(incomingTaskListenerSpy).toHaveBeenCalled();
expect(webSocketManagerOnSpy).toHaveBeenCalledWith('message', expect.any(Function));
expect(mockTaskManager.on).toHaveBeenCalledWith(
TASK_EVENTS.TASK_HYDRATE,
expect.any(Function)
);
});

it('should handle AGENT_NOT_FOUND error silently', async () => {
Expand Down Expand Up @@ -902,11 +911,7 @@ describe('webex.cc', () => {
it('should set up connectionLost and message event listener', () => {
webex.cc.setupEventListeners();

expect(connectionServiceOnSpy).toHaveBeenCalledWith(
'connectionLost',
expect.any(Function)
);
expect(connectionServiceOnSpy).toHaveBeenCalledWith('connectionLost', expect.any(Function));
});
});

});
Loading

0 comments on commit dd98dc6

Please sign in to comment.