Skip to content

Commit

Permalink
Spark 608448 ReLogin agent state timer (#4080)
Browse files Browse the repository at this point in the history
  • Loading branch information
rsarika authored Feb 6, 2025
1 parent 0c5a54e commit 88030b9
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 32 deletions.
46 changes: 43 additions & 3 deletions 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 stateTimer;

const authTypeElm = document.querySelector('#auth-type');
const credentialsFormElm = document.querySelector('#credentials');
Expand Down Expand Up @@ -58,6 +59,7 @@ const initiateConsultDialog = document.querySelector('#initiate-consult-dialog')
const agentMultiLoginAlert = document.querySelector('#agentMultiLoginAlert');
const consultTransferBtn = document.querySelector('#consult-transfer');
const transferElm = document.getElementById('transfer');
const timerElm = document.querySelector('#timerDisplay');

// Store and Grab `access-token` from sessionStorage
if (sessionStorage.getItem('date') > new Date().getTime()) {
Expand Down Expand Up @@ -532,6 +534,25 @@ function initWebex(e) {

credentialsFormElm.addEventListener('submit', initWebex);

function startStateTimer(startTime) {
if (stateTimer) {
clearInterval(stateTimer);
}

stateTimer = setInterval(() => {
const currentTime = new Date().getTime();
const timeDifference = currentTime - startTime;

const hours = String(Math.floor(timeDifference / (1000 * 60 * 60))).padStart(2, '0');
const minutes = String(Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60))).padStart(2, '0');
const seconds = String(Math.floor((timeDifference % (1000 * 60)) / 1000)).padStart(2, '0');
if(timerElm)
{
timerElm.innerHTML = `${hours}:${minutes}:${seconds}`;
}
}, 1000);
}


function register() {
webex.cc.register(true).then((agentProfile) => {
Expand Down Expand Up @@ -575,6 +596,11 @@ function register() {
const option = document.createElement('option');
option.text = idleCodes.name;
option.value = idleCodes.id;
if (agentProfile.lastStateAuxCodeId && agentProfile.lastStateAuxCodeId === idleCodes.id)
{
option.selected = true;
startStateTimer(agentProfile.lastStateChangeTimestamp)
}
idleCodesDropdown.add(option);
}
});
Expand All @@ -592,6 +618,7 @@ function register() {
if (data && typeof data === 'object' && data.type === 'AgentStateChangeSuccess') {
const DEFAULT_CODE = '0'; // Default code when no aux code is present
idleCodesDropdown.value = data.auxCodeId?.trim() !== '' ? data.auxCodeId : DEFAULT_CODE;
startStateTimer(data.lastStateChangeTimestamp);
}
});

Expand Down Expand Up @@ -627,16 +654,29 @@ async function handleAgentLogin(e) {
}

function doAgentLogin() {
webex.cc.stationLogin({teamId: teamsDropdown.value, loginOption: agentDeviceType, dialNumber: dialNumber.value}).then((response) => {
webex.cc.stationLogin({
teamId: teamsDropdown.value,
loginOption: agentDeviceType,
dialNumber: dialNumber.value
}).then((response) => {
console.log('Agent Logged in successfully', response);
loginAgentElm.disabled = true;
logoutAgentElm.classList.remove('hidden');
}
).catch((error) => {

// Read auxCode and lastStateChangeTimestamp from login response
const DEFAULT_CODE = '0'; // Default code when no aux code is present
const auxCodeId = response.data.auxCodeId?.trim() !== '' ? response.data.auxCodeId : DEFAULT_CODE;
const lastStateChangeTimestamp = response.data.lastStateChangeTimestamp;
const index = [...idleCodesDropdown.options].findIndex(option => option.value === auxCodeId);
idleCodesDropdown.selectedIndex = index !== -1 ? index : 0;
startStateTimer(new Date(lastStateChangeTimestamp));

}).catch((error) => {
console.log('Agent Login failed', error);
});
}


async function handleAgentStatus(event) {
auxCodeId = event.target.value;
agentStatus = idleCodesDropdown.options[idleCodesDropdown.selectedIndex].text;
Expand Down
5 changes: 4 additions & 1 deletion docs/samples/contact-center/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,16 @@ <h2 class="collapsible">
<button id="loginAgent" disabled class="btn btn-primary my-3" onclick="doAgentLogin()">Login</button>
<button id="logoutAgent" class="btn btn-primary my-3 ml-2 hidden" onclick="logoutAgent()">Logout</button>
</fieldset>
<fieldset>
<fieldset style="display: flex; flex-direction: column; align-items: flex-start; gap: 10px;">
<legend>Agent status</legend>
<select name= "idleCodesDropdown" id="idleCodesDropdown" class="form-control w-auto my-3" onchange="handleAgentStatus(event)">
<option value="" selected hidden>Select Idle Codes</option>
</select>
<button id="setAgentStatus" disabled class="btn btn-primary my-3 ml-2" onclick="setAgentStatus()">Set Agent
Status</button>
<div class="timer-container my-3">
<span id="timerDisplay">00:00:00</span>
</div>
</fieldset>
</div>
<fieldset id="buddyAgentsBox">
Expand Down
29 changes: 24 additions & 5 deletions packages/@webex/plugin-cc/src/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import {AGENT, WEB_RTC_PREFIX} from './services/constants';
import Services from './services';
import HttpRequest from './services/core/HttpRequest';
import LoggerProxy from './logger-proxy';
import {StateChange, Logout} from './services/agent/types';
import {StateChange, Logout, StateChangeSuccess} from './services/agent/types';
import {getErrorDetails} from './services/core/Utils';
import {Profile, WelcomeEvent, CC_EVENTS} from './services/config/types';
import {AGENT_STATE_AVAILABLE} from './services/config/constants';
import {AGENT_STATE_AVAILABLE, AGENT_STATE_AVAILABLE_ID} from './services/config/constants';
import {ConnectionLostDetails} from './services/core/websocket/types';
import TaskManager from './services/task/TaskManager';
import WebCallingService from './services/WebCallingService';
Expand Down Expand Up @@ -343,7 +343,12 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
private async silentRelogin(): Promise<void> {
try {
const reLoginResponse = await this.services.agent.reload();
const {auxCodeId, agentId, lastStateChangeReason, deviceType, dn} = reLoginResponse.data;
const {agentId, lastStateChangeReason, deviceType, dn, lastStateChangeTimestamp} =
reLoginResponse.data;
let {auxCodeId} = reLoginResponse.data;
this.agentConfig.lastStateChangeTimestamp = lastStateChangeTimestamp
? new Date(lastStateChangeTimestamp)
: new Date();

// To handle re-registration of event listeners on silent relogin
this.incomingTaskListener();
Expand All @@ -354,15 +359,29 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
'event=requestAutoStateChange | Requesting state change to available on socket reconnect',
{module: CC_FILE, method: this.silentRelogin.name}
);
auxCodeId = AGENT_STATE_AVAILABLE_ID;
const stateChangeData: StateChange = {
state: AGENT_STATE_AVAILABLE,
auxCodeId,
lastStateChangeReason,
agentId,
};
await this.setAgentState(stateChangeData);
try {
const agentStatusResponse = (await this.setAgentState(
stateChangeData
)) as StateChangeSuccess;
this.agentConfig.lastStateChangeTimestamp = agentStatusResponse.data
.lastStateChangeTimestamp
? new Date(agentStatusResponse.data.lastStateChangeTimestamp)
: new Date();
} catch (error) {
LoggerProxy.error(
`event=requestAutoStateChange | Error requesting state change to available on socket reconnect: ${error}`,
{module: CC_FILE, method: this.silentRelogin.name}
);
}
}

this.agentConfig.lastStateAuxCodeId = auxCodeId;
await this.handleDeviceType(deviceType as LoginOption, dn);
this.agentConfig.isAgentLoggedIn = true;
this.services.webSocketManager.on('message', this.handleWebSocketMessage);
Expand Down
2 changes: 1 addition & 1 deletion packages/@webex/plugin-cc/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {LOGGER} from '@webex/calling';

export default {
cc: {
allowMultiLogin: false,
allowMultiLogin: true,
allowAutomatedRelogin: true,
clientType: 'WebexCCSDK',
isKeepAliveEnabled: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/@webex/plugin-cc/src/services/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,4 +565,6 @@ export type Profile = {
lostConnectionRecoveryTimeout: number;
maskSensitiveData?: boolean;
isAgentLoggedIn?: boolean;
lastStateAuxCodeId?: string;
lastStateChangeTimestamp?: Date;
};
50 changes: 28 additions & 22 deletions packages/@webex/plugin-cc/test/unit/spec/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ describe('webex.cc', () => {
force: true,
isKeepAliveEnabled: false,
clientType: 'WebexCCSDK',
allowMultiLogin: false,
allowMultiLogin: true,
},
});
expect(configSpy).toHaveBeenCalled();
Expand Down Expand Up @@ -398,18 +398,18 @@ describe('webex.cc', () => {
},
});
expect(result).toEqual({});

const onSpy = jest.spyOn(mockTaskManager, 'on');
const emitSpy = jest.spyOn(webex.cc, 'trigger');
const ccEmitSpy = jest.spyOn(webex.cc, 'emit');
const incomingCallCb = onSpy.mock.calls[0][1];

expect(onSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, incomingCallCb);

incomingCallCb(mockTask);

expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, mockTask);
// Verify message event listener
// Verify message event listener
const messageCallback = mockWebSocketManager.on.mock.calls.find(call => call[0] === 'message')[1];
const agentStateChangeEventData = {
type: CC_EVENTS.AGENT_STATE_CHANGE,
Expand Down Expand Up @@ -755,6 +755,7 @@ describe('webex.cc', () => {
auxCodeId: 'auxCodeId',
agentId: 'agentId',
lastStateChangeReason: 'agent-wss-disconnect',
lastStateChangeTimestamp: 1738575135188,
deviceType: LoginOption.BROWSER,
dn: '12345',
},
Expand All @@ -767,9 +768,10 @@ describe('webex.cc', () => {
isAgentLoggedIn: false,
} as Profile;

const setAgentStateSpy = jest
.spyOn(webex.cc, 'setAgentState')
.mockResolvedValue({} as SetStateResponse);
const date = new Date();
const setAgentStateSpy = jest.spyOn(webex.cc, 'setAgentState').mockResolvedValue({
data: {lastStateChangeTimestamp: date.getTime()},
} as unknown as SetStateResponse);
jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue(mockReLoginResponse);

const registerWebCallingLineSpy = jest.spyOn(
Expand All @@ -790,11 +792,13 @@ describe('webex.cc', () => {
);
expect(setAgentStateSpy).toHaveBeenCalledWith({
state: 'Available',
auxCodeId: 'auxCodeId',
auxCodeId: '0', // even if get auxcodeId from relogin response, it should be 0 for available state
lastStateChangeReason: 'agent-wss-disconnect',
agentId: 'agentId',
});
expect(webex.cc.agentConfig.isAgentLoggedIn).toBe(true);
expect(webex.cc.agentConfig.lastStateAuxCodeId).toBe('0');
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();
Expand All @@ -819,42 +823,44 @@ describe('webex.cc', () => {
{module: CC_FILE, method: 'silentRelogin'}
);
});

it('should handle errors during silent relogin', async () => {
const error = new Error('Error while performing silentReLogin');
jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error);

await expect(webex.cc['silentRelogin']()).rejects.toThrow(error);
});

it('should update agentConfig with deviceType during silent relogin for EXTENSION', async () => {
const mockReLoginResponse = {
data: {
auxCodeId: 'auxCodeId',
agentId: 'agentId',
lastStateChangeReason: 'agent-wss-disconnect',
deviceType: LoginOption.EXTENSION,
dn: '12345',
lastStateChangeTimestamp: 1738575135188,
},
};

// Mock the agentConfig
webex.cc.agentConfig = {
agentId: 'agentId',
agentProfileID: 'test-agent-profile-id',
isAgentLoggedIn: false,
} as Profile;

const registerWebCallingLineSpy = jest.spyOn(
webex.cc.webCallingService,
'registerWebCallingLine'
);
jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue(mockReLoginResponse);

await webex.cc['silentRelogin']();

expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.EXTENSION);
expect(webex.cc.agentConfig.defaultDn).toBe('12345');
expect(webex.cc.agentConfig.lastStateAuxCodeId).toBe('auxCodeId');
expect(webex.cc.agentConfig.lastStateChangeTimestamp).toStrictEqual(new Date(1738575135188));
});

it('should update agentConfig with deviceType during silent relogin for AGENT_DN', async () => {
Expand All @@ -868,18 +874,18 @@ describe('webex.cc', () => {
subStatus: 'subStatusValue',
},
};

// Mock the agentConfig
webex.cc.agentConfig = {
agentId: 'agentId',
agentProfileID: 'test-agent-profile-id',
isAgentLoggedIn: false,
} as Profile;

jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue(mockReLoginResponse);

await webex.cc['silentRelogin']();

expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.AGENT_DN);
expect(webex.cc.agentConfig.defaultDn).toBe('67890');
});
Expand Down

0 comments on commit 88030b9

Please sign in to comment.