Skip to content

Lab 3 - Scrolling Queue Statistics

Story

As a contact center director, I want my agents to have scrolling text on their agent desktop with call queue statistics for calls which they are eligible to receive, so they can see them without looking at another screen.

Requirements

  1. Queues the agent is eligible to receive calls from:
    • Queues which include the agent's team
    • Direct skilled queues where the agent's skills match
    • Agent assigned queues where the agent is assigned
  2. For all calls:
    • List the queue name
    • List the number of calls
    • Show the longest wait time in queue
  3. Queue statistics should scroll on the agent desktop
    • Data should update automatically

Data and Actions

List all queues assigned to the agent by the team they are logged into

Navigate to List references for a specific Team API

Get the team ID for your team from Control Hub > Contact Center > Teams

Fill in the required details
Click to run the request
Copy the cURL and import it into Postman


List all skilled queues from which the agent can receive calls

Navigate to List skill based Contact Service Queue(s)by user ID

Get the userId for from Control Hub > Contact Center > Contact Center Users

Fill in the required details
Click to run the request
Copy the cURL and import it into Postman


List all queues in which the agent is directly assigned

Navigate to List agent based Contact Service Queue(s)by user ID
Use the same userId as the previous request to fill in the required details
Click to run the request
Copy the cURL and import it into Postman


Did you notice anything interesting about the API calls and responses?
  • All three use the same method (GET)
  • The returned JSON has nearly the same structure
  • All three calls are using the same headers
  • All three are using the same URL root

Keep Postman open as we will be using the information in a future step


Use the Search API to retrieve the number of contacts and oldest contact createdTime for the queues which the agent is assigned

For this query you will be using aggregations and a compound filter to retrieve the queue statistics.

Open the Search API GraphQL Workbench

Click the Add New button

Use the Authorization Tool (Tools > Authorization)

Login:
Password: password

Copy the authorization header into the environment variables

Set the URL: https://api.wxcc-us1.cisco.com/search

Open the Docs pane and add the query for task using the ADD QUERY button
Remove the has section
Remove the intervalInfo and pageInfo sections
Remove all fields in the tasks section except lastQueue and aggregation
After lastQueue add: {name}
After aggregation add: {name value} In the Arguments section of the query, remove the lines for aggregation, aggregationInterval, and pagination
Click the suitcase icon and select Prettify


Creating the aggregations

Abstract

An Aggregation can return a count, sum, average, max, min, or cardinality of a field along with a name you provide. They can also have their own set of filters which can be used to further refine the data into the information you require.
In their most basic form an aggregation is represented like: { field: "string", type: count, name: "string" } inside an array. They can be further bifurcated by having other fields in the query. In our case we are going to slice our aggregations based on the lastQueue where they were assigned.

Create an aggregation to return the count of contact

{ field: "id", type: count, name: "contacts" }

We are returning the count of the task IDs and naming is "contacts"

Replace the aggregating template example in the query with your new aggregation leaving the square brackets. Then press enter twice to move the closing square bracket down to make room for the next aggregation.

Create an aggregation to return the min createdTime and name it oldestStart

{ field: "createdTime", type: min, name: "oldestStart" }

We are returning the min or lowest Epoch time

Add this aggregation directly below the one you just created. Prettify your query.

Check your Aggregations


Creating the compound filter

Abstract

In the previous lab, you used an and filter group to exclude records and fields which did not match the filter criteria. In this query you will be nesting an inclusive or filter group inside an excluding and filter group.

Inside the curly braces of the filter, type: and:[], then press enter between the square brackets

How would you add a filter if you only want to return contacts which are active and in queue?
{isActive:{equals:true}}
{status:{equals:"parked"}}

After adding the previous filters inside the and group, on the next line type: {or:[]}, then press enter between the square brackets.
Using the Queue IDs returned from the List references for a specific Team API query you have in Postman:

For each of the Queue IDs add a new filter inside the or square brackets with the Queue ID inside the quotes for equals:
{lastQueue:{id:{equals:""}}}

Check your filters


Testing the query

Place a call to your assigned inbound number:

