coverted to typescript and imported from eo project.

todo: fix api calls to CA
This commit is contained in:
Peter Morton 2023-12-19 00:01:56 -06:00
parent 0fd5b2ec6a
commit b95d2a2a81
23 changed files with 3628 additions and 152 deletions

31
.eslintrc.cjs Normal file
View File

@ -0,0 +1,31 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-essential",
"plugin:vue/vue3-strongly-recommended",
"plugin:vue/vue3-recommended",
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier/skip-formatting",
],
parser: "vue-eslint-parser",
parserOptions: {
ecmaVersion: "latest",
project: ["./tsconfig.json"],
tsconfigRootDir: __dirname,
parser: "@typescript-eslint/parser",
},
rules: {
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_+$",
},
],
},
ignorePatterns: ["/*", "!/src"],
};

View File

@ -8,6 +8,12 @@
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Channel Automation Vue Apps</title>
<script>
function resizeIframe(obj) {
obj.style.height =
obj.contentWindow.document.documentElement.scrollHeight + "px";
}
</script>
</head>
<body>
<div id="app"></div>

3084
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,21 +7,33 @@
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"lint": "eslint --ext .ts,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
},
"dependencies": {
"eslint": "^8.51.0",
"eslint-plugin-vue": "^9.17.0",
"prettier": "^3.0.3",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@rushstack/eslint-patch": "^1.6.1",
"@vue/eslint-config-typescript": "^12.0.0",
"d3": "^7.8.5",
"vue": "^3.3.4",
"vue-cookies": "^1.8.3",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@types/d3": "^7.4.3",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^8.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-vue": "^9.19.2",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-eslint": "^1.8.1",
"vue-tsc": "^1.8.16"
}
}

View File

@ -1,10 +0,0 @@
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'
},
};

View File

@ -4,7 +4,7 @@
<div id="router">
<!-- route outlet -->
<!-- component matched by the route will render here -->
<router-view></router-view>
<router-view />
</div>
</template>

7
src/app.config.ts Normal file
View File

@ -0,0 +1,7 @@
export const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
export const routerBase = import.meta.env.VITE_ROUTER_BASE;
export default {
apiBaseUrl,
routerBase,
};

View File

@ -0,0 +1,79 @@
<script lang="ts" setup>
import * as d3 from "d3";
import { onMounted } from "vue";
const props = defineProps({
score: { type: Number, default: 30 },
});
const id = "csrScoreBar";
const width = 300;
onMounted(() => {
drawScale(id, d3.interpolateRdYlGn);
});
function drawScale(id: string, interpolator: number) {
var data = Array.from(Array(100).keys());
var cScale = d3.scaleSequential().interpolator(interpolator).domain([0, 99]);
var xScale = d3.scaleLinear().domain([0, 99]).range([0, width]);
d3.select("#" + id)
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", (d) => Math.floor(xScale(d)))
.attr("y", 0)
.attr("height", 40)
.attr("width", (d) => {
if (d == 99) {
return 6;
}
return Math.floor(xScale(d + 1)) - Math.floor(xScale(d)) + 1;
})
.attr("fill", (d) => {
if (d >= props.score && d < props.score + 1) {
return "black";
}
return cScale(d);
});
}
</script>
<template>
<div class="crsContainer">
<div></div>
<font-awesome-icon
id="auth"
:icon="['fas', 'user-shield']"
size="2xl"
style="color: #45b408"
/>
Call Risk Score is {{ score }}
<div class="img-overlay-wrap">
<svg :id="id" :width="width" height="40"></svg>
</div>
</div>
</template>
<style>
.crsContainer {
align-items: flex-start;
border: 2px solid #45b408;
border-radius: 5px;
font-family: OpenSans, Ariel, sans-serif;
font-size: 11px;
gap: 8px;
margin: 8px;
padding: 11px 16px 11px 12px;
}
.img-overlay-wrap svg {
/* <= optional, for responsiveness */
display: block;
max-width: 100%;
height: auto;
margin: 8px;
}
</style>

View File

