Skip to content

Instantly share code, notes, and snippets.

@boblail
Created May 5, 2020 19:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boblail/f39109828d40583fa2228f702307cc81 to your computer and use it in GitHub Desktop.
Save boblail/f39109828d40583fa2228f702307cc81 to your computer and use it in GitHub Desktop.
Amazon's Angular app for v1 of the CCP
(function() {
angular
.module("ccpModule", ["ngRoute", "ngAnimate", "ngResource"])
.config(function($routeProvider) {
$routeProvider
.when("/", {
templateUrl: "index.html",
controller: "MainController"
})
.when("/dialPad", {
templateUrl: "dial-pad.html",
controller: "DialPadController"
})
.when("/contacts", {
templateUrl: "contact-selector.html",
controller: "ContactController"
})
.when("/changeStatus", {
templateUrl: "state-selector.html",
controller: "StateController"
})
.when("/setting", {
templateUrl: "setting.html",
controller: "SettingController"
})
.when("/report", {
templateUrl: "report-issue.html",
controller: "ReportController"
})
.when("/locale", {
templateUrl: "locale.html",
controller: "LocalizationController"
});
})
.controller("CCPController", function(
$scope,
$window,
$interval,
$timeout,
utils,
errorHandler,
LocaleService,
phoneNumberFormatterFactory,
initParams,
CCPInitService,
MetricListener,
WorkFlowFactory,
DISMISSIBLE_ERROR_TYPE,
BEEP_AUDIO,
CCP_UI_STATES,
CCP_STATE_NAMES,
CCP_PAGE_SITE,
AGENT_PHONE_TYPE,
CALL_ERRORS,
AGENT_STATE_TYPE,
CCP_STRINGS,
MASTER_TOPICS,
CCP_ICONS
) {
var self = this;
/* Private members */
angular.extend(self, {
timerRefresh: null,
thirdPartyTimerRefresh: null,
INIT_THRESHOLD_MS: 60000, // The number of milliseconds we are willing to wait before declaring that initialization has failed.
QUEUE_CALLBACK_ERROR_STATE: "Failed callback" /* TODO: This really really needs to be localized! */,
lastAgentStatus: null,
workflow: null,
uiStateToCssClass: (function() {
// TODO: use bracket notation for dynamic key. Update to jshint2 so that build will not fail.
// Refer to http://stackoverflow.com/questions/19837916/creating-object-with-dynamic-keys
var map = {};
map[CCP_UI_STATES.OFFLINE] = "offline";
map[CCP_UI_STATES.ACW] = "offline";
map[CCP_UI_STATES.AUX] = "offline";
map[CCP_UI_STATES.AVAILABLE] = "available";
map[CCP_UI_STATES.CONNECTED] = "connected";
map[CCP_UI_STATES.ERROR] = "error";
map[CCP_UI_STATES.ON_HOLD] = "onhold";
map[CCP_UI_STATES.OUTBOUND] = "outbound";
map[CCP_UI_STATES.INBOUND] = "inbound";
map[CCP_UI_STATES.CONNECTING] = "inbound";
map[CCP_UI_STATES.INCOMING] = "incoming";
map[CCP_UI_STATES.MISSED] = "missed";
map[CCP_UI_STATES.INITIALIZING] = "offline";
map[CCP_UI_STATES.DISCONNECTED] = "disconnected";
map[CCP_UI_STATES.AGENT_HOLD] = "onhold";
map[CCP_UI_STATES.MONITORING] = "monitoring";
return map;
})(),
ccpStateToQcbState: (function() {
var map = {};
map[CCP_STATE_NAMES.INCOMING] = CCP_STATE_NAMES.INCOMING_CALLBACK;
map[CCP_STATE_NAMES.OUTBOUND] = CCP_STATE_NAMES.CONNECTING;
return map;
})(),
setCSMWorkflowEvent: function(eventName, data, dedupe) {
if (self.workflow !== null) {
if (dedupe) {
// Since StreamJS publishes multiple events for a single agent snapshot update, we want to deduplicate these events
// However, we don't want to deduplicate events that are triggered by the user, like clicking a dial multiple times, since it's useful to learn about user behavior.
var DEDUP_TIME_MS = 500;
self.workflow.eventWithDedup(eventName, data, DEDUP_TIME_MS);
} else {
self.workflow.event(eventName, data);
}
}
},
setCcpErrorState: function(name, errorTextToUser, errorTextToAdmin, helpUrlText, helpUrl) {
$scope.ccpState.uiState = CCP_UI_STATES.ERROR;
$scope.ccpState.name = name;
if ($scope.isQueueCallBack()) {
$scope.ccpState.name = this.QUEUE_CALLBACK_ERROR_STATE;
}
if (errorTextToUser || errorTextToAdmin) {
$scope.ccpState.errorDetails.errorTextToUser = errorTextToUser;
$scope.ccpState.errorDetails.errorTextToAdmin = errorTextToAdmin;
if (helpUrlText && helpUrl) {
$scope.ccpState.errorDetails.helpUrlText = helpUrlText;
$scope.ccpState.errorDetails.helpUrl = helpUrl;
}
}
var contact = utils.getVoiceContact();
if (contact !== null) {
var initialConn = contact.getInitialConnection();
var thirdPartyConn = contact.getSingleActiveThirdPartyConnection();
if (initialConn) {
if (initialConn.getType() !== lily.ConnectionType.OUTBOUND) {
$scope.ccpState.customerNumber = utils.getContactNumber();
}
} else if (thirdPartyConn) {
$scope.ccpState.customerNumber = utils.getThirdPartyContactNumber();
} else {
$scope.ccpState.customerNumber = "";
}
self.setCSMWorkflowEvent($scope.ccpState.name, null, true);
} else {
$scope.ccpState.customerNumber = "";
}
},
setCcpSubState: function(name, icon) {
self.clearCcpSubState();
$scope.ccpState.subState.name = name;
$scope.ccpState.subState.icon = icon;
},
clearCcpSubState: function() {
$scope.ccpState.subState = {};
},
setCcpState: function(uiState, name) {
if ($scope.ccpState.uiState === CCP_UI_STATES.INITIALIZING) {
// When this function is called and the last uiState is Initializing, CCP just finished initialization.
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API
// https://www.w3.org/TR/navigation-timing/
var now = new Date().getTime();
if (window.performance.timing.loadEventEnd) {
var initTime = now - window.performance.timing.loadEventEnd;
csm.API.addTime("Initialization", initTime);
} else {
document.addEventListener("load", function(e) {
var initTime = now - window.performance.timing.loadEventEnd;
csm.API.addTime("Initialization", initTime);
});
}
}
$scope.ccpState.uiState = uiState;
$scope.ccpState.name = name;
$scope.ccpState.pending = false;
if ($scope.isQueueCallBack() && this.ccpStateToQcbState[name]) {
$scope.ccpState.name = this.ccpStateToQcbState[name];
}
$scope.errorDetails = {};
var contact = utils.getVoiceContact();
if (contact) {
var initialConn = contact.getActiveInitialConnection();
var thirdPartyConn = contact.getSingleActiveThirdPartyConnection();
var primaryConn = initialConn || thirdPartyConn;
if (primaryConn) {
if (primaryConn === initialConn) {
$scope.ccpState.customerNumber = utils.getContactNumber();
} else if (primaryConn === thirdPartyConn) {
$scope.ccpState.customerNumber = utils.getThirdPartyContactNumber();
} else {
$scope.ccpState.customerNumber = "";
}
}
self.setCSMWorkflowEvent($scope.ccpState.name, null, true);
} else {
$scope.ccpState.customerNumber = "";
}
},
setThirdPartyState: function(uiState, name) {
$scope.ccpState.thirdParty = {};
$scope.ccpState.thirdParty.uiState = uiState;
$scope.ccpState.thirdParty.name = name;
$scope.ccpState.thirdParty.contactName = utils.getThirdPartyContactNumber();
self.setCSMWorkflowEvent("ThirdParty-" + $scope.ccpState.thirdParty.name, null, true);
}
});
/* Models */
angular.extend($scope, {
agentExists: false,
ccpState: {
uiState: CCP_UI_STATES.INITIALIZING,
name: CCP_STATE_NAMES.INITIALIZING,
site: CCP_PAGE_SITE.MAIN,
stateDuration: 0,
thirdParty: {},
errorDetails: {},
pending: false
},
ccpScope: {}, // An object containing data to talk to children controllers.
CCP_UI_STATES: CCP_UI_STATES,
CCP_STATE_NAMES: CCP_STATE_NAMES,
CCP_PAGE_SITE: CCP_PAGE_SITE,
AGENT_PHONE_TYPE: AGENT_PHONE_TYPE
});
/* Methods */
angular.extend($scope, {
initCCP: function() {
lily.getLog().info("Initializing Contact Control Panel ...");
$timeout(function() {
if ($scope.ccpState.uiState === CCP_UI_STATES.INITIALIZING) {
// Initialization failed
$scope.ccpState.uiState = CCP_UI_STATES.ERROR;
$scope.ccpState.name = CCP_STATE_NAMES.INITIALIZATION_FAILURE;
csm.API.addError("Initialization");
} else {
csm.API.addSuccess("Initialization");
}
}, self.INIT_THRESHOLD_MS);
CCPInitService.initialize(initParams, {
success: function() {
csm.API.pageReady();
MetricListener.start();
},
failure: function(reason) {
lily.getLog().error("Initialization failed with the following reason: " + reason);
$scope.ccpState.uiState = CCP_UI_STATES.ERROR;
$scope.ccpState.name = CCP_STATE_NAMES.INITIALIZATION_FAILURE;
}
});
lily.agent(function(agent) {
$scope.agentExists = true;
// Fetch contacts only once when agent data is available.
$scope.setContacts();
self.timerRefresh = $interval(function() {
if ($scope.ccpState && $scope.ccpState.name) {
$scope.ccpState.stateDuration = utils.getStateDuration($scope.ccpState.name);
}
}, 500);
self.thirdPartyTimerRefresh = $interval(function() {
if ($scope.ccpState && $scope.ccpState.thirdParty) {
$scope.ccpState.thirdParty.stateDuration = utils.getThirdPartyStateDuration();
}
}, 500);
agent.onRefresh(function() {
$scope.agentState = agent.getStatus().name;
if ($scope.ccpState.uiState === CCP_UI_STATES.AUX || $scope.ccpState.uiState === CCP_UI_STATES.ACW) {
$scope.ccpState.name = $scope.agentState;
}
if (utils.getVoiceContact() === null && agent.getStatus().type === lily.AgentStatusType.ROUTABLE) {
self.setCcpState(CCP_UI_STATES.AVAILABLE, $scope.agentState);
}
$scope.agentStates = agent.getAgentStates();
$scope.permissions = lily.set(agent.getPermissions());
$scope.agentPhoneType = agent.isSoftphoneEnabled() ? AGENT_PHONE_TYPE.SOFT : AGENT_PHONE_TYPE.DESK;
$scope.agentExtension = phoneNumberFormatterFactory.getNumberWithoutCountryCode(agent.getExtension());
$scope.selectedCountry = phoneNumberFormatterFactory.getCountryForNumber(agent.getExtension());
$scope.dialableCountries = !agent.getDialableCountries()
? []
: phoneNumberFormatterFactory.getCountryCodeList(agent.getDialableCountries());
});
agent.onRoutable(function() {
self.lastAgentStatus = $scope.getCurrentAgentState();
self.setCcpState(CCP_UI_STATES.AVAILABLE, $scope.agentState);
});
agent.onNotRoutable(function() {
self.lastAgentStatus = $scope.getCurrentAgentState();
self.setCcpState(CCP_UI_STATES.AUX, $scope.agentState);
});
agent.onMuteToggle(function(obj) {
$scope.agentOnMute = obj.muted;
if ($scope.agentOnMute) {
self.setCcpSubState(CCP_STATE_NAMES.MUTED, CCP_ICONS.MUTED);
} else {
self.clearCcpSubState();
}
});
agent.onOffline(function() {
self.lastAgentStatus = $scope.getCurrentAgentState();
self.setCcpState(CCP_UI_STATES.OFFLINE, $scope.agentState);
});
agent.onAfterCallWork(function() {
self.setCcpState(CCP_UI_STATES.ACW, CCP_STATE_NAMES.ACW);
});
agent.onSoftphoneError(function(softPhoneError) {
//TODO urls to be updated later when documentation is ready and public
var message = "";
var errorType = softPhoneError.errorType;
var endPointUrl = softPhoneError.endPointUrl;
if (errorType === lily.SoftphoneErrorTypes.WEBRTC_ERROR) {
self.setCcpErrorState(
CALL_ERRORS[lily.AgentErrorStates.REALTIME_COMMUNICATION_ERROR],
CCP_STRINGS.SOFTPHONE_ERROR_MESSAGES.SOFTPHONE_CONNECTION_FAILED,
CCP_STRINGS.SOFTPHONE_ERROR_MESSAGES.WEBRTC_ERROR,
"",
""
);
} else {
if (errorType === lily.SoftphoneErrorTypes.ICE_COLLECTION_TIMEOUT) {
message = CCP_STRINGS.SOFTPHONE_ERROR_MESSAGES.MEDIA_CHANNEL_FAILED.replace("${url}", endPointUrl);
} else if (errorType === lily.SoftphoneErrorTypes.SIGNALLING_CONNECTION_FAILURE) {
message = CCP_STRINGS.SOFTPHONE_ERROR_MESSAGES.SIGNALLING_CHANNEL_FAILED.replace(
"${url}",
endPointUrl
);
} else if (errorType === lily.SoftphoneErrorTypes.SIGNALLING_HANDSHAKE_FAILURE) {
message = CCP_STRINGS.SOFTPHONE_ERROR_MESSAGES.SIGNALLING_HANDSHAKE_FAILED.replace(
"${url}",
endPointUrl
);
}
var errorDetails = errorHandler.getErrorDetailsByType(errorType);
if (errorDetails !== null) {
errorDetails.messageToAdmin = message;
$scope.dismissibleError = errorDetails;
}
}
});
agent.onError(function() {
var currentUiStateName = $scope.ccpState.name;
if (currentUiStateName === CALL_ERRORS[lily.AgentErrorStates.REALTIME_COMMUNICATION_ERROR]) {
//CCP UI state is already set From onSoftphoneError method.
return;
}
var agentStatus = agent.getStatus().name;
if (agentStatus === lily.AgentErrorStates.MISSED_CALL_AGENT) {
var metric = new csm.Metric("MissedCall", csm.UNIT.COUNT, 1);
metric.addDimension("Source", "agent.onError");
csm.API.addMetric(metric);
self.setCcpState(CCP_UI_STATES.MISSED, CALL_ERRORS[agentStatus]);
} else if (agentStatus === lily.AgentErrorStates.FAILED_CONNECT_CUSTOMER) {
self.setCcpState(CCP_UI_STATES.MISSED, CALL_ERRORS[agentStatus]);
$timeout(function() {
if (self.lastAgentStatus) {
lily
.getLog()
.info(
"Failed to connect to the customer. Reverting back to the last state: " +
JSON.stringify(self.lastAgentStatus)
);
$scope.changeAgentState(self.lastAgentStatus);
} else {
lily
.getLog()
.info(
"Failed to connect to the customer. Unable to determine the last state. Setting the agent to available"
);
$scope.setAvailable();
}
}, 3000);
} else if (agentStatus === lily.AgentErrorStates.FAILED_CONNECT_AGENT) {
if (self.lastAgentStatus) {
lily
.getLog()
.info(
"Failed to connect to the agent. Reverting back to the last state: " +
JSON.stringify(self.lastAgentStatus)
);
$scope.changeAgentState(self.lastAgentStatus);
} else {
lily
.getLog()
.info(
"Failed to connect to the agent. Unable to determine the last state. Setting the agent to offline."
);
$scope.changeAgentState($scope.getOfflineState());
}
}
//If a queue_callback workitem is missed, the agent will be in Default state.
else if (utils.getQueueCallBackContact() != null && agentStatus === lily.AgentErrorStates.DEFAULT) {
self.setCcpState(CCP_UI_STATES.MISSED, CALL_ERRORS[lily.AgentErrorStates.MISSED_QUEUE_CALLBACK]);
} else {
self.setCcpErrorState(CALL_ERRORS[agentStatus]);
}
});
});
lily.contact(function(contact) {
contact.notification = null;
$window.onbeforeunload = function(e) {
var dialogText = CCP_STRINGS.OTHERS.CLOSE_WINDOW_WARNING;
e.returnValue = dialogText;
return dialogText;
};
self.workflow = WorkFlowFactory.get(contact.getContactId());
contact.onConnecting(function() {
if (contact.isInbound()) {
self.setCcpState(CCP_UI_STATES.INBOUND, CCP_STATE_NAMES.INBOUND);
$scope.setupNotification(contact, CCP_STRINGS.NOTIFICATIONS.INCOMING_CALL_TITLE);
} else {
self.setCcpState(CCP_UI_STATES.OUTBOUND, CCP_STATE_NAMES.OUTBOUND);
}
});
/**
* Currently only used for queue_callback contact type. Voice directly goes to connecting status.
* If we have incoming events for other contact types in the future, we need to branch based
* on contact type.
*/
contact.onIncoming(function() {
self.setCcpState(CCP_UI_STATES.INCOMING, CCP_STATE_NAMES.INCOMING);
$scope.setupNotification(contact, CCP_STRINGS.NOTIFICATIONS.INCOMING_CALLBACK_TITLE);
});
contact.onMissed(function() {
var metric = new csm.Metric("MissedCall", csm.UNIT.COUNT, 1);
metric.addDimension("Source", "contact.onMissed");
csm.API.addMetric(metric);
self.setCcpState(CCP_UI_STATES.MISSED, CCP_STATE_NAMES.MISSED_CALL);
});
contact.onACW(function() {
self.setCcpState(CCP_UI_STATES.ACW, CCP_STATE_NAMES.ACW);
});
contact.onAccepted(function() {
self.setCcpState(CCP_UI_STATES.CONNECTING, CCP_STATE_NAMES.CONNECTING);
});
contact.onEnded(function() {
$scope.ccpState.thirdParty = {};
$window.onbeforeunload = null;
self.workflow = null;
});
/**
* TODO: We will have a timer to trigger the queue_callback outbound call during the pending state.
* For now, we just directly initiate the voice call after the incoming queue_callback request (as work_item)
* is accepted.
*/
//contact.onPending(pending);
contact.onConnected(function() {
if (contact.notification) {
contact.notification.close();
contact.notification = null;
}
var initialConn = contact.getActiveInitialConnection();
if (initialConn && initialConn.getType() === lily.ConnectionType.MONITORING) {
self.setCcpState(CCP_UI_STATES.MONITORING, CCP_STATE_NAMES.MONITORING);
} else {
self.setCcpState(CCP_UI_STATES.CONNECTED, CCP_STATE_NAMES.CONNECTED);
contact.onRefresh(function() {
$scope.ccpState.thirdParty = {};
if (contact.isConnected()) {
var agentConn = contact.getAgentConnection();
var initialConn = contact.getActiveInitialConnection();
var thirdPartyConn = contact.getSingleActiveThirdPartyConnection();
var primaryConn = initialConn || thirdPartyConn;
if (primaryConn) {
if (primaryConn.isConnected()) {
self.setCcpState(CCP_UI_STATES.CONNECTED, CCP_STATE_NAMES.CONNECTED);
} else if (primaryConn.isOnHold()) {
self.setCcpState(CCP_UI_STATES.ON_HOLD, CCP_STATE_NAMES.ON_HOLD);
} else if (primaryConn === thirdPartyConn && primaryConn.isConnecting()) {
self.setCcpState(CCP_UI_STATES.OUTBOUND, CCP_STATE_NAMES.OUTBOUND);
} else {
self.setCcpState(CCP_UI_STATES.DISCONNECTED, CCP_STATE_NAMES.DISCONNECTED);
}
if (initialConn && thirdPartyConn) {
if (thirdPartyConn.isConnecting()) {
self.setThirdPartyState(CCP_UI_STATES.OUTBOUND, CCP_STATE_NAMES.OUTBOUND);
} else if (initialConn.isConnected() && thirdPartyConn.isConnected()) {
if (agentConn.isOnHold()) {
self.setCcpState(CCP_UI_STATES.AGENT_HOLD, CCP_STATE_NAMES.AGENT_HOLD);
} else {
self.setThirdPartyState(CCP_UI_STATES.CONNECTED, CCP_STATE_NAMES.JOINED);
self.setCcpState(CCP_UI_STATES.CONNECTED, CCP_STATE_NAMES.JOINED);
}
} else if (thirdPartyConn.isConnected()) {
self.setThirdPartyState(CCP_UI_STATES.CONNECTED, CCP_STATE_NAMES.CONNECTED);
} else if (thirdPartyConn.isOnHold()) {
self.setThirdPartyState(CCP_UI_STATES.ON_HOLD, CCP_STATE_NAMES.ON_HOLD);
}
}
}
}
});
}
});
});
},
getStateClass: function(state) {
return self.uiStateToCssClass[state];
},
isChangeStatusEnabled: function() {
// *Always* allow the change state menu to be closed - see https://tt.amazon.com/0124905187 et al
if ($scope.ccpState.site === $scope.CCP_PAGE_SITE.SET_STATUS) {
return true;
}
if (
$scope.ccpState.uiState === CCP_UI_STATES.ERROR &&
$scope.ccpState.name === CCP_STATE_NAMES.INITIALIZATION_FAILURE
) {
return false;
}
var states = [
CCP_UI_STATES.CONNECTED,
CCP_UI_STATES.INBOUND,
CCP_UI_STATES.OUTBOUND,
CCP_UI_STATES.INCOMING,
CCP_UI_STATES.ON_HOLD
];
return !states.includes($scope.ccpState.uiState);
},
isContactDetailsEnabled: function() {
var states = [CCP_UI_STATES.ACW, CCP_UI_STATES.AUX, CCP_UI_STATES.MONITORING];
return (
states.includes($scope.ccpState.uiState) ||
$scope.isInIncomingState() ||
$scope.isInConnectingState() ||
$scope.isInConversationState()
);
},
isInThreeWayCall: function() {
var threeWayStates = [CCP_UI_STATES.CONNECTED, CCP_UI_STATES.OUTBOUND, CCP_UI_STATES.ON_HOLD];
return threeWayStates.includes($scope.ccpState.thirdParty.uiState);
},
clearDismissibleError: function(error) {
$scope.dismissibleError = undefined;
},
isInConversationState: function() {
var convStates = [CCP_UI_STATES.CONNECTED, CCP_UI_STATES.ON_HOLD];
return convStates.includes($scope.ccpState.uiState);
},
isInIncomingState: function() {
var connStates = [CCP_UI_STATES.INCOMING];
return connStates.includes($scope.ccpState.uiState);
},
isInConnectingState: function() {
var connStates = [CCP_UI_STATES.INBOUND, CCP_UI_STATES.OUTBOUND];
return connStates.includes($scope.ccpState.uiState);
},
isInNonConversationState: function() {
var connStates = [
CCP_UI_STATES.AVAILABLE,
CCP_UI_STATES.OFFLINE,
CCP_UI_STATES.AUX,
CCP_UI_STATES.ACW,
CCP_UI_STATES.MISSED
];
return connStates.includes($scope.ccpState.uiState);
},
isInNotRoutableState: function() {
var connStates = [
CCP_UI_STATES.OFFLINE,
CCP_UI_STATES.AUX,
CCP_UI_STATES.ACW,
CCP_UI_STATES.MISSED,
CCP_UI_STATES.ERROR
];
return connStates.includes($scope.ccpState.uiState);
},
isHandlingThirdParty: function() {
return utils.isHandlingThirdParty();
},
isAllHold: function() {
var contact = utils.getVoiceContact();
if (contact == null) {
return false;
}
var initialConn = contact.getInitialConnection();
var thirdPartyConn = contact.getSingleActiveThirdPartyConnection();
if (initialConn && thirdPartyConn) {
return initialConn.isOnHold() && thirdPartyConn.isOnHold();
} else {
return false;
}
},
isQueueCallBack: function() {
return utils.getQueueCallBackContact() != null;
},
getCurrentAgentState: function() {
var agent = new lily.Agent();
var state = agent.getState();
if (!state.agentStateArn) {
state = $scope.agentStates.filter(function(s) {
return s.name === state.name;
})[0];
}
return state;
},
changeAgentState: function(state) {
utils.setAgentState(state, {
success: function(data) {
window.location.href = "#/";
},
failure: function(data) {
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.CHANGE_STATE);
}
});
},
setAvailable: function() {
$scope.changeAgentState($scope.getAvailableState());
},
getAvailableState: function() {
var agent = new lily.Agent();
var state = $scope.agentStates.filter(function(st) {
return st.type === lily.AgentStatusType.ROUTABLE;
})[0];
return state;
},
getOfflineState: function() {
var agent = new lily.Agent();
var state = $scope.agentStates.filter(function(st) {
return st.type === lily.AgentStatusType.OFFLINE;
})[0];
return state;
},
/* Get the list of contacts */
setContacts: function() {
utils.getAddresses({
success: function(data) {
$scope.ccpScope.contacts = data;
$scope.ccpScope.displayContacts = data;
},
failure: function(err, data) {
lily
.getLog()
.error("Fetch Addresses failed!")
.withObject({ err: err, data: data });
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.GET_ADDRESSES);
}
});
},
setupNotification: function(contact, notificationTitle) {
lily.ifMaster(MASTER_TOPICS.NOTIFICATIONS, function() {
if (!contact.notification) {
var nm = lily.core.getNotificationManager();
var phoneNumber = contact
.getInitialConnection()
.getAddress()
.stripPhoneNumber();
contact.notification = nm.show(notificationTitle + phoneNumber, {
body: CCP_STRINGS.NOTIFICATIONS.INCOMING_CALL_BODY,
clicked: function() {
window.focus();
this.close();
}
});
}
});
},
playBeep: function() {
BEEP_AUDIO.play();
}
});
/* Call control functionality */
angular.extend($scope, {
dial: function(number) {
self.setCSMWorkflowEvent("Click dial");
self.lastAgentStatus = $scope.getCurrentAgentState();
$scope.ccpState.customerNumber = utils.dial(number, {
success: function() {
$scope.outboundDebounce = false;
},
failure: function(data) {
$scope.outboundDebounce = false;
var errorDetails;
if (data.includes("UNDIALABLE")) {
errorDetails = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.UNSUPPORTED_COUNTRY);
var countries = [];
$scope.dialableCountries.forEach(function(country) {
countries.push(country.name);
});
var message = errorDetails.messageToUser.replace("${countries}", countries.toString());
errorDetails.messageToUser = message;
} else if (data.includes("InvalidConfigurationException")) {
errorDetails = errorHandler.getErrorDetailsByType(
DISMISSIBLE_ERROR_TYPE.INVALID_OUTBOUND_CONFIGURATION
);
} else {
errorDetails = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.INVALID_NUMBER);
}
$scope.dismissibleError = errorDetails;
}
});
},
hangUpAgent: function() {
self.setCSMWorkflowEvent("Click hangUpAgent");
$scope.ccpState.pending = true;
utils
.getVoiceContact()
.getAgentConnection()
.destroy({
success: function() {
lily.getLog().info("Agent disconnected successfully.");
},
failure: function(reason) {
$scope.ccpState.pending = false;
lily.getLog().error("Failed hanging up agent due to " + reason);
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.HANG_UP);
}
});
},
hangUpCustomer: function() {
self.setCSMWorkflowEvent("Click hangUpCustomer");
$scope.ccpState.pending = true;
utils
.getVoiceContact()
.getInitialConnection()
.destroy({
success: function() {
lily.getLog().info("Customer hung up by agent.");
},
failure: function(reason) {
$scope.ccpState.pending = false;
lily.getLog().error("Failed hanging up customer due to " + reason);
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.DISCONNECT);
}
});
},
hangUpThirdParty: function() {
self.setCSMWorkflowEvent("Click hangUpThirdParty");
var thirdParty = utils.getVoiceContact().getSingleActiveThirdPartyConnection();
$scope.ccpState.pending = true;
if (!thirdParty) {
return;
}
thirdParty.destroy({
success: function() {
lily.getLog().info("Third party hung up.");
},
failure: function(reason) {
$scope.ccpState.pending = false;
lily.getLog().error("Failed hanging up third party due to " + reason);
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.DISCONNECT);
}
});
},
muteToggle: function(status) {
$scope.agentOnMute = status;
utils.muteToggle(status);
},
hold: function() {
self.setCSMWorkflowEvent("Click hold");
$scope.ccpState.pending = true;
utils.hold({
failure: function() {
$scope.ccpState.pending = false;
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.HOLD);
}
});
},
holdCustomer: function() {
self.setCSMWorkflowEvent("Click holdCustomer");
$scope.ccpState.pending = true;
utils.threeWayHoldImpl(true, {
failure: function() {
$scope.ccpState.pending = false;
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.HOLD);
}
});
},
holdThirdParty: function() {
self.setCSMWorkflowEvent("Click holdThirdParty");
$scope.ccpState.pending = true;
utils.threeWayHoldImpl(false, {
failure: function() {
$scope.ccpState.pending = false;
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.HOLD);
}
});
},
resume: function() {
self.setCSMWorkflowEvent("Click resume");
$scope.ccpState.pending = true;
// Resume based on the current context
// This edge case happens when disconnecting customer in a 3 way call, then hold 3rd party
// In this case, resume should resume 3rd party
utils.resume({
failure: function() {
$scope.ccpState.pending = false;
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.RESUME);
}
});
},
resumeCustomer: function() {
self.setCSMWorkflowEvent("Click resumeCustomer");
$scope.ccpState.pending = true;
utils.resumeCustomer({
failure: function() {
$scope.ccpState.pending = false;
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.RESUME);
}
});
},
resumeThirdParty: function() {
self.setCSMWorkflowEvent("Click resumeThirdParty");
$scope.ccpState.pending = true;
utils.resumeThirdParty({
failure: function() {
$scope.ccpState.pending = false;
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.RESUME);
}
});
},
acceptCall: function() {
self.setCSMWorkflowEvent("Click acceptCall");
$scope.ccpState.pending = true;
utils.acceptCall({
failure: function() {
$scope.ccpState.pending = false;
}
});
},
conference: function() {
self.setCSMWorkflowEvent("Click conference");
$scope.ccpState.pending = true;
utils.conference({
failure: function() {
$scope.ccpState.pending = false;
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.JOIN);
}
});
},
allHold: function() {
self.setCSMWorkflowEvent("Click allHold");
$scope.ccpState.pending = true;
utils.allHold({
failure: function() {
$scope.ccpState.pending = false;
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.HOLD);
}
});
},
swapCall: function() {
self.setCSMWorkflowEvent("Click swapCall");
$scope.ccpState.pending = true;
utils.swapCall({
failure: function() {
$scope.ccpState.pending = false;
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.SWAP);
}
});
},
transferToNumber: function(number) {
self.setCSMWorkflowEvent("Click transferToNumber");
utils.transferToNumber(number, {
success: function(data) {
$scope.outboundDebounce = false;
},
failure: function(data) {
$scope.outboundDebounce = false;
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.TRANSFER);
}
});
},
transferToDestination: function(destination) {
self.setCSMWorkflowEvent("Click transferToDestination");
var failure = function() {
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.TRANSFER);
};
if ($scope.isInConversationState()) {
if (destination.type === lily.AddressType.PHONE_NUMBER) {
utils.transferToNumber(destination.phoneNumber, {
failure: failure
});
} else {
utils.transferToDestination(destination, {
failure: failure
});
}
} else {
$scope.dial(destination.phoneNumber);
}
},
sendDigit: function(digit) {
self.setCSMWorkflowEvent("Click sendDigit");
// HACK START, don't sent digit when it's in ACW, due to Lily-Defect-1660
if ($scope.ccpState.uiState === CCP_UI_STATES.ACW) {
return;
}
// HACK END
utils.sendDigit(digit, {
failure: function(data) {
lily
.getLog()
.error("Failed to send digit " + digit)
.withData(data);
}
});
}
});
})
.controller("MainController", function($scope, utils, CCP_UI_STATES, CCP_STATE_NAMES, CCP_PAGE_SITE) {
$scope.ccpState.site = CCP_PAGE_SITE.MAIN;
angular.extend($scope, {
showAgentHangUpButton: function() {
var connStates = [
CCP_UI_STATES.CONNECTED,
CCP_UI_STATES.CONNECTING,
CCP_UI_STATES.ON_HOLD,
CCP_UI_STATES.OUTBOUND,
CCP_UI_STATES.AGENT_HOLD,
CCP_UI_STATES.MONITORING
];
return connStates.includes($scope.ccpState.uiState);
}
});
})
.controller("StateController", function($scope, utils, CCP_PAGE_SITE) {
$scope.ccpState.site = CCP_PAGE_SITE.SET_STATUS;
angular.extend($scope, {
logOut: function() {
utils.logout();
}
});
})
.controller("ContactController", function(
$scope,
$timeout,
utils,
CCP_UI_STATES,
CCP_STATE_NAMES,
CCP_PAGE_SITE,
ENTER_KEY_CODE
) {
$scope.ccpState.site = CCP_PAGE_SITE.OTHERS;
$timeout(function() {
$("#contactRealInput").focus();
$scope.setContacts();
});
angular.extend($scope, {
filterContacts: function() {
$scope.ccpScope.displayContacts = $scope.ccpScope.contacts.filter(function(contact) {
return contact.name.toLowerCase().includes($scope.input.toLowerCase());
});
},
isValidInput: function() {
return (
($scope.ccpScope.displayContacts && $scope.ccpScope.displayContacts.length === 1) ||
($scope.input && $scope.input.match(/^[0-9+][0-9]+$/) !== null)
);
},
dialOrTransfer: function() {
if ($scope.ccpScope.displayContacts && $scope.ccpScope.displayContacts.length === 1) {
if ($scope.isInConversationState()) {
$scope.transferToDestination($scope.ccpScope.displayContacts[0]);
} else {
$scope.dial($scope.ccpScope.displayContacts[0]);
}
return;
}
if ($scope.input && $scope.input.match("^[0-9+][0-9]+$")) {
if ($scope.isInConversationState()) {
$scope.transferToNumber($scope.input);
} else {
$scope.dial($scope.input);
}
}
},
onKeyPress: function(keyEvent) {
if (keyEvent.which === ENTER_KEY_CODE) {
$scope.dialOrTransfer();
}
}
});
})
.controller("DialPadController", function(
$scope,
$timeout,
utils,
DIAL_PAD_STRUCT,
CCP_UI_STATES,
CCP_STATE_NAMES,
CCP_PAGE_SITE,
ENTER_KEY_CODE,
STORAGE_KEYS,
defaultOutboundNumber
) {
$scope.ccpState.site = CCP_PAGE_SITE.OTHERS;
$timeout(function() {
$("#numberRealInput").focus();
});
var phoneUtil = libphonenumber.PhoneNumberUtil.getInstance();
var OUTBOUND_DEBOUNCE_TIMEOUT_MS = 5000;
angular.extend($scope, {
outboundNumber: "",
outboundDebounce: false,
DIAL_PAD_STRUCT: DIAL_PAD_STRUCT,
AGENT_DIALABLE_COUNTRY: $scope.dialableCountries,
onNumberButtonClick: function(button) {
$scope.playBeep();
// If the zero button was held until a plus was created, don't do anything when the button was released
if ($scope.holdingZeroCreatedPlus) {
$scope.holdingZeroCreatedPlus = false;
return;
}
// If the zero button is being held, but the timeout hasn't triggered yet, cancel it
if ($scope.holdingZeroTimeoutPromise) {
$timeout.cancel($scope.holdingZeroTimeoutPromise);
$scope.holdingZeroTimeoutPromise = null;
}
$scope.outboundNumber += button.number;
if (utils.isConnected()) {
$scope.sendDigit(button.number);
}
},
onNumberButtonKeyDown: function(button) {
// If the zero button is held down for a full second, put a plus in the outbound number
if (button.number === "0") {
$scope.holdingZeroTimeoutPromise = $timeout(function() {
$scope.outboundNumber += "+";
$scope.holdingZeroTimeoutPromise = null;
$scope.holdingZeroCreatedPlus = true;
}, 1000);
} else {
$scope.holdingZeroCreatedPlus = false;
}
},
onKeyPress: function(keyEvent) {
if (keyEvent.which === ENTER_KEY_CODE) {
var phoneNumber = $scope.getCurrentNumber();
if ($scope.isInConversationState()) {
$scope.transferToNumber(phoneNumber);
} else {
$scope.dial(phoneNumber);
}
}
},
dialNumber: function() {
var phoneNumber = $scope.getCurrentNumber();
$scope.outboundDebounce = true;
// Just in case the debounce isn't cleared by the API calls,
// we want to make sure the button is re-enabled eventually.
window.setTimeout(function() {
$scope.outboundDebounce = false;
}, OUTBOUND_DEBOUNCE_TIMEOUT_MS);
if ($scope.isInConversationState()) {
$scope.transferToNumber(phoneNumber);
} else {
$scope.dial(phoneNumber);
}
},
getCurrentNumber: function() {
if ($scope.outboundNumber.trim()) {
return $scope.selectedCountry.code + $scope.outboundNumber.trim();
} else {
return "";
}
},
numberEntered: function() {
if ($scope.outboundNumber) {
var digit = $scope.outboundNumber[$scope.outboundNumber.length - 1];
if (utils.isConnected()) {
$scope.sendDigit(digit);
}
}
},
selectCountryCode: function(cc) {
$scope.selectedCountry = cc;
$scope.showCountryDropdown = false;
$scope.adjustCallableCountryCss(cc);
localStorage.setItem(STORAGE_KEYS.LAST_USED_COUNTRY, cc.value);
},
getDefaultCountry: function() {
if ($scope.AGENT_DIALABLE_COUNTRY) {
var i;
// 1: Check local storage for the last selected outbound number, if valid
var lastUsedCountry = localStorage.getItem(STORAGE_KEYS.LAST_USED_COUNTRY);
for (i = $scope.AGENT_DIALABLE_COUNTRY.length - 1; i >= 0; i--) {
if ($scope.AGENT_DIALABLE_COUNTRY[i].value === lastUsedCountry) {
return $scope.AGENT_DIALABLE_COUNTRY[i];
}
}
// 2: Use the country on the profile's default outbound queue, if valid
var profileDefaultCountry;
try {
// Bypass phone number validation for Philippines number pattern migration.
// Old pattern: +632XXXXXXX, New pattern +632[3-8]XXXXXXX
// Remove newly added digit to get the region code from google-libphonenumber library
// TODO: remove this snippet after google-libphonenumber library's update is available
if (defaultOutboundNumber.match(/^\+632[3-8]\d{7}/) && defaultOutboundNumber.length === 12) {
defaultOutboundNumber = defaultOutboundNumber.substring(0, 4) + defaultOutboundNumber.substring(5);
}
profileDefaultCountry = phoneUtil.getRegionCodeForNumber(phoneUtil.parse(defaultOutboundNumber, ""));
} catch (err) {
lily
.getLog()
.warn(err.message)
.withException(err);
}
if (profileDefaultCountry) {
for (i = $scope.AGENT_DIALABLE_COUNTRY.length - 1; i >= 0; i--) {
if ($scope.AGENT_DIALABLE_COUNTRY[i].value.toLowerCase() === profileDefaultCountry.toLowerCase()) {
return $scope.AGENT_DIALABLE_COUNTRY[i];
}
}
}
// 3: Use the first dialable country
if ($scope.AGENT_DIALABLE_COUNTRY.length > 0) {
return $scope.AGENT_DIALABLE_COUNTRY[0];
}
}
return null;
},
adjustCallableCountryCss: function(cc) {
$("#DropDownIcon").removeClass("dropdown-icon-smaller dropdown-icon-smallest");
if (cc.code.length > 3) {
$("#CountryFlag").hide();
$("#DropDownIcon").addClass("dropdown-icon-smallest");
} else if (cc.code.length === 3) {
$("#CountryFlag").show();
$("#DropDownIcon").addClass("dropdown-icon-smaller");
} else {
$("#CountryFlag").show();
}
}
});
$scope.selectedCountry = $scope.getDefaultCountry();
if (!$scope.selectedCountry) {
$timeout(function() {
$scope.selectedCountry = $scope.getDefaultCountry();
});
}
$("#dialPadPanel").click(function(e) {
if (!$(e.target).is(".CountryCodeInput, .CountryCodeInput *")) {
$scope.$apply(function() {
$scope.showCountryDropdown = false;
});
}
});
})
.controller("SettingController", function(
$scope,
$timeout,
utils,
errorHandler,
phoneNumberFormatterFactory,
DISMISSIBLE_ERROR_TYPE,
CCP_PAGE_SITE,
AGENT_PHONE_TYPE,
LOCALE_NAME_MAP,
preferredLocale
) {
$scope.ccpState.site = CCP_PAGE_SITE.SETTINGS;
angular.extend($scope, {
AGENT_DIALABLE_COUNTRY: $scope.dialableCountries,
showCountryDropdown: false,
selectedLocale: LOCALE_NAME_MAP[preferredLocale],
downloadLog: function() {
lily.getLog().download();
},
setPhoneType: function() {
var useSoftphone = $scope.agentPhoneType === AGENT_PHONE_TYPE.SOFT;
// Used for toggling the save button
$scope.savingExtension = false;
if (!useSoftphone && !$scope.agentExtension) {
return;
}
var config = {};
config.softphoneEnabled = useSoftphone;
if ($scope.selectedCountry && $scope.selectedCountry.code && $scope.agentExtension) {
config.extension = $scope.selectedCountry.code + $scope.agentExtension;
}
if (useSoftphone || $scope.settingsForm.$valid) {
$scope.savingExtension = true;
utils.saveAgentExtensionConfig(config, {
failure: function(data) {
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.SET_CONFIG);
$scope.savingExtension = false;
},
success: function() {
$scope.savingExtension = false;
}
});
}
},
selectCountryCode: function(cc) {
$scope.selectedCountry = cc;
$scope.showCountryDropdown = false;
$scope.adjustCallableCountryCss(cc);
$scope.validatePhoneNumber();
},
validatePhoneNumber: function() {
// Delay to make sure the form elements are rendered and the country dropdown value is set
$timeout(function() {
$scope.settingsForm.agentExtension.$validate();
if (!$scope.settingsForm.$valid) {
//for the selected country if the phone number is not valid reset back to empty.
$scope.agentExtension = "";
}
}, 200);
},
toggleDropdown: function() {
$scope.showCountryDropdown = !$scope.showCountryDropdown;
},
adjustCallableCountryCss: function(cc) {
$("#DropDownIcon").removeClass("dropdown-icon-smaller dropdown-icon-smallest");
if (cc.code.length > 3) {
$("#CountryFlag").hide();
$("#DropDownIcon").addClass("dropdown-icon-smallest");
} else if (cc.code.length === 3) {
$("#CountryFlag").show();
$("#DropDownIcon").addClass("dropdown-icon-smaller");
} else {
$("#CountryFlag").show();
}
}
});
//Adding an explicit watcher to dialableCountries to make sure the country list gets populated always
$scope.$watch("dialableCountries", function() {
$scope.AGENT_DIALABLE_COUNTRY = $scope.dialableCountries;
});
$("#settingSelector").click(function(e) {
if (!$(e.target).is(".CountryCodeInput, .CountryCodeInput *")) {
$scope.$apply(function() {
$scope.showCountryDropdown = false;
});
}
});
})
.controller("ReportController", function(
$scope,
utils,
errorHandler,
DISMISSIBLE_ERROR_TYPE,
CCP_PAGE_SITE,
REPORT_REASONS,
NOTES_MAX_LENGTH
) {
$scope.ccpState.site = CCP_PAGE_SITE.OTHERS;
angular.extend($scope, {
REPORT_REASONS: REPORT_REASONS,
NOTES_MAX_LENGTH: NOTES_MAX_LENGTH,
submitStatus: null,
enforceNotesLength: function() {
if ($scope.notes.length > NOTES_MAX_LENGTH) {
$scope.notes = $scope.notes.substring(0, NOTES_MAX_LENGTH);
}
},
submit: function() {
utils.flagCall({
subject: "agent-flagged-call",
type: $scope.reason.type,
description: $scope.notes,
success: function() {
$scope.submitStatus = "Submit report succeeded.";
},
failure: function(data) {
lily
.getLog()
.error("Failed to submit the issue report.")
.withData(data);
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.REPORT);
}
});
}
});
})
.controller("LocalizationController", function(
$scope,
utils,
errorHandler,
preferredLocale,
LocaleService,
$window,
DISMISSIBLE_ERROR_TYPE,
CCP_PAGE_SITE,
REPORT_REASONS,
NOTES_MAX_LENGTH,
LOCALE_NAME_MAP,
localization
) {
$scope.ccpState.site = CCP_PAGE_SITE.OTHERS;
angular.extend($scope, {
selectedLocale: preferredLocale,
locale_name_map: LOCALE_NAME_MAP,
supportedLocales: [],
initializeLocale: function() {
LocaleService.getSupportedLocales(
{},
function success(data) {
$scope.supportedLocales = data.list;
},
function error(date) {}
);
},
setLocale: function(locale) {
var config = {};
config.agentPreferences = { locale: locale };
utils.saveAgentLocaleConfig(config, {
success: function(data) {
$window.location.href = "#/";
$window.location.reload();
},
failure: function(data) {
$scope.dismissibleError = errorHandler.getErrorDetailsByType(DISMISSIBLE_ERROR_TYPE.SET_LOCALE);
}
});
}
});
});
})();
(function() {
angular
.module("ccpModule")
.constant("CCP_UI_STATES", {
// State for UI transition
AVAILABLE: "AVAILABLE", //Green
OFFLINE: "OFFLINE", //Grey
AUX: "AUX", //Grey
ACW: "ACW", //Grey
CONNECTED: "CONNECTED", //Green
CONNECTING: "CONNECTING", //Green
ON_HOLD: "ON_HOLD", //Orange
OUTBOUND: "OUTBOUND", //Blue left to right gradient
INBOUND: "INBOUND", //Blue right to left gradient
INCOMING: "INCOMING",
MONITORING: "MONITORING", //Orange
ERROR: "ERROR", //Red
MISSED: "MISSED", //Yellow
INITIALIZING: "INITIALIZING", //Blue right to left gradient
DISCONNECTED: "DISCONNECTED", //Hidden
AGENT_HOLD: "AGENT_HOLD" //Orange
})
.constant("MASTER_TOPICS", {
NOTIFICATIONS: "NOTIFICATIONS"
})
.factory("CCP_ICONS", function(CCP_STRINGS) {
// State names for display
return {
MUTED: "mute-white"
};
})
.factory("CCP_STATE_NAMES", function(CCP_STRINGS) {
// State names for display
return {
CONNECTED: CCP_STRINGS.STATE_NAMES.CONNECTED,
CONNECTING: CCP_STRINGS.STATE_NAMES.CONNECTING,
ON_HOLD: CCP_STRINGS.STATE_NAMES.ON_HOLD,
OUTBOUND: CCP_STRINGS.STATE_NAMES.OUTBOUND,
INBOUND: CCP_STRINGS.STATE_NAMES.INBOUND,
INCOMING: CCP_STRINGS.STATE_NAMES.INCOMING,
INCOMING_CALLBACK: CCP_STRINGS.STATE_NAMES.INCOMING_CALLBACK,
MONITORING: CCP_STRINGS.STATE_NAMES.MONITORING,
ACW: CCP_STRINGS.STATE_NAMES.ACW,
JOINED: CCP_STRINGS.STATE_NAMES.JOINED,
ERROR: CCP_STRINGS.STATE_NAMES.ERROR,
MISSED_CALL: CCP_STRINGS.STATE_NAMES.MISSED_CALL,
INITIALIZING: CCP_STRINGS.STATE_NAMES.INITIALIZING,
INITIALIZATION_FAILURE: CCP_STRINGS.STATE_NAMES.INITIALIZATION_FAILURE,
DISCONNECTED: "<disconnected>", // This should never be seen as it should be invisible, indicates error
AGENT_HOLD: CCP_STRINGS.STATE_NAMES.AGENT_HOLD,
MUTED: CCP_STRINGS.STATE_NAMES.MUTED
};
})
.constant("CCP_PAGE_SITE", {
MAIN: 1,
SETTINGS: 2,
SET_STATUS: 3,
OTHERS: 4
})
.constant("AGENT_STATE_TYPE", {
ROUTABLE: "Routable",
OFFLINE: "Offline",
AUX: "Aux"
})
.constant("AGENT_PHONE_TYPE", {
SOFT: 1,
DESK: 0
})
.factory("CALL_ERRORS", function(CCP_STRINGS) {
var errorMap = {};
errorMap[lily.AgentErrorStates.AGENT_HUNG_UP] = CCP_STRINGS.ERRORS.AGENT_HUNG_UP;
errorMap[lily.AgentErrorStates.BAD_ADDRESS_CUSTOMER] = CCP_STRINGS.ERRORS.BAD_ADDRESS_CUSTOMER;
errorMap[lily.AgentErrorStates.BAD_ADDRESS_AGENT] = CCP_STRINGS.ERRORS.BAD_ADDRESS_AGENT;
errorMap[lily.AgentErrorStates.FAILED_CONNECT_AGENT] = CCP_STRINGS.ERRORS.FAILED_CONNECT_AGENT;
errorMap[lily.AgentErrorStates.FAILED_CONNECT_CUSTOMER] = CCP_STRINGS.ERRORS.FAILED_CONNECT_CUSTOMER;
errorMap[lily.AgentErrorStates.LINE_ENGAGED_AGENT] = CCP_STRINGS.ERRORS.LINE_ENGAGED_AGENT;
errorMap[lily.AgentErrorStates.LINE_ENGAGED_CUSTOMER] = CCP_STRINGS.ERRORS.LINE_ENGAGED_CUSTOMER;
errorMap[lily.AgentErrorStates.MISSED_CALL_CUSTOMER] = CCP_STRINGS.ERRORS.MISSED_CALL_CUSTOMER;
errorMap[lily.AgentErrorStates.MISSED_CALL_AGENT] = CCP_STRINGS.ERRORS.MISSED_CALL_AGENT;
errorMap[lily.AgentErrorStates.MISSED_QUEUE_CALLBACK] = CCP_STRINGS.ERRORS.MISSED_QUEUE_CALLBACK;
errorMap[lily.AgentErrorStates.REALTIME_COMMUNICATION_ERROR] = CCP_STRINGS.ERRORS.REALTIME_COMMUNICATION_ERROR;
errorMap[lily.AgentErrorStates.DEFAULT] = CCP_STRINGS.ERRORS.DEFAULT;
return errorMap;
})
.constant("REFRESH_AGENT_INFO_DELAY", 500)
.constant("VALID_NUMBER_INPUT_REGEX", /[0-9\*\#]/)
.factory("DIAL_PAD_STRUCT", function(CCP_STRINGS) {
return [
[
{
number: "1",
text: "hidden"
},
{
number: "2",
text: CCP_STRINGS.NUMBER_TEXT.TWO
},
{
number: "3",
text: CCP_STRINGS.NUMBER_TEXT.THREE
}
],
[
{
number: "4",
text: CCP_STRINGS.NUMBER_TEXT.FOUR
},
{
number: "5",
text: CCP_STRINGS.NUMBER_TEXT.FIVE
},
{
number: "6",
text: CCP_STRINGS.NUMBER_TEXT.SIX
}
],
[
{
number: "7",
text: CCP_STRINGS.NUMBER_TEXT.SEVEN
},
{
number: "8",
text: CCP_STRINGS.NUMBER_TEXT.EIGHT
},
{
number: "9",
text: CCP_STRINGS.NUMBER_TEXT.NINE
}
],
[
{
number: "*",
text: "hidden"
},
{
number: "0",
text: CCP_STRINGS.NUMBER_TEXT.ZERO
},
{
number: "#",
text: "hidden"
}
]
];
})
.factory("REPORT_REASONS", function(CCP_STRINGS) {
return [
{
type: CCP_STRINGS.REPORT_REASONS.AUDIO_RELATED,
value: CCP_STRINGS.REPORT_REASONS.UNABLE_TO_HEAR_CUSTOMER
},
{
type: CCP_STRINGS.REPORT_REASONS.AUDIO_RELATED,
value: CCP_STRINGS.REPORT_REASONS.CANT_HEAR_AGENT
},
{
type: CCP_STRINGS.REPORT_REASONS.AUDIO_RELATED,
value: CCP_STRINGS.REPORT_REASONS.NO_AUDIO
},
{
type: CCP_STRINGS.REPORT_REASONS.AUDIO_RELATED,
value: CCP_STRINGS.REPORT_REASONS.CUSTOMER_TOO_QUIET
},
{
type: CCP_STRINGS.REPORT_REASONS.AUDIO_RELATED,
value: CCP_STRINGS.REPORT_REASONS.AGENT_TOO_QUIET
},
{
type: CCP_STRINGS.REPORT_REASONS.AUDIO_RELATED,
value: CCP_STRINGS.REPORT_REASONS.STATIC_ON_LINE
},
{
type: CCP_STRINGS.REPORT_REASONS.AUDIO_RELATED,
value: CCP_STRINGS.REPORT_REASONS.FAILED_COMPLETE_CALL
},
{
type: CCP_STRINGS.REPORT_REASONS.AUDIO_RELATED,
value: CCP_STRINGS.REPORT_REASONS.AUDIO_DROPPED
},
{
type: CCP_STRINGS.REPORT_REASONS.AUDIO_RELATED,
value: CCP_STRINGS.REPORT_REASONS.GAPS_IN_AUDIO
},
{
type: CCP_STRINGS.REPORT_REASONS.AUDIO_RELATED,
value: CCP_STRINGS.REPORT_REASONS.EXCESSIVE_DELAY
},
{
type: CCP_STRINGS.REPORT_REASONS.CALL_HANDLING,
value: CCP_STRINGS.REPORT_REASONS.FAILED_TRANSFER
},
{
type: CCP_STRINGS.REPORT_REASONS.CALL_HANDLING,
value: CCP_STRINGS.REPORT_REASONS.FAILED_ON_HOLD
},
{
type: CCP_STRINGS.REPORT_REASONS.CALL_HANDLING,
value: CCP_STRINGS.REPORT_REASONS.FAILED_RESUME
},
{
type: CCP_STRINGS.REPORT_REASONS.CALL_HANDLING,
value: CCP_STRINGS.REPORT_REASONS.DROPPED_TALKING
},
{
type: CCP_STRINGS.REPORT_REASONS.CALL_HANDLING,
value: CCP_STRINGS.REPORT_REASONS.DROPPED_ON_HOLD
}
];
})
.constant("NOTES_MAX_LENGTH", 500)
.constant("ENTER_KEY_CODE", 13)
.constant("DISMISSIBLE_ERROR_TYPE", {
INVALID_NUMBER: 1,
UNSUPPORTED_COUNTRY: 2,
HANG_UP: 3,
DISCONNECT: 4,
HOLD: 5,
RESUME: 6,
ACCEPT: 7,
JOIN: 8,
SWAP: 9,
TRANSFER: 10,
CHANGE_STATE: 11,
GET_ADDRESSES: 12,
SEND_DIGIT: 13,
SET_CONFIG: 14,
REPORT: 15,
SET_LOCALE: 16,
INVALID_OUTBOUND_CONFIGURATION: 17
})
.constant("LOCALE_NAME_MAP", {
en_US: "English",
de_DE: "Deutsch",
es_ES: "Español",
fr_FR: "Français",
ja_JP: "日本語",
it_IT: "Italiano",
ko_KR: "한국어",
pt_BR: "Português",
zh_CN: "中文(简体)",
zh_TW: "中文(繁體)"
})
.constant("STORAGE_KEYS", {
LAST_USED_COUNTRY: "lastUsedCountry"
});
})();
(function() {
angular
.module("ccpModule")
.factory("MiscServices", function($resource, initParams) {
return $resource(
"",
{},
{
logout: {
url: document.requestPathPrefix + "/logout",
method: "GET",
params: {
token: initParams.authToken
}
}
}
);
})
.factory("LocaleService", function($resource) {
return $resource(
"",
{},
{
getSupportedLocales: {
url: document.requestPathPrefix + "/ccp/locale",
method: "GET",
transformResponse: function(data) {
return { list: angular.fromJson(data) };
}
}
}
);
})
.factory("WorkFlowFactory", function() {
return {
get: function(instanceId, data) {
return csm.API.getWorkflow("VoiceCall", instanceId, data);
}
};
})
.factory("MetricListener", function(WorkFlowFactory) {
return {
start: function() {
// Listen on shared worker published metrics
connect.core.upstream.onUpstream(connect.EventType.API_METRIC, function(data) {
connect.ifMaster(connect.MasterTopics.METRICS, function() {
var latencyMetric = new csm.Metric(data.name, csm.UNIT.MILLISECONDS, data.time);
latencyMetric.addDimension("Metric", "Time");
var errorMetric;
if (data.error) {
errorMetric = new csm.Metric(data.name, csm.UNIT.COUNT, 1);
} else {
errorMetric = new csm.Metric(data.name, csm.UNIT.COUNT, 0);
}
errorMetric.addDimension("Metric", "Error");
data.dimensions.forEach(function(dimension) {
errorMetric.addDimension(dimension.name, dimension.value);
latencyMetric.addDimension(dimension.name, dimension.value);
});
csm.API.addMetric(latencyMetric);
csm.API.addMetric(errorMetric);
});
});
// Listen on StreamJS published metrics
// Since StreamJS is public, we only accept and publish CSM whitelisted metrics here.
var WHITELISTED_STREAM_EVENTS = [
"Ringtone Connecting",
"Callback Ringtone Connecting",
"Ringtone Stop",
"Ringtone Start",
"Softphone Session Failed",
"Softphone Connecting",
"Softphone Session Connected",
"Softphone Session Completed",
"MultiSessions",
"MultiSessionHangUp"
];
connect.core.getEventBus().subscribe(connect.EventType.CLIENT_METRIC, function(event) {
if (event.name && WHITELISTED_STREAM_EVENTS.indexOf(event.name) !== -1) {
if (event.contactId) {
var workflow = WorkFlowFactory.get(event.contactId);
var DEDUP_TIME_MS = 500;
workflow.eventWithDedup(event.name, event.data, DEDUP_TIME_MS);
}
}
});
var WHITELISTED_STREAM_EVENTS_FOR_METRICS = ["MultiSessions", "MultiSessionHangUp"];
connect.core.getEventBus().subscribe(connect.EventType.CLIENT_METRIC, function(event) {
if (event.name && WHITELISTED_STREAM_EVENTS_FOR_METRICS.indexOf(event.name) !== -1) {
var errorMetric = new csm.Metric(event.name, csm.UNIT.COUNT, 1);
errorMetric.addDimension("Metric", "Event");
csm.API.addMetric(errorMetric);
}
});
}
};
});
})();
(function() {
angular
.module("ccpModule")
.factory("utils", function(
$timeout,
$location,
REFRESH_AGENT_INFO_DELAY,
CCP_STATE_NAMES,
VALID_NUMBER_INPUT_REGEX,
MiscServices
) {
var obj = {};
var phoneUtil = libphonenumber.PhoneNumberUtil.getInstance();
obj.setAgentState = function(state, callbacks) {
var agent = getAgent();
agent.setStatus(angular.copy(state), callbacks);
};
obj.getStateDuration = function(state) {
var millis = 0;
if (
getVoiceContact() &&
lily.contains(
[
CCP_STATE_NAMES.INCOMING,
CCP_STATE_NAMES.CONNECTED,
CCP_STATE_NAMES.ON_HOLD,
CCP_STATE_NAMES.JOINED,
CCP_STATE_NAMES.INBOUND,
CCP_STATE_NAMES.MONITORING
],
state
)
) {
var activeConnection =
getVoiceContact().getActiveInitialConnection() || getVoiceContact().getSingleActiveThirdPartyConnection();
millis = activeConnection ? activeConnection.getStatusDuration() : getVoiceContact().getStatusDuration();
} else {
millis = getAgent().getStatusDuration();
}
return formatMillis(millis);
};
obj.getThirdPartyStateDuration = function() {
var contact = getVoiceContact();
if (contact == null) {
return "";
}
var conn = contact.getSingleActiveThirdPartyConnection();
if (conn == null) {
return "";
}
var millis = conn.getStatusDuration();
return formatMillis(millis);
};
obj.acceptCall = function(callbacks) {
var contact = getVoiceContact();
contact.accept({
success: function() {
if (callbacks && callbacks.success) {
callbacks.success();
}
},
failure: function(data) {
if (callbacks && callbacks.failure) {
callbacks.failure();
}
lily
.getLog()
.error("utils.acceptCall failed. ")
.withData(data);
}
});
};
obj.dial = function(number, callbacks) {
var agent = getAgent();
lily.assertNotNull(number, "number");
if (number.trim() === "") {
return;
}
var address = lily.Address.byPhoneNumber(number);
agent.connect(address, {
success: function(data) {
$location.path("/");
lily.getLog().info("Outbound Call succeeded!");
if (callbacks && callbacks.success) {
callbacks.success(data);
}
},
failure: function(data) {
lily
.getLog()
.error("Outbound Call failed!")
.withObject(data);
if (callbacks && callbacks.failure) {
callbacks.failure(data);
}
}
});
return address.stripPhoneNumber();
};
obj.muteToggle = function(mute) {
mute ? getAgent().mute() : getAgent().unmute();
};
obj.hold = function(callbacks) {
lily.getLog().info("Holding " + (obj.isHandlingThirdParty() ? "third party" : "customer"));
if (obj.isHandlingThirdParty()) {
var thirdParty = getVoiceContact()
.getThirdPartyConnections()
.filter(function(conn) {
return conn.isActive();
})[0];
thirdParty.hold(callbacks);
} else {
getVoiceContact()
.getInitialConnection()
.hold(callbacks);
}
};
(obj.threeWayHoldImpl = function(holdingCustomer, callbacks) {
var contact = getVoiceContact();
var initialConn = contact ? contact.getInitialConnection() : null;
var thirdPartyConn = contact ? contact.getSingleActiveThirdPartyConnection() : null;
if (contact !== null && initialConn && thirdPartyConn) {
if (initialConn.isConnected() && thirdPartyConn.isConnected()) {
if (holdingCustomer) {
initialConn.hold(callbacks);
} else {
thirdPartyConn.hold(callbacks);
}
} else {
getVoiceContact().toggleActiveConnections(callbacks);
}
}
}),
(obj.resume = function(callbacks) {
lily.getLog().info("Resuming " + (obj.isHandlingThirdParty() ? "third party" : "customer"));
if (obj.isHandlingThirdParty()) {
obj.resumeThirdParty(callbacks);
} else {
obj.resumeCustomer(callbacks);
}
});
obj.resumeCustomer = function(callbacks) {
lily.getLog().info("Resuming customer");
getVoiceContact()
.getInitialConnection()
.resume(callbacks);
};
obj.resumeThirdParty = function(callbacks) {
lily.getLog().info("Resuming third party");
var thirdParty = getVoiceContact()
.getThirdPartyConnections()
.filter(function(conn) {
return conn.isActive();
})[0];
thirdParty.resume(callbacks);
};
obj.conference = function(callbacks) {
getVoiceContact().conferenceConnections(callbacks);
};
obj.swapCall = function(callbacks) {
getVoiceContact().toggleActiveConnections(callbacks);
};
obj.allHold = function(callbacks) {
var connectedConns = getVoiceContact()
.getConnections()
.filter(function(conn) {
return (
conn.getType() !== lily.ConnectionType.AGENT &&
conn.getStatus().type === lily.ConnectionStatusType.CONNECTED
);
});
/**
* This is necessary due to the nature of Conferenced state
* in the VoiceService. We can't immediately put both legs
* on hold or one of the calls will fail. So we put one leg
* on hold, wait ALL_HOLD_DELAY_TIMEOUT_MS milliseconds, then
* put the other leg on hold. This gives GACD Critical and
* VoiceService enough time to update the conversation state
* so that the second hold operation is valid.
*/
var allHoldImpl = function(conns) {
var self = this;
var ALL_HOLD_DELAY_TIMEOUT_MS = 500;
if (conns.length > 0) {
var conn = conns.pop();
conn.hold({
success: function() {
window.setTimeout(allHoldImpl(conns), ALL_HOLD_DELAY_TIMEOUT_MS);
if (callbacks && callbacks.success) {
callbacks.success();
}
},
failure: function(data) {
lily
.getLog()
.error("Failed to put %s conn on hold.", conn.getConnectionId())
.withData(data);
if (callbacks && callbacks.failure) {
callbacks.failure();
}
}
});
}
};
allHoldImpl(connectedConns);
};
obj.getPhoneInitMethod = function() {
return getVoiceContact().getInitiationMethod();
};
obj.transferToDestination = function(destination, callbacks) {
getVoiceContact().addConnection(angular.copy(destination), {
success: function(data) {
if (callbacks && callbacks.success) {
callbacks.success(data);
}
window.location.href = "#/";
},
failure: function(data) {
if (callbacks && callbacks.failure) {
callbacks.failure(data);
}
}
});
};
obj.transferToNumber = function(number, callbacks) {
lily.assertNotNull(number, "number");
if (number.trim() === "") {
return;
}
getVoiceContact().addConnection(lily.Address.byPhoneNumber(number), {
success: function(data) {
if (callbacks && callbacks.success) {
callbacks.success();
}
window.location.href = "#/";
},
failure: function(data) {
if (callbacks && callbacks.failure) {
callbacks.failure(data);
}
}
});
};
obj.sendDigit = function(digit, callbacks) {
if (!obj.isValidNumberInput(digit)) {
lily.getLog().warn("sendDigit input is not a number, '#' or '*'.");
return;
}
var activeConnections = getVoiceContact()
.getConnections()
.filter(function(conn) {
return conn.getType() !== lily.ConnectionType.AGENT && conn.isConnected();
});
if (activeConnections && activeConnections.length === 1) {
activeConnections[0].sendDigits(digit, {
success: function(data) {
lily.getLog().info("Send digit " + digit + " succeeded.");
if (callbacks && callbacks.success) {
callbacks.success(data);
}
},
failure: function(data) {
lily.getLog().info("Send digit " + digit + " failed.");
if (callbacks && callbacks.failure) {
callbacks.failure(data);
}
}
});
} else {
lily.getLog().error("utils.sendDigit() failed, can not determine active connection!");
}
};
obj.getPhoneNumberFromConnection = function(conn) {
if (!conn) {
lily.getLog().warn("obj.getPhoneNumberFromConnection() provided a null connection, returning empty string.");
return "";
}
var address = conn.getAddress();
if (address) {
var result = address.stripPhoneNumber();
if (result) {
return result;
} else {
lily
.getLog()
.warn(
"obj.getPhoneNumberFromConnection() provided a connection address with null phone number, returning empty string."
);
return "";
}
} else {
lily
.getLog()
.warn(
"obj.getPhoneNumberFromConnection() provided a connection with null address, returning empty string."
);
return "";
}
};
obj.isValidPhoneNumber = function(extension) {
var valid = false;
try {
valid = phoneUtil.isValidNumber(phoneUtil.parseAndKeepRawInput(extension));
} catch (e) {
lily.getLog().info("Phone number %s is not in E.164 format.", extension);
}
return valid;
};
obj.getContactNumber = function(mediaLegType) {
var conn = getVoiceContact().getInitialConnection();
return obj.getPhoneNumberFromConnection(conn);
};
obj.getThirdPartyContactNumber = function() {
var conn = getVoiceContact().getSingleActiveThirdPartyConnection();
return obj.getPhoneNumberFromConnection(conn);
};
obj.flagCall = function(flagParams) {
lily.assertNotNull(flagParams, "flagParams");
getVoiceContact().notifyIssue(flagParams.type, flagParams.description, flagParams);
};
obj.saveAgentExtensionConfig = function(config, callbacks) {
lily.assertNotNull(config, "config");
lily.assertNotNull(config.softphoneEnabled, "config.softphoneEnabled");
var agent = getAgent();
var newConfig = agent.getConfiguration();
newConfig.extension = config.extension || newConfig.extension;
newConfig.softphoneEnabled = config.softphoneEnabled;
agent.setConfiguration(newConfig, {
success: function(data) {
lily
.getLog()
.info("Changed agent phone type to " + (newConfig.softphoneEnabled ? "softphone" : "deskphone"));
if (callbacks && callbacks.success) {
callbacks.success(data);
}
},
failure: function(data) {
lily
.getLog()
.error("Failed to enable softphone for agent.")
.withObject(data);
if (callbacks && callbacks.failure) {
callbacks.failure(data);
}
}
});
};
obj.saveAgentLocaleConfig = function(config, callbacks) {
lily.assertNotNull(config, "config");
lily.assertNotNull(config.agentPreferences, "config.agentPreferences");
lily.assertNotNull(config.agentPreferences.locale, "config.agentPreferences.locale");
var agent = getAgent();
var newConfig = agent.getConfiguration();
if (!newConfig.agentPreferences) {
newConfig.agentPreferences = {};
}
newConfig.agentPreferences.locale = config.agentPreferences.locale;
agent.setConfiguration(newConfig, {
success: function(data) {
lily.getLog().info("Changed agent locale to " + newConfig.agentPreferences.locale);
if (callbacks && callbacks.success) {
callbacks.success(newConfig.agentPreferences.locale);
}
},
failure: function(data) {
lily
.getLog()
.error("Failed to change agent locale to " + newConfig.agentPreferences.locale)
.withObject(data);
if (callbacks && callbacks.failure) {
callbacks.failure(data);
}
}
});
};
obj.isHandlingThirdParty = function() {
var contact = getVoiceContact();
if (contact == null) {
return false;
}
var initialConn = contact.getActiveInitialConnection();
var thirdPartyConn = contact.getSingleActiveThirdPartyConnection();
if (initialConn && thirdPartyConn) {
if (initialConn.isConnected()) {
return false;
} else {
return true;
}
} else if (thirdPartyConn) {
return true;
} else {
return false;
}
};
obj.getAddresses = function(callbacks) {
// If agent is not in a call, get external type transfer destinations from default outbound queue
// Otherwise, get all transfer destinations for the queue that the current call comes from
var skillIds = getAgent()
.getAllQueueARNs()
.concat([getAgent().getRoutingProfile().defaultOutboundQueue.queueARN]);
var voiceContact = getVoiceContact();
var isOutbound = !(
voiceContact &&
voiceContact.getQueue() &&
voiceContact.getStatus().type !== lily.ContactStatusType.ENDED
);
getAgent().getEndpoints(skillIds, {
success: function(data) {
var addresses = data.addresses;
if (isOutbound) {
addresses = addresses.filter(function(addr) {
return addr.type === lily.AddressType.PHONE_NUMBER;
});
}
if (callbacks && callbacks.success) {
callbacks.success(addresses);
}
},
failure: function(err, data) {
if (callbacks && callbacks.failure) {
callbacks.failure(err, data);
}
}
});
};
/**
* If param isThirdParty is provided, check for the proper connection,
* otherwise, return true if any is connected
*/
obj.isConnected = function(isThirdParty) {
var contact = getVoiceContact();
if (contact == null) {
return false;
}
var initialConn = contact.getActiveInitialConnection();
var thirdPartyConn = contact.getSingleActiveThirdPartyConnection();
if (isThirdParty === undefined || isThirdParty === null) {
return (initialConn && initialConn.isConnected()) || (thirdPartyConn && thirdPartyConn.isConnected());
} else {
if (isThirdParty) {
return thirdPartyConn && thirdPartyConn.isConnected();
} else {
return initialConn && initialConn.isConnected();
}
}
};
/* miscellaneous services */
obj.logout = function() {
MiscServices.logout(
{},
function success(data) {
// Signal the shared worker to terminate.
var eventBus = lily.core.getEventBus();
eventBus.trigger(lily.EventType.TERMINATE);
},
function error(data) {
lily
.getLog()
.error("utils.logout failed. " + data)
.withData(data);
}
);
};
obj.isValidNumberInput = function(digit) {
return VALID_NUMBER_INPUT_REGEX.test(digit);
};
/* private functions */
var getAgent = function() {
return new lily.Agent();
};
// When we dial out the queue_callback, it is handled in the same way as a voice contact.
var getVoiceContact = function() {
return getAgent().getContacts(lily.ContactType.VOICE)[0] || getQueueCallBackContact();
};
var getQueueCallBackContact = function() {
return getAgent().getContacts(lily.ContactType.QUEUE_CALLBACK)[0] || null;
};
obj.getVoiceContact = getVoiceContact;
obj.getQueueCallBackContact = getQueueCallBackContact;
var formatMillis = function(millis) {
var HOURS_PER_DAY = 24;
var MINUTES_PER_HOUR = 60;
var SECONDS_PER_MINUTE = 60;
var MILLIS_PER_SECOND = 1000;
var MILLIS_PER_MINUTE = MILLIS_PER_SECOND * SECONDS_PER_MINUTE;
var MILLIS_PER_HOUR = MILLIS_PER_MINUTE * MINUTES_PER_HOUR;
var time = {
hours: millis / MILLIS_PER_HOUR,
minutes: (millis % MILLIS_PER_HOUR) / MILLIS_PER_MINUTE,
seconds: (millis % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND
};
if (time.hours > 0) {
return lily.sprintf("%d:%02d:%02d", time.hours, time.minutes, time.seconds);
} else {
return lily.sprintf("%d:%02d", time.minutes, time.seconds);
}
};
return obj;
});
})();
/*
* init.js: Initialization functions used to setup the contact control panel.
*
* Refactored to integrate with LilyStreamJS.
*
* Author: Lain Supe (supelee)
* Date: Monday, April 11th 2016
* Refactor Date: Tuesday, November 22nd 2016
*/
(function() {
angular.module("ccpModule").factory("CCPInitService", function(utils, $window) {
/**
* Internal function used to initialize LilyStreamJS prior to AgentControlPanel creation.
*
* @param params.baseUrl The baseUrl provided by bootstrapAPI.
* @param callbacks.success Called when the process completes successfully.
* @param callbacks.failure Called if there was an error initializing the API.
*/
var initializeAPI = function(params, callbacks) {
try {
if (lily.core.initialized) {
callbacks.success();
} else {
lily.core.initSharedWorker(params);
lily.core.initRingtoneEngines(params);
lily.core.initSoftphoneManager(params);
callbacks.success();
}
} catch (e) {
var log = lily
.getLog()
.error("Failed to initialize LilyStreamJS.")
.withException(e)
.withObject(params);
callbacks.failure(JSON.stringify(log));
}
};
var obj = {};
obj.initialize = function(initParams, callbacks) {
initializeAPI(initParams, {
success: callbacks.success || function() {},
failure: callbacks.failure || function() {}
});
};
return obj;
});
})();
(function() {
angular
.module("phoneNumberModule", ["ccpModule"])
.factory("phoneNumberFormatterFactory", function(COUNTRY_NAME_MAP) {
var phoneUtil = libphonenumber.PhoneNumberUtil.getInstance();
return {
phoneNumberTranslator: function(value) {
lily.assertNotNull(value, "value");
var formattedNumber = value;
try {
// Bypass phone number validation for Philippines number pattern migration.
// Old pattern: +632XXXXXXX, New pattern +632[3-8]XXXXXXX
// TODO: remove this snippet after google-libphonenumber library's update is available
if (value.match(/^\+632[3-8]\d{7}/) && value.length === 12) {
return (
value.substring(0, 3) +
" " +
value.substring(3, 4) +
" " +
value.substring(4, 8) +
" " +
value.substring(8)
);
}
var number = phoneUtil.parseAndKeepRawInput(value, "");
var regionCode = phoneUtil.getRegionCodeForNumber(number);
var phoneNumber = phoneUtil.parse(value, regionCode);
formattedNumber = phoneUtil.format(phoneNumber, libphonenumber.PhoneNumberFormat.INTERNATIONAL);
} catch (err) {
lily
.getLog()
.warn(err.message)
.withException(err);
}
return formattedNumber;
},
getCountryCodeList: function(countryList) {
lily.assertNotNull(countryList, "countryList");
var countryCodeList = [];
countryList.forEach(function(country) {
var countryCode = "+" + phoneUtil.getCountryCodeForRegion(country);
var flagClass = country + "-flag";
var countryName = COUNTRY_NAME_MAP[country.toUpperCase()];
countryCodeList.push({ value: country, name: countryName, code: countryCode, flag: flagClass });
});
// Put countries without name at the bottom of the list
var countriesWithoutName = countryCodeList.filter(function(c) {
return !c.name;
});
var countriesWithName = countryCodeList
.filter(function(c) {
return !!c.name;
})
.sort(function sortComparator(lhs, rhs) {
return lhs.name.localeCompare(rhs.name);
});
return countriesWithName.concat(countriesWithoutName);
},
getCountryForNumber: function(value) {
if (!value) {
return { value: "us", name: COUNTRY_NAME_MAP.US, code: "+1", flag: "us-flag" };
}
try {
// Bypass phone number validation for Philippines number pattern migration.
// Old pattern: +632XXXXXXX, New pattern +632[3-8]XXXXXXX
// Remove newly added digit to get the country info from google-libphonenumber library
// TODO: remove this snippet after google-libphonenumber library's update is available
if (value.match(/^\+632[3-8]\d{7}/) && value.length === 12) {
value = value.substring(0, 4) + value.substring(5);
}
var number = phoneUtil.parseAndKeepRawInput(value, "");
var regionCode = phoneUtil.getRegionCodeForNumber(number);
var countryCode = "+" + phoneUtil.getCountryCodeForRegion(regionCode);
var flagClass = regionCode + "-flag";
var countryName = COUNTRY_NAME_MAP[regionCode.toUpperCase()];
return { value: regionCode, name: countryName, code: countryCode, flag: flagClass };
} catch (e) {
lily.getLog().info("Phone number %s is not in E.164 format.", value);
}
},
getNumberWithoutCountryCode: function(value) {
if (!value) {
return value;
}
try {
// Bypass phone number validation for Philippines number pattern migration.
// Old pattern: +632XXXXXXX, New pattern +632[3-8]XXXXXXX
// TODO: remove this snippet after google-libphonenumber library's update is available
if (value.match(/^\+632[3-8]\d{7}/) && value.length === 12) {
return value.substring(3);
}
var number = phoneUtil.parseAndKeepRawInput(value, "");
return number.getNationalNumber();
} catch (e) {
lily.getLog().info("Phone number %s is not in E.164 format.", value);
// If failed to parse, return original value
return value;
}
}
};
})
.directive("validPhoneNumber", function(utils, phoneNumberFormatterFactory) {
return {
require: "ngModel",
link: function(scope, elm, attrs, ctrl) {
// Centralized directive for validating phone number
ctrl.$validators.validPhoneNumber = function(modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
return false;
}
// Bypass phone number validation for Philippines number pattern migration.
// Old pattern: +632XXXXXXX, New pattern +632[3-8]XXXXXXX
// TODO: remove this snippet after google-libphonenumber library's update is available
modelValue = String(modelValue);
if (attrs.selectedCountry === "+63" && modelValue.match(/^2[3-8]\d{7}/) && modelValue.length === 9) {
return true;
}
return utils.isValidPhoneNumber(attrs.selectedCountry + modelValue);
};
}
};
})
.directive("phoneNumberFormatter", function(phoneNumberFormatterFactory) {
return {
restrict: "AE",
scope: true,
controllerAs: "",
link: function(scope, element, attrs) {
scope.$watch(attrs.phoneNumberFormatter, function(newValue, oldValue) {
var formattedNumber = phoneNumberFormatterFactory.phoneNumberTranslator(newValue);
if (element.text() !== formattedNumber) {
element.text(formattedNumber);
}
});
}
};
});
})();
/**
* Handler for dismissible errors in the UI
*/
(function() {
angular.module("ccpModule").factory("errorHandler", function(CCP_STRINGS, DISMISSIBLE_ERROR_TYPE) {
// localized strings
var DETAILS = CCP_STRINGS.DISMISSIBLE_ERRORS;
var SOFT_HEADERS = CCP_STRINGS.SOFTPHONE_ERROR_MESSAGE_HEADERS;
var SOFT_DETAILS = CCP_STRINGS.SOFTPHONE_ERROR_MESSAGES;
return {
getErrorDetailsByType: function(errorType) {
switch (errorType) {
/********* Call Control Errors *********/
case DISMISSIBLE_ERROR_TYPE.INVALID_NUMBER:
return {
messageToUser: DETAILS.OUTBOUND_INVALID
};
case DISMISSIBLE_ERROR_TYPE.UNSUPPORTED_COUNTRY:
return {
messageToUser: DETAILS.OUTBOUND_UNDIALABLE
};
case DISMISSIBLE_ERROR_TYPE.HANG_UP:
return {
messageToUser: DETAILS.HANG_UP
};
case DISMISSIBLE_ERROR_TYPE.DISCONNECT:
return {
messageToUser: DETAILS.DISCONNECT
};
case DISMISSIBLE_ERROR_TYPE.HOLD:
return {
messageToUser: DETAILS.HOLD
};
case DISMISSIBLE_ERROR_TYPE.RESUME:
return {
messageToUser: DETAILS.RESUME
};
case DISMISSIBLE_ERROR_TYPE.ACCEPT:
return {
messageToUser: DETAILS.ACCEPT
};
case DISMISSIBLE_ERROR_TYPE.JOIN:
return {
messageToUser: DETAILS.JOIN
};
case DISMISSIBLE_ERROR_TYPE.SWAP:
return {
messageToUser: DETAILS.SWAP
};
case DISMISSIBLE_ERROR_TYPE.TRANSFER:
return {
messageToUser: DETAILS.TRANSFER
};
case DISMISSIBLE_ERROR_TYPE.CHANGE_STATE:
return {
messageToUser: DETAILS.CHANGE_STATE
};
case DISMISSIBLE_ERROR_TYPE.GET_ADDRESSES:
return {
messageToUser: DETAILS.GET_ADDRESSES
};
case DISMISSIBLE_ERROR_TYPE.SEND_DIGIT:
return {
messageToUser: DETAILS.SEND_DIGIT
};
case DISMISSIBLE_ERROR_TYPE.SET_CONFIG:
return {
messageToUser: DETAILS.SET_CONFIG
};
case DISMISSIBLE_ERROR_TYPE.REPORT:
return {
messageToUser: DETAILS.REPORT
};
case DISMISSIBLE_ERROR_TYPE.SET_LOCALE:
return {
messageToUser: DETAILS.SET_LOCALE
};
case DISMISSIBLE_ERROR_TYPE.INVALID_OUTBOUND_CONFIGURATION:
return {
messageToUser: DETAILS.OUTBOUND_CONFIGURATION
};
/********* Soft Phone Errors *********/
case lily.SoftphoneErrorTypes.MICROPHONE_NOT_SHARED:
return {
header: SOFT_HEADERS.MICROPHONE_NOT_SHARED,
messageToUser: SOFT_DETAILS.MICROPHONE_NOT_SHARED,
urlText: CCP_STRINGS.SOFTPHONE_HELP_URLS.MICROPHONE_NOT_SHARED,
url: "https://docs.aws.amazon.com/connect/latest/userguide/agentconsole-guide.html#accessing-microphone"
};
case lily.SoftphoneErrorTypes.UNSUPPORTED_BROWSER:
return {
header: SOFT_HEADERS.UNSUPPORTED_BROWSER,
messageToUser: SOFT_DETAILS.UNSUPPORTED_BROWSER,
urlText: CCP_STRINGS.SOFTPHONE_HELP_URLS.UNSUPPORTED_BROWSER,
url: "https://docs.aws.amazon.com/connect/latest/adminguide/what-is-amazon-connect.html#browsers"
};
case lily.SoftphoneErrorTypes.ICE_COLLECTION_TIMEOUT:
return {
header: SOFT_HEADERS.SOFTPHONE_CALL_FAILED,
messageToUser: SOFT_DETAILS.SOFTPHONE_CONNECTION_FAILED
};
case lily.SoftphoneErrorTypes.SIGNALLING_CONNECTION_FAILURE:
return {
header: SOFT_HEADERS.SOFTPHONE_CALL_FAILED,
messageToUser: SOFT_DETAILS.SOFTPHONE_CONNECTION_FAILED
};
case lily.SoftphoneErrorTypes.SIGNALLING_HANDSHAKE_FAILURE:
return {
header: SOFT_HEADERS.SOFTPHONE_CALL_FAILED,
messageToUser: SOFT_DETAILS.SOFTPHONE_CONNECTION_FAILED
};
default:
// This should never happen, just a reminder to developer.
lily.getLog().warn("Unknown error type encountered: " + errorType);
return null;
}
}
};
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment