Added lint and format scripts

This commit is contained in:
Peter Morton 2023-06-27 13:59:29 -05:00
parent 689554d78a
commit 794fb59842
28 changed files with 2993 additions and 742 deletions

10
.eslintrc.cjs Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
env: {
node: true,
},
extends: ["eslint:recommended", "plugin:vue/vue3-recommended", "prettier"],
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
},
};

1
.prettierrc.json Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -1,24 +1,31 @@
# client # client
The is a single page application built using Vite (https://vitejs.dev/)
## Project setup ## Project setup
``` ```
npm install npm install
``` ```
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
``` ```
npm run serve npm run dev
``` ```
### Compiles and minifies for production ### Compiles and minifies for production
``` ```
npm run build npm run build
``` ```
### Lints and fixes files ### Lints and fixes files
``` ```
npm run lint npm run lint
``` ```
### Customize configuration ### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/). See [Configuration Reference](https://cli.vuejs.org/config/).

2174
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,9 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
}, },
"dependencies": { "dependencies": {
"d3": "^7.8.5", "d3": "^7.8.5",
@ -18,6 +20,10 @@
"devDependencies": { "devDependencies": {
"@types/d3-sankey": "^0.12.1", "@types/d3-sankey": "^0.12.1",
"@vitejs/plugin-vue": "^3.1.0", "@vitejs/plugin-vue": "^3.1.0",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-vue": "^9.15.1",
"prettier": "2.8.8",
"vite": "^3.1.0" "vite": "^3.1.0"
} }
} }

View File