@ -0,0 +1,52 @@
<script lang="ts" setup>
defineProps({
authenticated: { type: Boolean, default: false },
});
</script>
<template>
<div class="transferSummary">
<div v-if="authenticated" class="authboxgreen">
<font-awesome-icon
id="auth"
:icon="['fas', 'user-shield']"
size="2xl"
style="color: #45b408"
/>
Customer Authenticated
</div>
<div v-else class="authboxred">
<font-awesome-icon id="noauth" :icon="['fas', 'user-lock']" size="2xl" />
Customer Not Authenticated
</div>
</div>
</template>
<style>
.authboxgreen,
.authboxred {
margin: 8px;
border-radius: 5px;
padding: 11px 16px 11px 12px;
align-items: flex-start;
gap: 8px;
border-radius: 5px;
font-size: 14px;
font-family: OpenSans, Ariel, sans-serif;
font-size: 11px;
}
.authboxgreen {
border: 2px solid #45b408;
}
.authboxred {
border: 2px solid #e03800;
}
#auth {
color: #45b408;
}
#noauth {
color: #e03800;
}
</style>

View File

@ -0,0 +1,12 @@
<script lang="ts" setup>
defineProps({
message: { type: String, default: "No message property set" },
});
</script>
<template>
<p v-if="message">
{{ message }}
</p>
</template>
<style></style>

View File