You can mute the volume for the call as you will not actually be answering the call.

Use the time tool to set the from: 1 day ago and to: Now
Press the Send Request button
You can end the call once your query has returned the results


Optimizing and exporting the query

Using the options in the suitcase menu

Refactor the query
Rename the query to queueStats
Export the cURL and import it into Postman

Do not close the browser tab

In Postman:

Uncheck all headers except: Content-Type and Accept
Add an Authorization Header:
Key: Authorization
Value: Bearer placeHolder
Change the body type to JSON and Beautify it


Creating the Web Component

Create a new Web Component

Create a new file in the src directory named queue-scroll.ts

In the new file, type (not paste) littemplate

Select the Create LitElement Component With lit-html


Create Properties for the required variables

@property() token?: string
@property() orgId?: string
@property() teamId?: string
@property() agentId?: string


Create States for the required data elements

@state() queueStats = []
@state() queueFilter: object[] = []


Create a new async method to query the Search API

async getStats(){}
In Postman, use the code feature to generate a JavaScript - Fetch using the async/await options and press copy
Between the curly braces of the getAgents method, press enter then paste the copied code from postman
In the headers section of the method, find the Authorization header and change "Bearer placeHolder" to `Bearer ${this.token}`
In the raw section which holds the stringified JSON:

Change the from variable value to represent a time 24 hours (86400000 ms) before the current time (Date.now()) using a string literal expression: `${Date.now() - 86400000}`

Change the to variable value to represent the time now: `${Date.now()}`

Set the type of requestOptions to be an object by adding this notation, after its name and before the equals sign: : object

In the try section of the method:

Change result to equal: response.json() instead of response.text()


Return to the GraphQL Workbench to understand how to use the returned data

In the center pane of Altair, copy the data returned from the query
Open JSON Path Finder
Paste the copied data into the left pane
In the right pane, navigate until you find the array of queues and their aggregations

What is the JSON path which will return the array of queues and their aggregations?

x.data.task.tasks

What is the JSON path for the value of the queue name in the first array item?

x.data.task.tasks[0].lastQueue.name

What is the JSON path for the value of the oldestStart aggregation in the first array item?

x.data.task.tasks[0].aggregation[0].value

What is the JSON path for the value of the number of contacts in the first array item?

x.data.task.tasks[0].aggregation[1].value


Use map to create an unordered list item for each item returned in the query

Abstract

In this line of code you are going set the value of the state queueStats using map on the JSON results from the Graph QL query. For each item in the array of queue information, you are going to create a list item of an unordered list using an html template. Each list item will include; the queue name, number of contacts, and how long the contact has been in the queue.

In the try section of the getStats method, below the line const result = await response.json();:

Insert this updated line of code using the information from JSON path Finder:
this.queueStats = await result.(JSON path to the array items).map((item: any) =>{})

this.queueStats = await result.data.task.tasks.map((item: any) => {})

In the curly bracket of the arrow function use insert this html template then update the template to use the array values:

return html`<li> | Queue: ${<replace with queue name>} Contacts: ${<replace with the count of contacts>} Wait: ${new Date(Date.now() - <replace with the oldestSrart time>).toISOString().slice(11, -5)} |</li>`

Answer

return html`<li> | Queue: ${item.lastQueue.name} Contacts: ${item.aggregation[1].value} Wait: ${new Date(Date.now() - item.aggregation[0].value).toISOString().slice(11, -5)} |</li>`


Update the render method

Add this code, which includes an unordered list and a temporary testing button, into the html template of the render method:

        <button @click=${this.getStats}>test</button>     
        <div class="marquee-container">
            <ul class="marquee">
                ${this.queueStats}
                ${this.queueStats}
            </ul>
        </div>

Save the file


Add to index.html

In the index.html:

Add the script tag for this web component into the header

<script type="module" src="/src/queue-scroll.ts"></script>

Add the web component's html tag and pass the property values for token, orgId, teamId, and agentId

Fill in the empty values:
<queue-scroll token="" orgId="" teamId="" agentId=""></queue-scroll>

Save the file


Start the Development server and test

In the terminal of VS Code run: yarn dev
Launch the development server index page

