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
The is a single page application built using Vite (https://vitejs.dev/)
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
npm run dev
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
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": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
},
"dependencies": {
"d3": "^7.8.5",
@ -18,6 +20,10 @@
"devDependencies": {
"@types/d3-sankey": "^0.12.1",
"@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"
}
}

View File

@ -1,75 +1,75 @@
{
"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": 1000
}
]
}
"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": 1000
}
]
}

View File

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

View File

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

View File

@ -4,65 +4,67 @@ defineProps(["summary"]);
</script>
<template>
<div v-if="summary" class="stats">
<div class="item">
<div class="measure">{{ summary.totalCount }}</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 v-if="summary" class="stats">
<div class="item">
<div class="measure">{{ summary.totalCount }}</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>
</template>
<style>
.stats {
display: flex;
display: flex;
}
.item {
width: 100%;
border: 2px solid rgb(57, 145, 201);
border-radius: 4px;
background-color: #f7f9fc;
font-weight: bold;
white-space: normal;
margin: 5px;
padding: 5px;
display: flex;
align-items: center;
width: 100%;
border: 2px solid rgb(57, 145, 201);
border-radius: 4px;
background-color: #f7f9fc;
font-weight: bold;
white-space: normal;
margin: 5px;
padding: 5px;
display: flex;
align-items: center;
}
.measure {
width: 100%;
color: rgb(57, 145, 201);
font-family: 'DejaVu Sans', Ariel, Helvetica, sans-serif;
text-align: center;
font-size: 16px;
padding: 5px;
width: 100%;
color: rgb(57, 145, 201);
font-family: "DejaVu Sans", Ariel, Helvetica, sans-serif;
text-align: center;
font-size: 16px;
padding: 5px;
}
.label {
width: 100%;
font-family: 'DejaVu Sans', Ariel, Helvetica, sans-serif;
font-size: 11px;
text-align: center;
padding: 5px;
width: 100%;
font-family: "DejaVu Sans", Ariel, Helvetica, sans-serif;
font-size: 11px;
text-align: center;
padding: 5px;
}
</style>

View File

@ -1,11 +1,20 @@
<script setup>
// eslint-disable-next-line
defineProps(["customers"]);
</script>
<template>
<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>
</div>
<div v-else>No Customer</div>
</template>
<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>
</div>
<div v-else>No Customer</div>
</template>

View File

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

View File

@ -3,14 +3,19 @@
defineProps(["email"]);
</script>
<template>
<p>Interaction ID {{ email.systemId }}</p>
<p>To: <span v-for="address in email.toAddresses" :key="address">{{ address }} </span> </p>
<p>From: {{ email.fromAddress }}</p>
<p>Subject: {{ email.subject }}</p>
<p>Thread ID: {{ email.threadId }}</p>
<p>Interaction ID {{ email.systemId }}</p>
<p>
To:
<span v-for="address in email.toAddresses" :key="address"
>{{ address }}
</span>
</p>
<p>From: {{ email.fromAddress }}</p>
<p>Subject: {{ email.subject }}</p>
<p>Thread ID: {{ email.threadId }}</p>
</template>
<style>
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"]);
</script>
<template>
<p v-if="handledBy">{{ handledBy.username }}</p>
<p v-else>unknown</p>
</template>
<p v-if="handledBy">{{ handledBy.username }}</p>
<p v-else>unknown</p>
</template>

View File

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

View File

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

View File

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

View File

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