@ -1,75 +1,75 @@
{ {
"nodes": [ "nodes": [
{ {
"name": "Email", "name": "Email",
"category": "Channel" "category": "Channel"
}, },
{ {
"name": "Messaging", "name": "Messaging",
"category": "Channel" "category": "Channel"
}, },
{ {
"name": "Legacy Live Chat", "name": "Legacy Live Chat",
"category": "Channel" "category": "Channel"
}, },
{ {
"name": "Live Chat", "name": "Live Chat",
"category": "Sub Channel" "category": "Sub Channel"
}, },
{ {
"name": "Facebook Messenger", "name": "Facebook Messenger",
"category": "Sub Channel" "category": "Sub Channel"
}, },
{ {
"name": "Twitter DM", "name": "Twitter DM",
"category": "Sub Channel" "category": "Sub Channel"
}, },
{ {
"name": "WhatsApp", "name": "WhatsApp",
"category": "Sub Channel" "category": "Sub Channel"
}, },
{ {
"name": "Other", "name": "Other",
"category": "Sub Channel" "category": "Sub Channel"
}, },
{ {
"name": "Default", "name": "Default",
"category": "Queue" "category": "Queue"
}, },
{ {
"name": "General Enquires", "name": "General Enquires",
"category": "Queue" "category": "Queue"
}, },
{ {
"name": "Complaints", "name": "Complaints",
"category": "Queue" "category": "Queue"
}, },
{ {
"name": "Case Closed", "name": "Case Closed",
"category": "Outcome" "category": "Outcome"
}, },
{ {
"name": "Case Updated", "name": "Case Updated",
"category": "Outcome" "category": "Outcome"
}, },
{ {
"name": "Completed", "name": "Completed",
"category": "Outcome" "category": "Outcome"
}, },
{ {
"name": "Escalated to Manager", "name": "Escalated to Manager",
"category": "Outcome" "category": "Outcome"
}, },
{ {
"name": "No Need for response", "name": "No Need for response",
"category": "Outcome" "category": "Outcome"
} }
], ],
"links": [ "links": [
{ {
"source": "Email", "source": "Email",
"target": "Default", "target": "Default",
"value": 1000 "value": 1000
} }
] ]
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<router-link to="/">Home</router-link> <router-link to="/">Home</router-link>
<router-link to="/about">About</router-link> <router-link to="/about">About</router-link>
<router-link to="/referenceId">Reference ID Tracker</router-link> <router-link to="/referenceId">Reference ID Tracker</router-link>
<router-link to="/interactionsSankey">Interaction Sankey</router-link> <router-link to="/interactionsSankey">Interaction Sankey</router-link>
<router-view></router-view> <router-view></router-view>
</template> </template>

View File

@ -1,47 +1,50 @@
<script setup> <script setup>
import HandledBy from './HandledBy.vue' import HandledBy from "./HandledBy.vue";
import EmailInteraction from './EmailInteraction.vue' import EmailInteraction from "./EmailInteraction.vue";
import DateTime from './DateTime.vue' import DateTime from "./DateTime.vue";
import OutcomeList from './OutcomeList.vue' import OutcomeList from "./OutcomeList.vue";
import CustomerList from './CustomerList.vue' import CustomerList from "./CustomerList.vue";
import NotesList from './NotesList.vue' import NotesList from "./NotesList.vue";
// eslint-disable-next-line // eslint-disable-next-line
const props = defineProps(["tableData"]); const props = defineProps(["tableData"]);
console.log(props.tableData) console.log(props.tableData);
</script> </script>
<template> <template>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Contact ID</th> <th>Contact ID</th>
<th>Start Time</th> <th>Start Time</th>
<th>End Time</th> <th>End Time</th>
<th>Direction</th> <th>Direction</th>
<th>Handled By</th> <th>Handled By</th>
<th>Active Duration (s)</th> <th>Active Duration (s)</th>
<th>Notes</th> <th>Notes</th>
<th>Interaction</th> <th>Interaction</th>
<th>Outcome</th> <th>Outcome</th>
<th>Customer</th> <th>Customer</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="td in tableData" :key="td"> <tr v-for="td in tableData" :key="td">
<td>{{ td.node.systemId }}</td> <td>{{ td.node.systemId }}</td>
<td><DateTime :date="td.node.startTime"/></td> <td><DateTime :date="td.node.startTime" /></td>
<td><DateTime :date="td.node.endTime"/></td> <td><DateTime :date="td.node.endTime" /></td>
<td>{{ td.node.direction }}</td> <td>{{ td.node.direction }}</td>
<td><HandledBy :handledBy="td.node.handledBy"/></td> <td><HandledBy :handled-by="td.node.handledBy" /></td>
<td>{{ td.node.activeDuration }}</td> <td>{{ td.node.activeDuration }}</td>
<td><NotesList :notes="td.node.notes"/></td> <td><NotesList :notes="td.node.notes" /></td>
<td> <td>
<EmailInteraction v-if="td.node.interaction.__typename === 'Email'" :email="td.node.interaction" /> <EmailInteraction
</td> v-if="td.node.interaction.__typename === 'Email'"
<td><OutcomeList :outcomes="td.node.outcome"/></td> :email="td.node.interaction"
<td><CustomerList :customers="td.node.customer"/></td> />
</tr> </td>
</tbody> <td><OutcomeList :outcomes="td.node.outcome" /></td>
</table> <td><CustomerList :customers="td.node.customer" /></td>
</template> </tr>
</tbody>
</table>
</template>

View File

@ -4,65 +4,67 @@ defineProps(["summary"]);
</script> </script>
<template> <template>
<div v-if="summary" class="stats"> <div v-if="summary" class="stats">
<div class="item"> <div class="item">
<div class="measure">{{ summary.totalCount }}</div> <div class="measure">{{ summary.totalCount }}</div>
<div class="label">Contacts Found</div> <div class="label">Contacts Found</div>
</div>
<div class="item">
<div class="measure">{{ summary.totalInboundCount }}</div>
<div class="label">INBOUND Contacts Found</div>
</div>
<div class="item">
<div class="measure">{{ summary.totalHTHours.toFixed(2) }}</div>
<div class="label">Total Handle Time (hours)</div>
</div>
<div class="item">
<div class="measure">{{ summary.activeHTMinutes.toFixed(2) }}</div>
<div class="label">Active Handle Time (minutes)</div>
</div>
<div class="item">
<div class="measure">{{ (summary.totalInboundActiveSeconds / 60).toFixed(2) }}</div>
<div class="label">Total [INBOUND] Active Time (minutes)</div>
</div>
</div> </div>
<div class="item">
<div class="measure">{{ summary.totalInboundCount }}</div>
<div class="label">INBOUND Contacts Found</div>
</div>
<div class="item">
<div class="measure">{{ summary.totalHTHours.toFixed(2) }}</div>
<div class="label">Total Handle Time (hours)</div>
</div>
<div class="item">
<div class="measure">{{ summary.activeHTMinutes.toFixed(2) }}</div>
<div class="label">Active Handle Time (minutes)</div>
</div>
<div class="item">
<div class="measure">
{{ (summary.totalInboundActiveSeconds / 60).toFixed(2) }}
</div>
<div class="label">Total [INBOUND] Active Time (minutes)</div>
</div>
</div>
</template> </template>
<style> <style>
.stats { .stats {
display: flex; display: flex;
} }
.item { .item {
width: 100%; width: 100%;
border: 2px solid rgb(57, 145, 201); border: 2px solid rgb(57, 145, 201);
border-radius: 4px; border-radius: 4px;
background-color: #f7f9fc; background-color: #f7f9fc;
font-weight: bold; font-weight: bold;
white-space: normal; white-space: normal;
margin: 5px; margin: 5px;
padding: 5px; padding: 5px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.measure { .measure {
width: 100%; width: 100%;
color: rgb(57, 145, 201); color: rgb(57, 145, 201);
font-family: 'DejaVu Sans', Ariel, Helvetica, sans-serif; font-family: "DejaVu Sans", Ariel, Helvetica, sans-serif;
text-align: center; text-align: center;
font-size: 16px; font-size: 16px;
padding: 5px; padding: 5px;
} }
.label { .label {
width: 100%; width: 100%;
font-family: 'DejaVu Sans', Ariel, Helvetica, sans-serif; font-family: "DejaVu Sans", Ariel, Helvetica, sans-serif;
font-size: 11px; font-size: 11px;
text-align: center; text-align: center;
padding: 5px; padding: 5px;
} }
</style> </style>

View File

@ -1,11 +1,20 @@
<script setup> <script setup>
// eslint-disable-next-line // eslint-disable-next-line
defineProps(["customers"]); defineProps(["customers"]);
</script> </script>
<template> <template>
<div v-if="customers"> <div v-if="customers">
<ul v-for="customer in customers.edges" :key="customer.node.ref">{{ customer.node.firstName }} {{ customer.node.lastName }} ({{ customer.node.ref }})</ul> <ul v-for="customer in customers.edges" :key="customer.node.ref">
</div> {{
<div v-else>No Customer</div> customer.node.firstName
</template> }}
{{
customer.node.lastName
}}
({{
customer.node.ref
}})
</ul>
</div>
<div v-else>No Customer</div>
</template>

View File

@ -1,12 +1,10 @@
<script setup> <script setup>
// eslint-disable-next-line const props = defineProps({ date: { type: String, required: true } });
const props = defineProps(["date"]);
const dateValue = new Date(props.date)
const date = dateValue.toLocaleDateString()
const time = dateValue.toLocaleTimeString()
const dateValue = new Date(props.date);
const localeDate = dateValue.toLocaleDateString();
const localeTime = dateValue.toLocaleTimeString();
</script> </script>
<template> <template>
<p>{{ date }} {{ time }}</p> <p>{{ localeDate }} {{ localeTime }}</p>
</template> </template>

View File

@ -3,14 +3,19 @@
defineProps(["email"]); defineProps(["email"]);
</script> </script>
<template> <template>
<p>Interaction ID {{ email.systemId }}</p> <p>Interaction ID {{ email.systemId }}</p>
<p>To: <span v-for="address in email.toAddresses" :key="address">{{ address }} </span> </p> <p>
<p>From: {{ email.fromAddress }}</p> To:
<p>Subject: {{ email.subject }}</p> <span v-for="address in email.toAddresses" :key="address"
<p>Thread ID: {{ email.threadId }}</p> >{{ address }}
</span>
</p>
<p>From: {{ email.fromAddress }}</p>
<p>Subject: {{ email.subject }}</p>
<p>Thread ID: {{ email.threadId }}</p>
</template> </template>
<style> <style>
p { p {
margin: 0; margin: 0;
} }
</style> </style>

View File

@ -1,12 +0,0 @@
<script setup>
// eslint-disable-next-line
defineProps(["errorMessage"]);
</script>
<template>
<p v-if="errorMessage">Error: {{ errorMessage }}</p>
</template>
<style>
</style>

View File

@ -0,0 +1,9 @@
<script setup>
// eslint-disable-next-line
defineProps(["message"]);
</script>
<template>
<p v-if="message">Error: {{ message }}</p>
</template>
<style></style>

View File

@ -3,6 +3,6 @@
defineProps(["handledBy"]); defineProps(["handledBy"]);
</script> </script>
<template> <template>
<p v-if="handledBy">{{ handledBy.username }}</p> <p v-if="handledBy">{{ handledBy.username }}</p>
<p v-else>unknown</p> <p v-else>unknown</p>
</template> </template>

View File

@ -3,7 +3,7 @@
defineProps(["notes"]); defineProps(["notes"]);
</script> </script>
<template> <template>
<div v-if="notes"> <div v-if="notes">
<span v-for="note in notes.edges" :key="note">{{ note.node.text }}</span> <span v-for="note in notes.edges" :key="note">{{ note.node.text }}</span>
</div> </div>
</template> </template>

View File

@ -1,10 +1,11 @@
<script setup> <script setup>
// eslint-disable-next-line // eslint-disable-next-line
defineProps(["outcomes"]); defineProps(["outcomes"]);
</script> </script>
<template> <template>
<div v-if="outcomes"> <div v-if="outcomes">
<div v-for="outcome in outcomes.edges" :key="outcome">{{ outcome.node.text }} </div> <div v-for="outcome in outcomes.edges" :key="outcome">
{{ outcome.node.text }}
</div> </div>
</template> </div>
</template>

View File

@ -1,13 +1,13 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/Home.vue"; import HomeView from "../views/HomeView.vue";
import ReferenceID from "../views/ReferenceID.vue"; import ReferenSearchByReferenceViewceID from "../views/SearchByReferenceView.vue";
import InteractionsSankey from "../views/InteractionsSankey.vue"; import InteractionsFlowView from "../views/InteractionsFlowView.vue";
const routes = [ const routes = [
{ {
path: "/", path: "/",
name: "home", name: "home",
component: Home, component: HomeView,
}, },
{ {
path: "/about", path: "/about",
@ -16,13 +16,17 @@ const routes = [
// this generates a separate chunk (about.[hash].js) for this route // this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited. // which is lazy-loaded when the route is visited.
component: () => component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue"), import(/* webpackChunkName: "about" */ "../views/AbouView.vue"),
}, },
{ path: "/referenceId", name: "referenceId", component: ReferenceID },
{ {
path: "/interactionsSankey", path: "/referenceId",
name: "interactionsSankey", name: "referenceId",
component: InteractionsSankey, component: ReferenSearchByReferenceViewceID,
},
{
path: "/interactionsFlow",
name: "interactionsFlow",
component: InteractionsFlowView,
}, },
]; ];

View File

@ -1,113 +1,114 @@
h1 { h1 {
font-size: 18px; font-size: 18px;
line-height: 19px; line-height: 19px;
overflow: hidden; overflow: hidden;
padding: 4px 0 12px 0; padding: 4px 0 12px 0;
min-height: 23px; min-height: 23px;
vertical-align: middle; vertical-align: middle;
font-family: "Lato", sans-serif; font-family: "Lato", sans-serif;
font-weight: 400; font-weight: 400;
border-bottom: 1px solid #c7c7c7; border-bottom: 1px solid #c7c7c7;
margin: 0; margin: 0;
} }
h2 { h2 {
font-size: 14px; font-size: 14px;
line-height: 19px; line-height: 19px;
overflow: hidden; overflow: hidden;
padding: 4px 0 4px 0; padding: 4px 0 4px 0;
min-height: 23px; min-height: 23px;
vertical-align: middle; vertical-align: middle;
font-family: "Lato", sans-serif; font-family: "Lato", sans-serif;
font-weight: 400; font-weight: 400;
border-bottom: 1px solid #c7c7c7; border-bottom: 1px solid #c7c7c7;
} }
h3 { h3 {
border-bottom: 1px solid rgb(199, 199, 199); border-bottom: 1px solid rgb(199, 199, 199);
display: block; display: block;
font-family: "Lato", sans-serif; font-family: "Lato", sans-serif;
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
line-height: 23px; line-height: 23px;
min-height: 23px; min-height: 23px;
overflow: hidden; overflow: hidden;
} }
body { body {
font: 12px/18px "OpenSans", Ariel, sans-serif; font: 12px/18px "OpenSans", Ariel, sans-serif;
} }
p { p {
color: rgb(51, 51, 51); color: rgb(51, 51, 51);
display: block; display: block;
font: normal normal normal normal 12px/18px OpenSans, Arial, sans-serif; font: normal normal normal normal 12px/18px OpenSans, Arial, sans-serif;
font-variant: normal; font-variant: normal;
text-align: left; text-align: left;
visibility: visible; visibility: visible;
white-space: normal; white-space: normal;
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
font: normal normal normal normal 12px/18px OpenSans, Arial, sans-serif; font: normal normal normal normal 12px/18px OpenSans, Arial, sans-serif;
width: 100%; width: 100%;
} }
th { th {
background: rgb(238, 238, 238) none repeat-x scroll 0px 0px / auto padding-box border-box; background: rgb(238, 238, 238) none repeat-x scroll 0px 0px / auto padding-box
border-right: 2px solid rgb(255, 255, 255); border-box;
border-bottom: 1px solid rgb(255, 255, 255); border-right: 2px solid rgb(255, 255, 255);
padding: 3px 6px 2px; border-bottom: 1px solid rgb(255, 255, 255);
text-align: left; padding: 3px 6px 2px;
vertical-align: bottom; text-align: left;
vertical-align: bottom;
} }
td { td {
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
text-align: left; text-align: left;
} }
tr { tr {
border-bottom: 1px solid rgb(199, 199, 199); border-bottom: 1px solid rgb(199, 199, 199);
padding: 4px 0px 3px; padding: 4px 0px 3px;
} }
input { input {
border-radius: 13px; border-radius: 13px;
height: 24px; height: 24px;
border-color: #515151; border-color: #515151;
outline: none; outline: none;
} }
input:focus { input:focus {
border-color: #3395ff; border-color: #3395ff;
outline: none; outline: none;
} }
button { button {
margin-left: 5px; margin-left: 5px;
color: #333333; color: #333333;
background-color: #ccc; background-color: #ccc;
background-image: none; background-image: none;
border: 2px solid transparent; border: 2px solid transparent;
height: 24px; height: 24px;
line-height: 22px; line-height: 22px;
white-space: nowrap; white-space: nowrap;
width: auto; width: auto;
min-width: 0px; min-width: 0px;
font-size: 12px; font-size: 12px;
padding: 0 6px !important; padding: 0 6px !important;
font-family: "OpenSans", Arial, sans-serif; font-family: "OpenSans", Arial, sans-serif;
text-align: center; text-align: center;
background-repeat: no-repeat; background-repeat: no-repeat;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 0 0 0 1px transparent inset; box-shadow: 0 0 0 1px transparent inset;
writing-mode: horizontal-tb !important; writing-mode: horizontal-tb !important;
appearance: auto; appearance: auto;
text-rendering: auto; text-rendering: auto;
align-items: flex-start; align-items: flex-start;
} }
/* button { /* button {
@ -123,60 +124,60 @@ button {
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#dfdfdf', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#dfdfdf', GradientType=0);
} */ } */
button:hover { button:hover {
background-color: #eeeeee; background-color: #eeeeee;
border-color: #007aff; border-color: #007aff;
color: var(--font-black); color: var(--font-black);
box-shadow: 0 1px 1px #ddd; box-shadow: 0 1px 1px #ddd;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
} }
button:active, button:active,
button:active:hover { button:active:hover {
border-color: #007aff; border-color: #007aff;
background-color: #d5d5d5; background-color: #d5d5d5;
color: #007aff; color: #007aff;
box-shadow: 1px 0 1px #ddd; box-shadow: 1px 0 1px #ddd;
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.3); box-shadow: 1px 0 1px rgba(0, 0, 0, 0.3);
} }
button[disabled], button[disabled],
button[disabled]:hover, button[disabled]:hover,
button[disabled]:active { button[disabled]:active {
background-image: none; background-image: none;
border-style: dashed; border-style: dashed;
background-color: transparent; background-color: transparent;
color: #666; color: #666;
border-color: #666; border-color: #666;
box-shadow: none; box-shadow: none;
} }
button:focus { button:focus {
background-color: #eeeeee; background-color: #eeeeee;
} }
.themeLinkStyle { .themeLinkStyle {
color: #007aff; color: #007aff;
} }
.themeLinkStyle:link, .themeLinkStyle:link,
.themeLinkStyle:visited { .themeLinkStyle:visited {
color: #007aff; color: #007aff;
} }
.themeLinkStyle:hover, .themeLinkStyle:hover,
.themeLinkStyle:focus { .themeLinkStyle:focus {
color: #ffffff; color: #ffffff;
} }
.themeLinkStyle:active, .themeLinkStyle:active,
.themeLinkStyle:active:hover { .themeLinkStyle:active:hover {
background-color: #d5d5d5; background-color: #d5d5d5;
} }
.transparentButtonStyle { .transparentButtonStyle {
background-color: transparent; background-color: transparent;
border: 1px solid transparent; border: 1px solid transparent;
} }
.transparentButtonStyle:hover { .transparentButtonStyle:hover {
background-color: transparent; background-color: transparent;
border-color: transparent; border-color: transparent;
} }
.transparentButtonStyle:active { .transparentButtonStyle:active {
background-color: transparent; background-color: transparent;
} }
.transparentButtonStyle[disabled] { .transparentButtonStyle[disabled] {
opacity: 0.6; opacity: 0.6;
cursor: default; cursor: default;
} }

View File

@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>About</h1>
</div>
</template>

5
src/views/AboutView.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>About</h1>
</div>
</template>

View File

@ -1,12 +0,0 @@
<template>
<div class="home">
<h1>Engagement Orchestration - Microservice Examples</h1>
<h2>Introduction</h2>
<p>This site provides a set of microservices to augment Engagement Orchestration using the <a
href="https://em-docs.verint.com/15_3/em-integration/Content/AdaptiveFramework/Adaptive_Framework_Components.htm?Highlight=Adaptive%20Framework">Adaptive
Framework</a> Integration.</p>
<h2>Services Available</h2>
<h3><a href=/referenceId>Reference ID</a></h3>
<p>/referenceId </p>
</div>
</template>

18
src/views/HomeView.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<div class="home">
<h1>Engagement Orchestration - Microservice Examples</h1>
<h2>Introduction</h2>
<p>
This site provides a set of microservices to augment Engagement
Orchestration using the
<a
href="https://em-docs.verint.com/15_3/em-integration/Content/AdaptiveFramework/Adaptive_Framework_Components.htm?Highlight=Adaptive%20Framework"
>Adaptive Framework</a
>
Integration.
</p>
<h2>Services Available</h2>
<h3><a href="/referenceId">Reference ID</a></h3>
<p>/referenceId</p>
</div>
</template>

View File

@ -0,0 +1,353 @@
<script setup>
import * as d3 from "d3";
import { sankey, sankeyLinkHorizontal, sankeyLeft } from "d3-sankey";
import { onMounted } from "vue";
import { v4 as uuidv4 } from "uuid";
const data = {
nodes: [
{
name: "Email",
category: "Channel",
},
{
name: "Messaging",
category: "Channel",
},
{
name: "Legacy Live Chat",
category: "Channel",
},
{
name: "Live Chat",
category: "Sub Channel",
},
{
name: "Facebook Messenger",
category: "Sub Channel",
},
{
name: "Twitter DM",
category: "Sub Channel",
},
{
name: "WhatsApp",
category: "Sub Channel",
},
{
name: "Other",
category: "Sub Channel",
},
{
name: "Default",
category: "Queue",
},
{
name: "General Enquires",
category: "Queue",
},
{
name: "Complaints",
category: "Queue",
},
{
name: "Case Closed",
category: "Outcome",
},
{
name: "Case Updated",
category: "Outcome",
},
{
name: "Completed",
category: "Outcome",
},
{
name: "Escalated to Manager",
category: "Outcome",
},
{
name: "No need for response",
category: "Outcome",
},
],
links: [
{
source: "Email",
target: "Default",
value: 342,
},
{
source: "Messaging",
target: "Live Chat",
value: 232,
},
{
source: "Messaging",
target: "Facebook Messenger",
value: 623,
},
{
source: "Messaging",
target: "Twitter DM",
value: 434,
},
{
source: "Messaging",
target: "WhatsApp",
value: 1243,
},
{
source: "Messaging",
target: "Other",
value: 150,
},
{
source: "Live Chat",
target: "Default",
value: 132,
},
{
source: "Live Chat",
target: "Complaints",
value: 90,
},
{
source: "Live Chat",
target: "General Enquires",
value: 42,
},
{
source: "WhatsApp",
target: "Default",
value: 343,
},
{
source: "WhatsApp",
target: "Complaints",
value: 300,
},
{
source: "WhatsApp",
target: "General Enquires",
value: 523,
},
{
source: "Facebook Messenger",
target: "Default",
value: 143,
},
{
source: "Facebook Messenger",
target: "Complaints",
value: 200,
},
{
source: "Facebook Messenger",
target: "General Enquires",
value: 323,
},
{
source: "Twitter DM",
target: "Default",
value: 143,
},
{
source: "Twitter DM",
target: "Complaints",
value: 50,
},
{
source: "Twitter DM",
target: "General Enquires",
value: 223,
},
{
source: "General Enquires",
target: "Case Closed",
value: 421,
},
{
source: "General Enquires",
target: "Completed",
value: 612,
},
{
source: "General Enquires",
target: "Escalated to Manager",
value: 23,
},
{
source: "General Enquires",
target: "No need for response",
value: 241,
},
{
source: "Complaints",
target: "Case Closed",
value: 21,
},
{
source: "Complaints",
target: "Completed",
value: 12,
},
{
source: "Complaints",
target: "Escalated to Manager",
value: 3,
},
{
source: "Complaints",
target: "No need for response",
value: 41,
},
{
source: "Default",
target: "Completed",
value: 41,
},
{
source: "Default",
target: "Case Updated",
value: 410,
},
{
source: "General Enquires",
target: "Case Updated",
value: 50,
},
{
source: "Complaints",
target: "Case Updated",
value: 410,
},
],
};
onMounted(() => {
// Specify the dimensions of the chart.
const width = 928;
const height = 600;
const format = d3.format(",.0f");
const linkColor = "source-target"; //
// ["static", "#aaa"],
// ["source-target", "source-target"],
// ["source", "source"],
// ["target", "target"]
// Create a SVG container.
const svg = d3
.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");
// Constructs and configures a Sankey generator.
const sankeyGen = sankey()
.nodeId((d) => d.name)
.nodeAlign(sankeyLeft) // d3.sankeyLeft, etc.
.nodeWidth(15)
.nodePadding(10)
.extent([
[1, 5],
[width - 1, height - 5],
]);
// Applies it to the data. We make a copy of the nodes and links objects
// so as to avoid mutating the original.
const { nodes, links } = sankeyGen({
nodes: data.nodes.map((d) => Object.assign({}, d)),
links: data.links.map((d) => Object.assign({}, d)),
});
// Defines a color scale.
const color = d3.scaleOrdinal(d3.schemeCategory10);
// Creates the rects that represent the nodes.
const rect = svg
.append("g")
.attr("stroke", "#000")
.selectAll()
.data(nodes)
.join("rect")
.attr("x", (d) => d.x0)
.attr("y", (d) => d.y0)
.attr("height", (d) => d.y1 - d.y0)
.attr("width", (d) => d.x1 - d.x0)
.attr("fill", (d) => color(d.category));
// Adds a title on the nodes.
rect.append("title").text((d) => `${d.name}\n${format(d.value)} TWh`);
// Creates the paths that represent the links.
const link = svg
.append("g")
.attr("fill", "none")
.attr("stroke-opacity", 0.5)
.selectAll()
.data(links)
.join("g")
.style("mix-blend-mode", "multiply");
// Creates a gradient, if necessary, for the source-target color option.
if (linkColor === "source-target") {
const gradient = link
.append("linearGradient")
.attr("id", (d) => (d.uid = uuidv4()))
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", (d) => d.source.x1)
.attr("x2", (d) => d.target.x0);
gradient
.append("stop")
.attr("offset", "0%")
.attr("stop-color", (d) => color(d.source.category));
gradient
.append("stop")
.attr("offset", "100%")
.attr("stop-color", (d) => color(d.target.category));
}
link
.append("path")
.attr("d", sankeyLinkHorizontal())
.attr(
"stroke",
linkColor === "source-target"
? (d) => `url(#${d.uid})`
: linkColor === "source"
? (d) => color(d.source.category)
: linkColor === "target"
? (d) => color(d.target.category)
: linkColor
)
.attr("stroke-width", (d) => Math.max(1, d.width));
link
.append("title")
.text((d) => `${d.source.name}${d.target.name}\n${format(d.value)} TWh`);
// Adds labels on the nodes.
svg
.append("g")
.selectAll()
.data(nodes)
.join("text")
.attr("x", (d) => (d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6))
.attr("y", (d) => (d.y1 + d.y0) / 2)
.attr("dy", "0.35em")
.attr("text-anchor", (d) => (d.x0 < width / 2 ? "start" : "end"))
.text((d) => d.name);
});
</script>
<template>
<div id="chart"></div>
</template>
<style scoped>
svg {
padding-inline: max(2rem, calc(50% - 24rem));
}
</style>

View File

@ -1,337 +0,0 @@
<script setup>
import * as d3 from 'd3'
import { sankey, sankeyLinkHorizontal, sankeyLeft} from 'd3-sankey';
import { onMounted } from 'vue'
import {v4 as uuidv4} from 'uuid'
const data = {
"nodes": [
{
"name": "Email",
"category": "Channel"
},
{
"name": "Messaging",
"category": "Channel"
},
{
"name": "Legacy Live Chat",
"category": "Channel"
},
{
"name": "Live Chat",
"category": "Sub Channel"
},
{
"name": "Facebook Messenger",
"category": "Sub Channel"
},
{
"name": "Twitter DM",
"category": "Sub Channel"
},
{
"name": "WhatsApp",
"category": "Sub Channel"
},
{
"name": "Other",
"category": "Sub Channel"
},
{
"name": "Default",
"category": "Queue"
},
{
"name": "General Enquires",
"category": "Queue"
},
{
"name": "Complaints",
"category": "Queue"
},
{
"name": "Case Closed",
"category": "Outcome"
},
{
"name": "Case Updated",
"category": "Outcome"
},
{
"name": "Completed",
"category": "Outcome"
},
{
"name": "Escalated to Manager",
"category": "Outcome"
},
{
"name": "No need for response",
"category": "Outcome"
}
],
"links": [
{
"source": "Email",
"target": "Default",
"value": 342
},
{
"source": "Messaging",
"target": "Live Chat",
"value": 232
},
{
"source": "Messaging",
"target": "Facebook Messenger",
"value": 623
},
{
"source": "Messaging",
"target": "Twitter DM",
"value": 434
},
{
"source": "Messaging",
"target": "WhatsApp",
"value": 1243
},
{
"source": "Messaging",
"target": "Other",
"value": 150
},
{
"source": "Live Chat",
"target": "Default",
"value": 132
},
{
"source": "Live Chat",
"target": "Complaints",
"value": 90
},
{
"source": "Live Chat",
"target": "General Enquires",
"value": 42
},
{
"source": "WhatsApp",
"target": "Default",
"value": 343
},
{
"source": "WhatsApp",
"target": "Complaints",
"value": 300
},
{
"source": "WhatsApp",
"target": "General Enquires",
"value": 523
},
{
"source": "Facebook Messenger",
"target": "Default",
"value": 143
},
{
"source": "Facebook Messenger",
"target": "Complaints",
"value": 200
},
{
"source": "Facebook Messenger",
"target": "General Enquires",
"value": 323
},
{
"source": "Twitter DM",
"target": "Default",
"value": 143
},
{
"source": "Twitter DM",
"target": "Complaints",
"value": 50
},
{
"source": "Twitter DM",
"target": "General Enquires",
"value": 223
},
{
"source": "General Enquires",
"target": "Case Closed",
"value": 421
},
{
"source": "General Enquires",
"target": "Completed",
"value": 612
},
{
"source": "General Enquires",
"target": "Escalated to Manager",
"value": 23
},
{
"source": "General Enquires",
"target": "No need for response",
"value": 241
},
{
"source": "Complaints",
"target": "Case Closed",
"value": 21
},
{
"source": "Complaints",
"target": "Completed",
"value": 12
},
{
"source": "Complaints",
"target": "Escalated to Manager",
"value": 3
},
{
"source": "Complaints",
"target": "No need for response",
"value": 41
},
{
"source": "Default",
"target": "Completed",
"value": 41
},
{
"source": "Default",
"target": "Case Updated",
"value": 410
},
{
"source": "General Enquires",
"target": "Case Updated",
"value": 50
},
{
"source": "Complaints",
"target": "Case Updated",
"value": 410
},
]
}
onMounted(() => {
// Specify the dimensions of the chart.
const width = 928;
const height = 600;
const format = d3.format(",.0f");
const linkColor = "source-target" //
// ["static", "#aaa"],
// ["source-target", "source-target"],
// ["source", "source"],
// ["target", "target"]
// Create a SVG container.
const svg = d3
.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");
// Constructs and configures a Sankey generator.
const sankeyGen = sankey()
.nodeId(d => d.name)
.nodeAlign(sankeyLeft) // d3.sankeyLeft, etc.
.nodeWidth(15)
.nodePadding(10)
.extent([[1, 5], [width - 1, height - 5]]);
// Applies it to the data. We make a copy of the nodes and links objects
// so as to avoid mutating the original.
const {nodes, links} = sankeyGen({
nodes: data.nodes.map(d => Object.assign({}, d)),
links: data.links.map(d => Object.assign({}, d))
});
// Defines a color scale.
const color = d3.scaleOrdinal(d3.schemeCategory10);
// Creates the rects that represent the nodes.
const rect = svg.append("g")
.attr("stroke", "#000")
.selectAll()
.data(nodes)
.join("rect")
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("height", d => d.y1 - d.y0)
.attr("width", d => d.x1 - d.x0)
.attr("fill", d => color(d.category));
// Adds a title on the nodes.
rect.append("title")
.text(d => `${d.name}\n${format(d.value)} TWh`);
// Creates the paths that represent the links.
const link = svg.append("g")
.attr("fill", "none")
.attr("stroke-opacity", 0.5)
.selectAll()
.data(links)
.join("g")
.style("mix-blend-mode", "multiply");
// Creates a gradient, if necessary, for the source-target color option.
if (linkColor === "source-target") {
const gradient = link.append("linearGradient")
.attr('id', (d) => (d.uid = uuidv4()))
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", d => d.source.x1)
.attr("x2", d => d.target.x0);
gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", d => color(d.source.category));
gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", d => color(d.target.category));
}
link.append("path")
.attr("d", sankeyLinkHorizontal())
.attr("stroke", linkColor === "source-target" ? (d) => `url(#${d.uid})`
: linkColor === "source" ? (d) => color(d.source.category)
: linkColor === "target" ? (d) => color(d.target.category)
: linkColor)
.attr("stroke-width", d => Math.max(1, d.width));
link.append("title")
.text(d => `${d.source.name}${d.target.name}\n${format(d.value)} TWh`);
// Adds labels on the nodes.
svg.append("g")
.selectAll()
.data(nodes)
.join("text")
.attr("x", d => d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
.attr("y", d => (d.y1 + d.y0) / 2)
.attr("dy", "0.35em")
.attr("text-anchor", d => d.x0 < width / 2 ? "start" : "end")
.text(d => d.name);
})
</script>
<template>
<div id="chart"></div>
</template>
<style scoped>
svg {
padding-inline: max(2rem, calc(50% - 24rem));
}
</style>

View File

@ -1,58 +0,0 @@
<script setup>
import { ref } from 'vue'
import ContactTable from '../components/ContactTable.vue'
import ContactsSummary from '../components/ContactsSummary.vue';
import Error from '../components/Error.vue';
const referenceId = ref('')
const contactData = ref(null)
const errorMessage = ref(null)
function onInput(e) {
referenceId.value = e.target.value
}
function fetchData() {
//clear errors
errorMessage.value = null
contactData.value = null
fetch(`${import.meta.env.VITE_EO_SERVICES_URL}/unified-data-gateway?referenceId=${ referenceId.value }`, {
credentials: "include" // fetch won't send cookies unless you set credentials
}).then (response => {
// check for error response
if (!response.ok) {
// get error message from body or default to response statusText
const error = data || response.statusText;
return Promise.reject(error);
}
response.json().then((data) => {
contactData.value = data;
});
})
.catch(error => {
console.log(error)
errorMessage.value = error;
});
}
</script>
<template>
<div id="app">
<h2>Search Criteria</h2>
<div>
<label for="referenceId">Reference ID: </label>
<input id="referenceId" :value="referenceId" @input="onInput" placeholder="enter Reference ID here" />
<button @click="fetchData">Search Contacts</button>
</div>
<h2>Results</h2>
<ContactsSummary v-if="contactData" :summary="contactData.data.summary" />
<div v-else>No Contacts Found</div>
<ContactTable v-if="contactData" :tableData="contactData.data.findContactsCompletedBetween.edges" />
<Error v-if="errorMessage" :errorMessage="errorMessage" />
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,69 @@
<script setup>
import { ref } from "vue";
import ContactTable from "../components/ContactTable.vue";
import ContactsSummary from "../components/ContactsSummary.vue";
import Error from "../components/Error.vue";
const referenceId = ref("");
const contactData = ref(null);
const errorMessage = ref(null);
function onInput(e) {
referenceId.value = e.target.value;
}
function fetchData() {
//clear errors
errorMessage.value = null;
contactData.value = null;
fetch(
`${import.meta.env.VITE_EO_SERVICES_URL}/unified-data-gateway?referenceId=${
referenceId.value
}`,
{
credentials: "include", // fetch won't send cookies unless you set credentials
}
)
.then((response) => {
// check for error response
if (!response.ok) {
// get error message from body or default to response statusText
const error = response.data || response.statusText;
return Promise.reject(error);
}
response.json().then((data) => {
contactData.value = data;
});
})
.catch((error) => {
console.log(error);
errorMessage.value = error;
});
}
</script>
<template>
<div id="app">
<h2>Search Criteria</h2>
<div>
<label for="referenceId">Reference ID: </label>
<input
id="referenceId"
:value="referenceId"
placeholder="enter Reference ID here"
@input="onInput"
/>
<button @click="fetchData">Search Contacts</button>
</div>
<h2>Results</h2>
<ContactsSummary v-if="contactData" :summary="contactData.data.summary" />
<div v-else>No Contacts Found</div>
<ContactTable
v-if="contactData"
:table-data="contactData.data.findContactsCompletedBetween.edges"
/>
<Error v-if="errorMessage" :error-message="errorMessage" />
</div>
</template>
<style scoped></style>

View File

@ -1,7 +1,7 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import vue from '@vitejs/plugin-vue' import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()] plugins: [vue()],
}) });