Add the Call Panel component to your application
The previous topic showed how to build a basic JET application and how to deploy it in your Fusion media toolbar.
Next, we need to show incoming call notifications, and buttons to accept or disconnect a call in the media toolbar application. To do this, you can build your logic to render UI elements based on your requirements. As part of the steps in this series, the UI elements required for showing the call notification, and buttons to accept and disconnect calls are encapsulated as a custom JET component.
States of the Call Panel component
- Ringing: The Call Panel component is in the Ringing state when the agent receives an incoming call. The Call Panel component will also be in a ringing state when the agent starts an outbound call which isn't yet picked up by the customer. Your application. During this state, the Call Panel component has information about the incoming call such as contact name (which is retrieved from the Fusion application), IVR data, and 2 buttons for accepting or rejecting a call.
- Accepted: The Call Panel component is in the Accepted state when the agent accepts an incoming call from the customer. The Call Panel component is also in an Accepted state when a customer accepts an outbound call started by an agent. During this state, the call-panel component will have the call hangup button and a call-in-progress animation in addition to the incoming call information.
- Disconnected: The Call Panel component moves to the Disconnected state when an agent rejects an incoming call or disconnects an ongoing call.
Create a custom component in your application
- Open the root folder of your toolbar application in the terminal and run the
command:
ojet create component call-panel
.This creates the
call panel
custom component in thesrc/ts/jet-composites
folder.For more information, see Create an Oracle JET Web Component.
Update the component's files based on your requirements
- To update the
src/ts/jet-composites/call-panel-view.html
file:<div class="oj-sm-margin-4x oj-flex oj-sm-justify-content-center"> <div class="oj-flex oj-bg-neutral-170 oj-sm-padding-6x call-panel oj-color-invert"> <div class="oj-sm-12 oj-flex oj-sm-flex-direction-column oj-sm-align-items-center oj-sm-justify-content-space-between"> <div class="oj-sm-margin-4x-top oj-flex oj-sm-flex-direction-column oj-sm-align-items-center top-panel"> <div class="oj-flex-item oj-typography-heading-xs oj-sm-margin-5x-bottom"> <!-- Shows the text Incoming Call / Calling in the component based on the call direction --> <oj-bind-if test="[[callContext().direction === 'inbound']]"> Incoming Call </oj-bind-if> <oj-bind-if test="[[callContext().direction === 'outbound']]"> Calling </oj-bind-if> </div> <!-- Adds a phone icon as avatar in the component --> <oj-c-avatar class="oj-avatar-8xl" role="img" icon-class="oj-ux-ico-call-incoming" background="green"></oj-c-avatar> <br/><br/> <div id="progressBarContainer" class="oj-flex-item"> <oj-bind-if test="[[callContext().state === 'ACCEPTED']]"> <!-- Show a call in progress animation during the ACCEPTED state --> <oj-c-progress-bar id="progressBar" aria-label="basic progress bar" value="-1"></oj-c-progress-bar> </oj-bind-if> </div> </div> <!-- Show caller information --> <div class="oj-sm-flex oj-sm-flex-direction-column call-details"> <div class="oj-flex-item oj-typography-heading-md oj-sm-margin-2x-vertical"> <oj-bind-text value="[[callContext().callerName]]"></oj-bind-text> </div> <div class="oj-flex-item oj-typography-subheading-xs"> <oj-bind-text value="[[callContext().phonenumber]]"></oj-bind-text> </div> <table class="ivr-data-table oj-typography-body-sm oj-sm-margin-4x oj-helper-text-align-left"> <oj-bind-for-each data="[[ivrDataProvider]]"> <template> <tr> <td><oj-bind-text value="[[$current.data.key]]"></oj-bind-text></td> <td>:</td> <td><oj-bind-text value="[[$current.data.value]]"></oj-bind-text></td> </tr> </template> </oj-bind-for-each> </table> </div> <div class="oj-flex oj-sm-justify-content-center oj-sm-margin-2x-vertical"> <oj-bind-if test="[[callContext().state === 'ACCEPTED']]"> <oj-c-button id="hold" display="icons" label="hold"> <span slot="startIcon" class="oj-ux-ico-pause-circle"></span> </oj-c-button> </oj-bind-if> </div> <div class="oj-flex oj-sm-justify-content-center"> <!-- Show call accept and disconnect buttons in the component --> <oj-bind-if test="[[callContext().state === 'RINGING' && callContext().direction === 'inbound']]"> <!-- Show call accept button only during inbound call RINGING state --> <oj-c-button id="acceptCall" display="icons" label="Accept" chroming="borderless" on-oj-action="[[ acceptClicked ]]" class="oj-sm-margin-2x-horizontal"> <span slot="startIcon" class="oj-ux-ico-call oj-sm-padding-4x oj-sm-padding-10x-horizontal call-accept"></span> </oj-c-button> </oj-bind-if> <oj-c-button id="declineCall" display="icons" label="Decline" chroming="danger" on-oj-action="[[ disconnectClicked ]]" class="oj-sm-margin-2x-horizontal"> <span slot="startIcon" class="oj-ux-ico-call-end oj-sm-padding-4x oj-sm-padding-10x-horizontal"></span> </oj-c-button> </div> </div> </div> </div>
- To update the
src/ts/jet-composites/call-panel-viewModel.ts
file:"use strict"; import * as ko from "knockout"; import Context = require("ojs/ojcontext"); import Composite = require("ojs/ojcomposite"); import ArrayDataProvider = require('ojs/ojarraydataprovider'); import "oj-c/button"; import "ojs/ojknockout"; import "oj-c/avatar"; import "oj-c/progress-bar"; interface CallContext { phonenumber: string; callerName: string; direction: string; eventId: string; ivrData: { [key: string]: string }; state: string; } export default class ViewModel implements Composite.ViewModel<Composite.PropertiesType> { busyResolve: (() => void); composite: Element; properties: Composite.PropertiesType; callContext: ko.Observable<CallContext>; ivrDataProvider: ArrayDataProvider<string, { [key: string]: string }>; constructor(context: Composite.ViewModelContext<Composite.PropertiesType>) { //At the start of your viewModel constructor const elementContext: Context = Context.getContext(context.element); const busyContext: Context.BusyContext = elementContext.getBusyContext(); const options = { "description": "Web Component Startup - Waiting for data" }; this.busyResolve = busyContext.addBusyState(options); this.composite = context.element; // Properties passed to the component this.properties = context.properties; this.callContext = ko.observable(this.properties.callContext); this.ivrDataProvider = new ArrayDataProvider(this.parseIvrData(), { keyAttributes: 'key' }); //Once all startup and async activities have finished, relocate if there are any async activities this.busyResolve(); } parseIvrData(): any { let data: any[] = []; if (this.callContext()?.ivrData) { data = Object.keys(this.callContext().ivrData).map((key: string) => { return { key: key, value: this.callContext().ivrData[key] } }); } const ivrData: any = ko.observableArray(data); return ivrData; } public acceptClicked: (event: any) => void = (event: any): void => { // Logic to fire and event to the container that the accept button is clicked const formattedEvent: object = {bubbles: true, cancelable: false, detail: {}}; const acceptButtonClickedEvent: CustomEvent = new CustomEvent('acceptButtonClicked', formattedEvent); this.composite.dispatchEvent(acceptButtonClickedEvent); } public disconnectClicked: (event: any) => void = (event: any): void => { // Logic to fire and event to the container that the disconecct button is clicked. // If the call is in ACCEPTED state, the value 'HANGUP' is passed as the event payload to the container, // Otherwise, the value 'REJECT' is passed as the event payload const disconnectionState: string = (this.callContext().state === 'ACCEPTED') ? 'WRAPUP' : 'REJECT'; const formattedEvent: object = {bubbles: true, cancelable: false, detail: { disconnectionState }}; const disconnectButtonClickedEvent: CustomEvent = new CustomEvent('disconnectButtonClicked', formattedEvent); this.composite.dispatchEvent(disconnectButtonClickedEvent); } //Lifecycle methods - implement if necessary activated(context: Composite.ViewModelContext<Composite.PropertiesType>): Promise<any> | void { }; connected(context: Composite.ViewModelContext<Composite.PropertiesType>): void { }; bindingsApplied(context: Composite.ViewModelContext<Composite.PropertiesType>): void { }; propertyChanged(context: Composite.PropertyChangedContext<Composite.PropertiesType>): void { if (context.property === 'callContext') { // Update callContext observable based on the propertyChange this.callContext({...this.callContext(), state: context.value.state}); } }; disconnected(element: Element): void { }; };
- To update the
src/ts/jet-composites/call-panel-styles.css
file:call-panel:not(.oj-complete) { visibility: hidden; } call-panel[hidden] { display: none; } .call-panel { min-height: 450px; min-width: 300px; border-radius: 10px; } .top-panel { color: #dfdfdf; } .call-accept { background-color: #28b328; } .call-accept:hover { background-color: #239b23; } .call-details { text-align: center; color: #ffffff; } .ivr-data-table { color: #c4c4c4; } #progressBarContainer { width: 100%; }
- To update the
src/ts/jet-composites/component.json
file:{ "name": "call-panel", "version": "1.0.0", "jetVersion": "^15.1.0", "displayName": "A user friendly, translatable name of the component.", "description": "A translatable high-level description for the component.", "properties": { "callContext": { "description": "The Call context details.", "displayName": "Call Context", "type": "object" } }, "methods": {}, "events": { "acceptButtonClicked": { "displayName": "acceptButtonClicked", "description": "Will be called when accept button is clicked.", "bubbles": true, "cancelable": true, "detail": { } }, "disconnectButtonClicked": { "displayName": "disconnectButtonClicked", "description": "Will be called when disconnect button is clicked.", "bubbles": true, "cancelable": true, "detail": { } } }, "slots": {} }