You should see your web component in the browser

Place a call to your inbound number
Press the test button on the widget after you hear hold music

You should see two identical entries appear with the Queue Name, Number of Contacts, and longest wait time.
There is a reason which there are 2 identical entries which will be discussed later in the lab.

Press the test button again.

The Longest Wait time should update.

Disconnect the call.
Press the test button again.

The queue information should disappear.


Create a new async method to aggregate a list of queues for the agent signed in and update the query filter with those queue values

Abstract

There are three APIs you will need to call in order to retrieve a full list of queues from which the agent may receive calls. However, this does not mean that you will need three different methods added to your web component, and with the URL paths and returned JSON being very similar, you have the ability to reuse code.

The URLs are formatted with the root: https://api.wxcc-us1.cisco.com/organization/{orgId}
And the paths:
/team/{teamId}/incoming-references
/v2/contact-service-queue/by-user-id/{agentId}/skill-based-queues
/v2/contact-service-queue/by-user-id/{agentId}/agent-based-queues

async getQueues(){}
In Postman, copy the code (Javascript - Fetch with async/await) for the List references for a specific Team API call Between the curly braces of the getAgents method, press enter then paste the copied code from postman

Update the Authorization header to use the property token

`Bearer ${this.token}`

Bellow the headers, add a new constant which holds an array of the URL paths:

const paths = [`/v2/contact-service-queue/by-user-id/${this.agentId}/agent-based-queues`, `/v2/contact-service-queue/by-user-id/${this.agentId}/skill-based-queues`, `/team/${this.teamId}/incoming-references`]

On the next line, make sure that the state queueFilter is an empty array before you add values by setting like this: this.queueFilter = []
Set the type of requestOptions to be an object by adding this notation, after its name and before the equals sign: : object

Abstract

In the next steps you will use a forEach method iterate through the paths array, making an API call "for each" path in the array. During each iteration you will use another forEach method to iterate through the returned data, then add a formatted object to the queueFilter array. After creating the queueFilter, you will call the getStats() method to populate the queue information.

Wrap the try/catch section in a forEach method:

On the line above the try, insert: paths.forEach(async (path, i) => {
Below the closing curly brace of the catch. insert: })

Change the URL in the fetch command to `https://api.wxcc-us1.cisco.com/organization/${this.orgId}${path}`
Change result to equal: response.json() instead of response.text()
Add the forEach to iterate through the returned data and push the formatted object into the queueFilter array:

result.data.forEach((q: any) => this.queueFilter.push({ lastQueue: { id: { equals: q.id } } }))

After the closing curly brace of the catch and before the closing }), add an if statement to check if the forEach method has completed so that you can call the getStats method and populate the UI:

if (i >= paths.length - 1) {this.getStats()}


Update the get Stats method to use the new filter

In the getStats method, find the or filter group in the query variables:

Replace the square braces and JSON contained in the square braces with: this.queueFilter

Show me


Update the testing button in the render method

In the html template of the render method:

On the test button, change the @click from this.getStats to this.getQueues


Testing

Call your assigned DN to place a call in the queue dn
Once the call is in queue, press the testing button

You should see two identical entries appear with the Queue Name, Number of Contacts, and longest wait time.

Disconnect the call.


Add CSS Styling

Replace the static CSS with this CSS

            :host {
            display: flex;
            }
            .marquee-container {
            width: 30vw;
            height: 50px; /* Set a fixed height for the container */
            overflow: hidden; 
            border:solid;
            border-radius:25px;
            }

            .marquee {
            list-style: none; /* Remove default list styles */
            display:flex;
            padding: 0;
            margin: 0;
            height:100%;
            width:max-content;
            animation: scroll linear infinite;
            animation-duration: 10s;
            align-items:center;
            }
            .marquee li {
            display:flex;
            align-self:center;
            align-items:center;
            justify-content:center;
            flex-shrink:0;
            font-size:2rem;
            white-space:nowrap;
            padding: 0 1rem 0 1rem;
            }
            .marquee:hover{
            animation-play-state: paused;

            }

            @keyframes scroll {
            0% {
                transform: translateX(0); /* Start position */
            }
            100% {
                transform: translateX(-50%); /* End position (fully scrolled) */
            }
            }