@ -0,0 +1,35 @@
<script lang="ts" setup>
defineProps({
title: { type: String, default: "Integration Card" },
doc: {
type: String,
default: "<html><body><p>HTML document</p></body></html>",
},
});
</script>
<template>
<h3>{{ title }}</h3>
<iframe
class="integrationCard"
:srcdoc="doc"
scrolling="no"
onload="resizeIframe(this)"
/>
</template>
<style>
span.customer {
color: #ff0000;
font-weight: bold;
}
span.iva {
color: #0000a0;
font-weight: bold;
}
iframe.integrationCard {
border: 0;
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,108 @@
<script lang="ts" setup>
defineProps({
summary: { type: String, default: "Summary not available" },
});
</script>
<template>
<div class="transferSummaryContainer">
<table>
<tr>
<td>
<svg
id=" Layer_1"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 98 89"
xml:space="preserve"
width="22"
height="22"
fill="none"
>
<g>
<path
class="st0"
d="M78.8,85H19.3c-3.1,0-5.7-2.5-5.7-5.7V33.7c0-3.1,2.5-5.7,5.7-5.7h59.5c3.1,0,5.7,2.5,5.7,5.7v45.6
C84.5,82.5,81.9,85,78.8,85z"
/>
<g>
<circle class="st1" cx="35.7" cy="51.9" r="6.7" />
<circle class="st1" cx="62.1" cy="51.9" r="6.7" />
</g>
<circle class="st0" cx="49" cy="9.1" r="5.1" />
<line class="st0" x1="49" y1="28" x2="49" y2="14.1" />
<path
class="st2"
d="M37.9,74.8c0,0,3.8,0,11.1,0s11.1-2.5,11.1-2.5"
/>
<path
class="st0"
d="M70.5,64.4h-43c-3.4,0-6.2-2.8-6.2-6.2V45.6c0-3.4,2.8-6.2,6.2-6.2h43c3.4,0,6.2,2.8,6.2,6.2v12.7
C76.7,61.7,73.9,64.4,70.5,64.4z"
/>
<path
class="st0"
d="M13.6,64.4h-2.7c-3.4,0-6.2-2.8-6.2-6.2V45.6c0-3.4,2.8-6.2,6.2-6.2h2.7"
/>
<path
class="st0"
d="M84.5,39.4h2.7c3.4,0,6.2,2.8,6.2,6.2v12.7c0,3.4-2.8,6.2-6.2,6.2h-2.7"
/>
</g>
</svg>
</td>
<td style="font-weight: bold">Transfer Bot</td>
</tr>
<tr>
<td />
<td>{{ summary }}</td>
</tr>
</table>
</div>
</template>
<style>
.transferSummaryContainer {
align-items: flex-start;
border: 2px solid #45b408;
border-radius: 5px;
color: white;
font-family: OpenSans, Ariel, sans-serif;
font-size: 11px;
gap: 8px;
margin: 8px;
padding: 11px 16px 11px 12px;
background: var(--button-emphasized-regular, #0c874b);
}
.st0 {
fill: none;
stroke: white;
stroke-width: 3;
stroke-linejoin: round;
stroke-miterlimit: 10;
}
.st1 {
fill: none;
stroke: white;
stroke-width: 2.6179;
stroke-linejoin: round;
stroke-miterlimit: 10;
}
.st2 {
fill: none;
stroke: white;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 10;
}
svg {
margin: 8px;
enable-background: new 0 0 98 89;
}
</style>

View File

@ -1,6 +1,13 @@
<script lang="ts" setup>
// eslint-disable-next-line
const props = defineProps(["label", "value"]);
const props = defineProps({
label: { type: String, default: "" },
value: { type: String, default: "" },
});
const value = props.value;
if (!props.value) {
console.error("No label property");
}
const rows = Math.ceil(props.value.length / 18);
</script>
<template>

54
src/helpers/index.ts Normal file
View File

@ -0,0 +1,54 @@
import { apiBaseUrl } from "../app.config.js";
import type { SessionIdentifier } from "../types/index";
export function getAuthKeyFromProperties(props: SessionIdentifier) {
let authKey;
if (props.sessionIdentifier && props.sessionIdentifier.length > 0) {
authKey = props.sessionIdentifier;
} else {
throw new Error(
"_sessionIdentifier property not found (check query params)",
);
}
return authKey;
}
export function getTenantProperty(key: string, props: SessionIdentifier) {
return new Promise((resolve, reject) => {
if (!props) {
reject("no props provided for authentication");
return;
}
if (!key || key.length == 0) {
reject("no key provided");
return;
}
const authKey = getAuthKeyFromProperties(props);
console.log(`Fetching TPS Property [${key}]`);
fetch(`${apiBaseUrl}/tps/${key}?authKey=${authKey}`, {
credentials: "include", // fetch won't send cookies unless you set credentials
})
.then((response) => {
// check for error response
if (!response.ok) {
reject(response.body || response.statusText);
}
response
.json()
.then((data: { data: { value: string } }) => {
console.log("Found Property:" + JSON.stringify(data));
resolve(data.data.value);
})
.catch((error) => {
console.error(error);
reject(error);
});
})
.catch((error) => {
console.error(error);
reject(error);
});
});
}

View File

@ -1,6 +1,25 @@
import { createApp } from "vue";
import { Component, createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import router from "./router";
createApp(App).use(router).mount("#app");
import VueCookies from "vue-cookies";
/* import the fontawesome core */
import { library } from "@fortawesome/fontawesome-svg-core";
/* import font awesome icon component */
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
/* import specific icons */
import { faUserShield, faUserLock } from "@fortawesome/free-solid-svg-icons";
/* add icons to the library */
library.add(faUserShield);
library.add(faUserLock);
createApp(App as Component)
.use(router)
.use(VueCookies, { expires: "7d" })
.component("font-awesome-icon", FontAwesomeIcon)
.mount("#app");

View File

@ -1,17 +1,22 @@
import { createRouter, createWebHashHistory } from "vue-router";
import Home from "../views/Home.vue";
import Home from "../views/HomePage.vue";
import TelephonyContext from "../views/TelephonyContext.vue";
import { Component } from "vue";
const routes = [
{ path: "/", component: Home },
{ path: "/", component: Home as Component },
{
path: "/telephonyContext/",
name: "telephonyContext",
components: { default: TelephonyContext },
components: { default: TelephonyContext as Component },
props: {
default: (route: { params: any; query: any }) => ({
default: (route: {
params: object;
query: { _sessionIdentifier: string };
}) => ({
...route.params,
...route.query,
sessionIdentifier: route.query._sessionIdentifier,
}),
},
},

3
src/types/index.ts Normal file
View File

@ -0,0 +1,3 @@
export interface SessionIdentifier {
sessionIdentifier: string;
}

19
src/views/DaVinciView.vue Normal file
View File

@ -0,0 +1,19 @@
<script lang="ts" setup>
import TransferSummary from "../components/TransferSummary.vue";
import DaVinciAuthentication from "../components/DaVinciAuthentication.vue";
import CallRiskScoreBar from "../components/CallRiskScoreBar.vue";
defineProps({
summary: { type: String, default: "<h4>Summary not available.</h4>" },
authenticated: { type: Boolean, default: false },
crsScore: { type: Number, default: 63 },
});
</script>
<template>
<div class="davinci">
<h3>Da Vinci AI</h3>
<DaVinciAuthentication :authenticated="authenticated" />
<CallRiskScoreBar :score="crsScore" />
<TransferSummary :summary="summary" />
</div>
</template>

View File

@ -4,9 +4,10 @@
<ul>
<li>
<router-link
to="/telephonyContext?ani=+13125138223&dnis=unknown&queue=GeneralInquires&direction=INBOUND&channel=AmazonConnect&type=Voice&_sessionIdentifier=bc93f1fc"
>Telephony Context</router-link
to="/telephonyContext?ani=+13125138223&dnis=unknown&queue=GeneralInquires&direction=INBOUND&channel=AmazonConnect&type=Voice&transferSummary=summary&integrationCardTitle=title&integrationCardDoc=doc&_sessionIdentifier=bc93f1fc"
>
Telephony Context
</router-link>
</li>
</ul>
</div>

View File

@ -1,6 +1,29 @@
<script lang="ts" setup>
import { ref, onMounted, inject } from "vue";
import { getTenantProperty } from "../helpers/index";
import VerticalLabelValue from "../components/VerticalLabelValue.vue";
defineProps({
import ErrorMessage from "../components/ErrorMessage.vue";
import IntegrationCard from "../components/IntegrationCard.vue";
import DaVinciView from "./DaVinciView.vue";
import type { VueCookies } from "vue-cookies";
import type { SessionIdentifier } from "../types/index";
var user = {
id: 1,
name: "Journal",
session: "25j_7Sl6xDq2Kc3ym0fmrSSk2xV2XkUkX",
};
const $cookies = inject<VueCookies>("$cookies");
if ($cookies) {
$cookies.set("user", user);
// print user name
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
console.log($cookies.get("user").name);
}
const props = defineProps({
channel: { type: String, default: "" },
type: { type: String, default: "" },
queue: { type: String, default: "" },
@ -8,85 +31,112 @@ defineProps({
ani: { type: String, default: "" },
dnis: { type: String, default: "" },
startTime: { type: String, default: "" },
transferSummary: { type: String, default: "" },
username: { type: String, default: "" },
sessionIdentifier: { type: String, default: "" },
transferSummary: { type: String, default: "", required: true },
integrationCardTitle: { type: String, default: "", required: true },
integrationCardDoc: {
type: String,
default: "",
required: true,
},
authenticated: { type: String, default: "false" },
crsScore: { type: String, default: "0" },
});
const errorMessage = ref("");
const transferSummaryValue = ref("");
const integrationCardTitleValue = ref("");
const integrationCardDocValue = ref(props.integrationCardDoc);
onMounted(() => {
fetchData();
});
function setValueFromTPS(
reference: { value: string },
param: string,
props: SessionIdentifier,
) {
if (param.startsWith("tps:")) {
const tpsProperty = param.substring(4, param.length);
getTenantProperty(tpsProperty, props)
.then((value: string) => {
reference.value = value;
})
.catch((err: Error) => {
errorMessage.value = `${err.message} for TPS property ${tpsProperty}`;
});
} else {
reference.value = param;
}
}
function fetchData() {
//clear errors
errorMessage.value = "";
setValueFromTPS(transferSummaryValue, props.transferSummary, props);
setValueFromTPS(integrationCardTitleValue, props.integrationCardTitle, props);
setValueFromTPS(integrationCardDocValue, props.integrationCardDoc, props);
}
</script>
<template>
<h2>Summary</h2>
<div class="customerAccount customer-detail-container" width="100%">
<div class="customer-profile-fields vertical-layout">
<div
class="column-layout"
style="width: 100%; max-width: 100%; height: 100%"
>
<div class="row1 column-layout-row">
<div
class="blockOuterSpacingRight col1 column-layout-cell"
style="width: 25%"
>
<h3>Telephony Data</h3>
<VerticalLabelValue
label="Channel"
:value="channel"
></VerticalLabelValue>
<VerticalLabelValue label="Type" :value="type"></VerticalLabelValue>
<VerticalLabelValue
label="Queue"
:value="queue"
></VerticalLabelValue>
<VerticalLabelValue
label="Direction"
:value="direction"
></VerticalLabelValue>
<VerticalLabelValue label="ANI" :value="ani"></VerticalLabelValue>
<VerticalLabelValue label="DNIS" :value="dnis"></VerticalLabelValue>
</div>
<div
class="blockOuterSpacingRight col2 column-layout-cell"
style="width: 25%"
>
<h3>Da Vinci Transfer Summary</h3>
<ul>
<li>
<span class="customer">Alice</span> is a new employee who needs
access to an application for her work, but she doesn't have the
password.
</li>
<li>
She asked the <span class="iva">IVA</span> but they are unable
to help her
</li>
<li>She tries to guess the password, but fails.</li>
<li>
She uses a hacking tool to find the password, which is
"ilovebing".
</li>
<li>
She logs into the application, unaware that the senior developer
is monitoring her.
</li>
</ul>
<div>
<h2>Summary</h2>
<div class="customerAccount customer-detail-container" width="100%">
<div class="customer-profile-fields vertical-layout">
<div
class="column-layout"
style="width: 100%; max-width: 100%; height: 100%"
>
<div class="row1 column-layout-row">
<div
class="blockOuterSpacingRight col1 column-layout-cell"
style="width: 25%"
>
<h3>Telephony Data</h3>
<VerticalLabelValue label="Channel" :value="channel" />
<VerticalLabelValue label="Type" :value="type" />
<VerticalLabelValue label="Queue" :value="queue" />
<VerticalLabelValue label="Direction" :value="direction" />
<VerticalLabelValue label="ANI" :value="ani" />
<VerticalLabelValue label="DNIS" :value="dnis" />
</div>
<div
class="blockOuterSpacingRight col2 column-layout-cell"
style="width: 25%"
>
<DaVinciView
:summary="transferSummaryValue"
:authenticated="authenticated == 'true'"
:crs-score="Number(crsScore)"
/>
</div>
<div
class="blockOuterSpacingRight col3 column-layout-cell"
style="width: 50%"
>
<IntegrationCard
:title="integrationCardTitleValue"
:doc="integrationCardDocValue"
/>
</div>
</div>
</div>
</div>
</div>
` `
<ErrorMessage v-if="errorMessage" :message="errorMessage" />
</div>
</template>
<style scoped>
@import "https://em28.verint.live/ClientResources/cr/202307201110/-/webclient/themes/default/theme.css";
@import "https://em28.verint.live/ClientResources/cr/202307201110/-/webclient/css/extensions/corecommon.css";
@import "https://shared-eo.verint.live/ClientResources/cr/202307201110/-/webclient/themes/default/theme.css";
@import "https://shared-eo.verint.live/ClientResources/cr/202307201110/-/webclient/css/extensions/corecommon.css";
p {
size: 24pt;
}
span.customer {
color: #ff0000;
font-weight: bold;
}
span.iva {
color: #0000a0;
font-weight: bold;
}
</style>
../types/index

10
src/vite-env.d.ts vendored
View File

@ -1 +1,11 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
readonly VITE_ROUTER_BASE: string;
// more env variables...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

7
tsconfig.eslint.json Normal file
View File

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"include": [
// ...
"babel.config.js"
]
}

View File

@ -1,4 +1,5 @@
import { defineConfig } from "vite";
import eslintPlugin from "vite-plugin-eslint";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
@ -6,6 +7,6 @@ export default defineConfig(({ command }) => {
if (command === "build") {
return { plugins: [vue()], base: "/ca_vue_apps/dist" };
} else {
return { plugins: [vue()] };
return { plugins: [eslintPlugin(), vue()] };
}
});