Save the file


Testing

Call your assigned DN to place a call in the queue dn
Once the call is in queue, press the testing button

You should see the queue stats scrolling in your web component.

Hover over the scrolling queue information

The scrolling should stop.
Note that the time value is not incrementing.

Disconnect the call


Add auto load and refresh functionality

Abstract

  • Instead of updating the widget data only when you push a button, you can execute your code automatically once the web component is loaded using the connectedCallback lifecycle.
  • You can also employ the setInterval method to run parts of your code on a set interval to refresh your data.
  • Since the queues the agent is assigned are not likely to change often, you only need to execute getQueues when the agent logs in.
  • If you are setting up event listeners or timers via connectedCallback, it is a best practice to remove them from memory when you unload the web component using the disconnectedCallback lifecycle. In order to clear the setInterval, you will create a state so that you can reference it with later.

Add a new state @state() _timerInterval?: any
Add the connectedCallback and disconnectedCallback methods

    connectedCallback() {
        super.connectedCallback()
        this.getQueues()
        this._timerInterval = setInterval(() => this.getStats(), 30000);
    }
    disconnectedCallback() {
        super.disconnectedCallback();
        clearInterval(this._timerInterval);
    }

Remove the testing button from the html template of the render method.
Save the file.


Testing

Call your assigned DN to place a call in the queue dn
Once the call is in queue you should see the queue stats scrolling in your web component.

You should see the wait time increment by 30 seconds as long as you are still in the queue.

Disconnect the call.

Note that after disconnecting the call, the data still will not update until the 30 second timer hits.


Fine tuning the experience

Abstract

You may have noticed that the scroll speed and the time in queue fields are static, meaning that it will take 10 seconds for the entire list of queue stats to scroll by regardless of the number of queues you are displaying and the longest queue time only updates when the data gets updated. In this step, you are going to address both shortcomings with just a few lanes of code.

Create a new state to hold the data returned from the search API @state() queueData?: any
Create a new state to hold the reference for a map update interval: @state() mapUpdate?: any
In the getStats method after the const result = await response.json(); line, add: this.queueData = await result.data.task.tasks
Remove the line which maps the returned data in the list items
Create a new method named: updateTemplate(){}
In the curly braces of updateTemplate, paste this code: this.queueStats = this.queueData.map((item: any) => { return html`<li> | Queue: ${item.lastQueue.name} Contacts: ${item.aggregation[1].value} Wait: ${new Date(Date.now() - item.aggregation[0].value).toISOString().slice(11, -5)} |</li>` })
In the connectedCallback method add: this.mapUpdate = setInterval(() => this.updateTemplate(), 1000);
In the disconnectedCallback method add: clearInterval(this.mapUpdate);
In the ul opening tag of the render method, after class="marquee", add: style="animation-duration: ${this.queueStats.length * 10}s"
Save the file.


Testing

Call your assigned DN to place a call in the queue dn
Once the call is in queue you should see the queue stats scrolling in your web component.

You should see the wait times increment by every second.

Disconnect the call.

Notice that the call time will continue to increment even though the call is no longer in the queue. Why?

The data is only updating every 30 seconds, but the UI is updating every second.


Add to Desktop Layout

In the agent section of your desktop layout JSON file, locate the advancedHeader area.

Add this JSON above the entry for the digital-outbound component

{
    "comp": "queue-scroll",
    "properties": {
        "orgId": "$STORE.agent.orgId",
        "token": "$STORE.auth.accessToken",
        "teamId": "$STORE.agent.teamId",
        "agentId": "$STORE.agent.agentId"
    },
    "script": "http://localhost:4173/index.js"
},

Save the JSON file as yourTeamName.json
Upload the JSON file


Testing

In the terminal of VS Code, press ctrl + c to terminate the development server
In the terminal of VS Code run: yarn game
Log into the Agent Desktop

Login:
Password: password
Team: team


Call your assigned DN to place a call in the queue dn

Run the tests you think will test the functionality.