From 920465b1e5109ed728802098fc70e5c98f14e586 Mon Sep 17 00:00:00 2001 From: Alex Tsarapkine Date: Mon, 19 Feb 2024 00:51:14 -0500 Subject: [PATCH 01/11] Added auto worker creation --- src/lib/auth.ts | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 311ebf8..762c1a7 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -17,14 +17,14 @@ export const authOptions: NextAuthOptions = { callbacks: { async jwt({ token, account }: any) { if (account) { - + console.log("we is here"); token.accessToken = account.access_token; token.idToken = account.id_token; token.oktaId = account.providerAccountId; token.groups = account.groups; + token.employeeNumber = account.employeeNumber; } - // Decrypting JWT to check if expired var tokenParsed = JSON.parse( Buffer.from(token.idToken.split('.')[1], 'base64').toString() @@ -34,8 +34,42 @@ export const authOptions: NextAuthOptions = { throw Error('expired token'); } - // Add fields to token that are useful - token.employeeNumber = tokenParsed.employeeNumber; + // Request token from Okta API to get user's employeeNumber + if (!token.employeeNumber) { + const response = await fetch(`${process.env.OKTA_OAUTH2_ISSUER}/api/v1/users/${tokenParsed.sub}`, { + headers: { + 'Authorization': `SSWS ${process.env.OKTA_API_KEY}` + } + }); + const userData = await response.json(); + token.employeeNumber = userData.profile.employeeNumber; + } + + // If employeeNumber is still not found, create a new Twilio worker and assign ID + if (!token.employeeNumber) { + console.log("TIME TO UPDATE") + const accountSid = process.env.TWILIO_ACCOUNT_SID; + const authToken = process.env.TWILIO_AUTH_TOKEN; + const client = require('twilio')(accountSid, authToken); + + client.taskrouter.v1.workspaces(process.env.TWILIO_WORKSPACE_SID) + .workers + .create({friendlyName: tokenParsed.email}) + .then(async (worker: { sid: any; }) => { + token.employeeNumber = worker.sid; + const profile = {'profile':{"employeeNumber": worker.sid}}; + + const response = await fetch(`${process.env.OKTA_OAUTH2_ISSUER}/api/v1/users/${tokenParsed.sub}`, { + method: 'POST', + headers: { + 'Authorization': `SSWS ${process.env.OKTA_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(profile) + }); + }) + } + token.groups = tokenParsed.groups; return token; From af9a828c976ee29d514e46bdd6216c2bae1d50aa Mon Sep 17 00:00:00 2001 From: Alex Tsarapkine Date: Mon, 19 Feb 2024 00:51:37 -0500 Subject: [PATCH 02/11] removed print --- src/lib/auth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 762c1a7..d44d814 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -47,7 +47,6 @@ export const authOptions: NextAuthOptions = { // If employeeNumber is still not found, create a new Twilio worker and assign ID if (!token.employeeNumber) { - console.log("TIME TO UPDATE") const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const client = require('twilio')(accountSid, authToken); From 003d237431bfde714d344966068383bc5bfb3cf2 Mon Sep 17 00:00:00 2001 From: Alex Tsarapkine Date: Mon, 19 Feb 2024 01:15:41 -0500 Subject: [PATCH 03/11] added enqueuing to incoming calls --- src/app/api/voice/route.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/api/voice/route.ts b/src/app/api/voice/route.ts index e4e7aa4..b90bc64 100644 --- a/src/app/api/voice/route.ts +++ b/src/app/api/voice/route.ts @@ -3,6 +3,7 @@ import VoiceResponse from "twilio/lib/twiml/VoiceResponse"; export async function POST(req: NextRequest) { const callerId = process.env.TWILIO_CALLER_ID; + const workflowSid = process.env.TWILIO_WORKFLOW_SID; const resp = new VoiceResponse(); @@ -15,7 +16,9 @@ export async function POST(req: NextRequest) { // then it is an incoming call towards your Twilio.Device. if (bodyTo == callerId) { // Incoming call - const dial = resp.dial(); + resp.say("Please hold"); + resp.enqueue({ workflowSid: workflowSid }); + } else if (bodyTo) { // Outgoing call const dial = resp.dial({ callerId }); From 072053e60966303c90b808fc3fdf70b9c9659efa Mon Sep 17 00:00:00 2001 From: Alex Tsarapkine Date: Mon, 19 Feb 2024 02:27:56 -0500 Subject: [PATCH 04/11] retrieved initials from session --- src/app/dashboard/layout.tsx | 13 +++++++++---- src/components/appbar/index.tsx | 10 +++++----- src/lib/auth.ts | 3 ++- src/types/next-auth.d.ts | 13 +++++++++++++ 4 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 src/types/next-auth.d.ts diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 73c62af..ed3f94a 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -17,11 +17,16 @@ export default function Layout({ if (status === 'loading') { return Loading...; } else if (session) { - console.log(session) - console.log("ASDASDAS") const isProgramManager = true; // TODO - const initials = "AA"; // TODO get initials from user name - + let initials = "AA"; + if (session.user?.name) { + const parts = session.user.name.split(' '); + if (parts.length > 1) { + initials = (parts[0][0] + parts[1][0]).toUpperCase(); + } else if (parts.length > 0) { + initials = parts[0][0].toUpperCase(); + } + } return (
diff --git a/src/components/appbar/index.tsx b/src/components/appbar/index.tsx index 85cb3fc..509dee4 100644 --- a/src/components/appbar/index.tsx +++ b/src/components/appbar/index.tsx @@ -37,7 +37,7 @@ import NotificationsCard from "./NotificationsCard"; import { signOut } from "next-auth/react"; import useCalls from "@/lib/hooks/useCalls"; -// import { useSession } from "next-auth/react"; +import { useSession } from "next-auth/react"; interface AppbarProps extends React.HTMLAttributes { initials: string; @@ -49,12 +49,12 @@ export default function Appbar({ initials, isProgramManager, }: AppbarProps) { - // const { data: session, status } = useSession(); + const { data: session, status } = useSession(); const { inCall, number, makeCall, setNumber, endCall } = useCalls({ - email: "michelleshx462@gmail.com", // TODO replace with okta auth info - workerSid: "WK3b277b4e6a1d67f2240477fa33f75ea4", // session?.employeeNumber, - friendlyName: "michelleshx462", // session?.user.name ?? '', + email: session?.user?.email || '', + workerSid: session?.employeeNumber || '', + friendlyName: session?.user?.name || '', }); return ( diff --git a/src/lib/auth.ts b/src/lib/auth.ts index d44d814..eed8171 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -17,7 +17,6 @@ export const authOptions: NextAuthOptions = { callbacks: { async jwt({ token, account }: any) { if (account) { - console.log("we is here"); token.accessToken = account.access_token; token.idToken = account.id_token; token.oktaId = account.providerAccountId; @@ -80,6 +79,8 @@ export const authOptions: NextAuthOptions = { session.userType = token.userType; session.employeeNumber = token.employeeNumber; session.groups = token.groups; + session.user.email = token.email + session.user.name = token.name return session; }, diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts new file mode 100644 index 0000000..82e5bc8 --- /dev/null +++ b/src/types/next-auth.d.ts @@ -0,0 +1,13 @@ +import NextAuth, { DefaultSession } from "next-auth" + +declare module "next-auth" { + +// extend session interface + interface Session { + employeeNumber: string, + user: { + email: string, + name: string + } + } +} \ No newline at end of file From 6ccc73b816e234d0c090812bed0959c59999d760 Mon Sep 17 00:00:00 2001 From: Alex Tsarapkine Date: Mon, 19 Feb 2024 02:40:43 -0500 Subject: [PATCH 05/11] fixed links --- src/components/appbar/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/appbar/index.tsx b/src/components/appbar/index.tsx index 509dee4..b132797 100644 --- a/src/components/appbar/index.tsx +++ b/src/components/appbar/index.tsx @@ -114,7 +114,7 @@ export default function Appbar({ My Account - + Settings @@ -123,7 +123,7 @@ export default function Appbar({ <> - + Agents From 0d8a453a4689b8405918af3d6175cae6a3ad51ff Mon Sep 17 00:00:00 2001 From: Alex Tsarapkine Date: Tue, 20 Feb 2024 00:32:15 -0500 Subject: [PATCH 06/11] initial agent task queue --- src/app/api/reservations/route.ts | 38 +++++++++++ src/app/api/tasks/route.ts | 24 +++++++ src/app/dashboard/tasks/page.tsx | 103 +++++++++++++++++++++++++++++- src/types/next-auth.d.ts | 1 + 4 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/app/api/reservations/route.ts create mode 100644 src/app/api/tasks/route.ts diff --git a/src/app/api/reservations/route.ts b/src/app/api/reservations/route.ts new file mode 100644 index 0000000..f0d6665 --- /dev/null +++ b/src/app/api/reservations/route.ts @@ -0,0 +1,38 @@ +import { NextRequest } from 'next/server'; +import { twilioClient } from '@/lib/twilioClient'; + +export async function GET(req: NextRequest) { + const workspaceSid = process.env.TWILIO_WORKSPACE_SID || ""; + const workerSid = req.nextUrl.searchParams.get('workerSid'); + + if (!workspaceSid) { + throw new Error('TWILIO_WORKSPACE_SID is not set'); + } + + if (!workerSid) { + throw new Error('Worker sid is not provided'); + } + + try { + const reservations = await twilioClient.taskrouter.v1.workspaces(workspaceSid) + .workers(workerSid) + .reservations + .list(); + + // console.log(reservations) + + const tasks = await Promise.all(reservations.map(async (reservation) => { + const task = await twilioClient.taskrouter.v1.workspaces(workspaceSid) + .tasks(reservation.taskSid) + .fetch(); + return { task: task, reservation: reservation} + })); + + // console.log("DASDASD") + // console.log(tasks) + + return new Response(JSON.stringify(tasks), { status: 200 }); + } catch (error) { + return new Response("something went wrong", { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts new file mode 100644 index 0000000..5f1c133 --- /dev/null +++ b/src/app/api/tasks/route.ts @@ -0,0 +1,24 @@ +import { NextRequest } from 'next/server'; +import { twilioClient } from '@/lib/twilioClient'; + +export async function GET(req: NextRequest) { + const workspaceSid = process.env.TWILIO_WORKSPACE_SID || ""; + const workerSid = req.nextUrl.searchParams.get('workerSid'); + + if (!workspaceSid) { + throw new Error('TWILIO_WORKSPACE_SID is not set'); + } + + try { + const tasks = await twilioClient.taskrouter.v1 + .workspaces(workspaceSid) + .tasks + .list(); + + console.log(tasks); + + return new Response(JSON.stringify(tasks), { status: 200 }); + } catch (error) { + return new Response("something went wrong", { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/dashboard/tasks/page.tsx b/src/app/dashboard/tasks/page.tsx index 0e539f3..aa8994d 100644 --- a/src/app/dashboard/tasks/page.tsx +++ b/src/app/dashboard/tasks/page.tsx @@ -1,7 +1,106 @@ +"use client" + +import { useState, useEffect } from 'react'; +import { useSession } from "next-auth/react"; +import { Button } from '@/components/ui/button'; + +function formatTime(seconds: number) { + const days = Math.floor(seconds / (24 * 60 * 60)); + seconds -= days * 24 * 60 * 60; + const hrs = Math.floor(seconds / (60 * 60)); + seconds -= hrs * 60 * 60; + const mnts = Math.floor(seconds / 60); + seconds -= mnts * 60; + + if (days) return days + (days > 1 ? " days" : " day") + " ago"; + if (hrs) return hrs + (hrs > 1 ? " hours" : " hour") + " ago"; + if (mnts) return mnts + (mnts > 1 ? " minutes" : " minute") + " ago"; + if (seconds) return seconds + (seconds > 1 ? " second" : " second") + " ago"; + +} + export default function Tasks() { + const [tasks, setTasks] = useState([]); + const [activeTasks, setActiveTasks] = useState([]); + const { data: session } = useSession(); + + useEffect(() => { + const fetchTasks = () => { + // fetch('/api/tasks') + // .then(response => response.json()) + // .then(data => setTasks(data)); + fetch('/api/reservations?workerSid=' + session?.employeeNumber) + .then(response => response.json()) + .then(data => { + setTasks(data); + setActiveTasks(data.filter((task: any) => task.reservation.reservationStatus === 'accepted' || task.reservation.reservationStatus === 'pending')); + }); + + }; + + // Fetch tasks immediately and then every 5 seconds + fetchTasks(); + const intervalId = setInterval(fetchTasks, 5000); + + // Clear interval on component unmount + return () => clearInterval(intervalId); + }, []); + return (
-

Tasks

+

Tasks

+

See all unresolved communications with clients here.

+

{activeTasks.length} outstanding task{activeTasks.length == 1 ? "" : "s"}

+ + + + + + + + + + {tasks && Array.isArray(tasks) && tasks + .filter((task: any) => task.reservation.reservationStatus === 'accepted' || task.reservation.reservationStatus === 'pending') + .map((task: any) => ( + + + + + + ))} + +
TaskInitiatedActions
+ {task.task.taskChannelUniqueName === 'voice' ? ( + <>Call {JSON.parse(task.task.attributes).from || "unknown"} + ) : task.task.taskChannelUniqueName === 'chat' ? ( + <>Respond to message from {JSON.parse(task.task.attributes).from || "unknown"} + ) : null} + {formatTime(task.task.age)} + {task.task.taskChannelUniqueName === 'voice' ? ( + + ) : task.task.taskChannelUniqueName === 'chat' ? ( + + ) : null} + + +
); -} +} \ No newline at end of file diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index 82e5bc8..cd54a93 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -5,6 +5,7 @@ declare module "next-auth" { // extend session interface interface Session { employeeNumber: string, + groups: string[], user: { email: string, name: string From feb097e7c3d502f8c7cfaff4c23ec07b1ef5fd7d Mon Sep 17 00:00:00 2001 From: Alex Tsarapkine Date: Tue, 20 Feb 2024 20:13:32 -0500 Subject: [PATCH 07/11] idk --- src/app/api/enqueue_calls/route.ts | 23 ++++++++++ src/app/api/eventcallback/route.ts | 14 ++++++ src/app/dashboard/page.tsx | 6 +++ src/app/dashboard/tasks/page.tsx | 12 ++++-- .../(dashboard)/tasks/IncomingCallModal.tsx | 43 +++++++++++++++++++ src/components/appbar/index.tsx | 13 +++++- src/lib/hooks/useCalls.ts | 43 ++++++++++++++++--- 7 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 src/app/api/enqueue_calls/route.ts create mode 100644 src/app/api/eventcallback/route.ts create mode 100644 src/components/(dashboard)/tasks/IncomingCallModal.tsx diff --git a/src/app/api/enqueue_calls/route.ts b/src/app/api/enqueue_calls/route.ts new file mode 100644 index 0000000..599087a --- /dev/null +++ b/src/app/api/enqueue_calls/route.ts @@ -0,0 +1,23 @@ + +import { NextRequest} from "next/server"; + +const accountSid = process.env.TWILIO_ACCOUNT_SID; +const authToken = process.env.TWILIO_AUTH_TOKEN; +const workspaceSid = process.env.TWILIO_WORKSPACE_SID || ''; +const workflowSid = process.env.TWILIO_WORKFLOW_SID; +const client = require('twilio')(accountSid, authToken); + + + +export async function POST(req: NextRequest ) { + try { + const resp = await client.taskrouter.v1.workspaces(workspaceSid).tasks.create({ + workflowSid, + }); + + return new Response(JSON.stringify(resp), { status: 200 }); + } catch (error) { + return new Response("something went wrong", { status: 500}); + } + +} diff --git a/src/app/api/eventcallback/route.ts b/src/app/api/eventcallback/route.ts new file mode 100644 index 0000000..f95e546 --- /dev/null +++ b/src/app/api/eventcallback/route.ts @@ -0,0 +1,14 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { twilioClient } from '@/lib/twilioClient'; + +export async function POST(req: NextRequest) { + + + console.log("RAAAAA") + // console.log(req) + + const params = req.nextUrl.searchParams; + console.log(Array.from(params.entries())); + + return new NextResponse(null, { status: 204, headers: { 'Content-Type': 'application/json' } }); +} \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 013e5ca..c4e67ce 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,7 +1,13 @@ +"use client" +import IncomingCallModal from "@/components/(dashboard)/tasks/IncomingCallModal"; +import NotificationsCard from "@/components/appbar/NotificationsCard"; + export default function Dashboard() { return (

Dashboard

+ {/* {}} rejectCall={() => {}} /> */} +
); } diff --git a/src/app/dashboard/tasks/page.tsx b/src/app/dashboard/tasks/page.tsx index aa8994d..235ba41 100644 --- a/src/app/dashboard/tasks/page.tsx +++ b/src/app/dashboard/tasks/page.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import { useSession } from "next-auth/react"; import { Button } from '@/components/ui/button'; +import useCalls from '@/lib/hooks/useCalls'; function formatTime(seconds: number) { const days = Math.floor(seconds / (24 * 60 * 60)); @@ -24,6 +25,12 @@ export default function Tasks() { const [activeTasks, setActiveTasks] = useState([]); const { data: session } = useSession(); + const { inCall, number, makeCall, setNumber, endCall } = useCalls({ + email: session?.user?.email || '', + workerSid: session?.employeeNumber || '', + friendlyName: session?.user?.name || '', + }); + useEffect(() => { const fetchTasks = () => { // fetch('/api/tasks') @@ -60,8 +67,7 @@ export default function Tasks() { - {tasks && Array.isArray(tasks) && tasks - .filter((task: any) => task.reservation.reservationStatus === 'accepted' || task.reservation.reservationStatus === 'pending') + {activeTasks && Array.isArray(activeTasks) && activeTasks .map((task: any) => ( @@ -76,7 +82,7 @@ export default function Tasks() { {task.task.taskChannelUniqueName === 'voice' ? ( diff --git a/src/components/(dashboard)/tasks/IncomingCallModal.tsx b/src/components/(dashboard)/tasks/IncomingCallModal.tsx new file mode 100644 index 0000000..6ed4533 --- /dev/null +++ b/src/components/(dashboard)/tasks/IncomingCallModal.tsx @@ -0,0 +1,43 @@ +import { formatPhoneNumber } from '@/lib/utils'; + +export default function IncomingCallModal({ + number, + acceptCall, + rejectCall, +}: { + number: string; + acceptCall: () => void; + rejectCall: () => void; +}) { + return ( +
+
+
+
+
+ Incoming Call +
+
+ {formatPhoneNumber(number)} +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/components/appbar/index.tsx b/src/components/appbar/index.tsx index b132797..be0d319 100644 --- a/src/components/appbar/index.tsx +++ b/src/components/appbar/index.tsx @@ -38,6 +38,7 @@ import { signOut } from "next-auth/react"; import useCalls from "@/lib/hooks/useCalls"; import { useSession } from "next-auth/react"; +import IncomingCallModal from "../(dashboard)/tasks/IncomingCallModal"; interface AppbarProps extends React.HTMLAttributes { initials: string; @@ -51,7 +52,16 @@ export default function Appbar({ }: AppbarProps) { const { data: session, status } = useSession(); - const { inCall, number, makeCall, setNumber, endCall } = useCalls({ + const { + inCall, + number, + makeCall, + setNumber, + endCall, + incomingCall, + acceptCall, + rejectCall + } = useCalls({ email: session?.user?.email || '', workerSid: session?.employeeNumber || '', friendlyName: session?.user?.name || '', @@ -143,6 +153,7 @@ export default function Appbar({
+ {incomingCall && ()} ); } diff --git a/src/lib/hooks/useCalls.ts b/src/lib/hooks/useCalls.ts index 6a285a3..1278320 100644 --- a/src/lib/hooks/useCalls.ts +++ b/src/lib/hooks/useCalls.ts @@ -114,17 +114,27 @@ export default function useCalls({ // }; const initializeDeviceListeners = () => { + console.log("11111111") if (!device.current) return; - + console.log("2222222") device.current.on("registered", function () { + console.log("POPOP") console.log("Twilio.Device Ready to make and receive calls!"); }); + console.log("3333333") + + console.log("device is", device.current) + + device.current.on("error", function (error: { message: string }) { console.log("Twilio.Device Error: " + error.message); }); + console.log("4444444") + device.current.on("incoming", (incomingCall: Call) => { + console.log("55555") setIncomingCall(true); setNumber(incomingCall.parameters.From); @@ -168,6 +178,7 @@ export default function useCalls({ !initialized && workerSid !== undefined ) { + console.log("USEEFFECT," ,email) checkEmail.current = email; const initializeCalls = async () => { await Promise.all([ @@ -220,22 +231,26 @@ export default function useCalls({ */ const makeCall = async (number: string) => { + console.log("dpqpqpqppqpqpqpp ", device.current) if (!device.current) return; - const params = { // get the phone number to call from the DOM To: number, }; + console.log("Making a call to", number); + const newCall = await device.current.connect({ params }); call.current = newCall; // TODO uncomment when taskrouter impelemnted // Turn agent activity status to reserved to prevent agent from receiving incoming calls - // const reservedActivity = agentActivities.current?.find( - // (activity) => activity.friendlyName === "Reserved" - // ); + const reservedActivity = agentActivities.current?.find( + (activity) => activity.friendlyName === "Reserved" + ); + + console.log("ADASA", worker.current?.sid, reservedActivity); // await fetch( // `/api/workers/?workspaceSid=${process.env.NEXT_PUBLIC_WORKSPACE_SID}&workerSid=${worker.current?.sid}`, @@ -334,18 +349,33 @@ export default function useCalls({ * */ async function initializeDevice(client: string) { + console.log("Initializing device", client) const token = await fetch( - `${process.env.NEXT_PUBLIC_URL}/api/token?client=${client}` + `http://localhost:3000/api/token?client=${client}` ); const value = await token.json(); + console.log("VALUE IS ", value) + const device = new Device(value.token, { logLevel: 1, codecPreferences: [Call.Codec.Opus, Call.Codec.PCMU], }); + console.log("REGISTERING DEVICE") + console.log(device) + + device.on('registered', () => { + console.log('sdfsddsfsdATT'); + }); + + device.on('incoming', (connection) => { + console.log("BEING CALLED") + }) + await device.register(); + console.log("RAAAAA") return device; } @@ -380,6 +410,7 @@ const initializeWorker = async ( const token = await tokenResponse.json(); const worker = new Worker(token); + console.log("WORKER IS", worker) await timeout(1000); // For some reason, this is some much needed black magic return worker; } catch (e) { From fa07a6ce512af3e2137e5941267a123ed2b72ae7 Mon Sep 17 00:00:00 2001 From: Alex Tsarapkine Date: Wed, 21 Feb 2024 01:05:58 -0500 Subject: [PATCH 08/11] added call transfer --- src/app/api/reservations/route.ts | 33 ++++++++++++++++++++++++------- src/app/dashboard/tasks/page.tsx | 22 ++++++++++++++------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/app/api/reservations/route.ts b/src/app/api/reservations/route.ts index f0d6665..e3f2e1c 100644 --- a/src/app/api/reservations/route.ts +++ b/src/app/api/reservations/route.ts @@ -1,8 +1,13 @@ import { NextRequest } from 'next/server'; import { twilioClient } from '@/lib/twilioClient'; +const workspaceSid = process.env.TWILIO_WORKSPACE_SID || ""; +const accountSid = process.env.TWILIO_ACCOUNT_SID; +const authToken = process.env.TWILIO_AUTH_TOKEN; + +const client = require('twilio')(accountSid, authToken); + export async function GET(req: NextRequest) { - const workspaceSid = process.env.TWILIO_WORKSPACE_SID || ""; const workerSid = req.nextUrl.searchParams.get('workerSid'); if (!workspaceSid) { @@ -19,20 +24,34 @@ export async function GET(req: NextRequest) { .reservations .list(); - // console.log(reservations) - const tasks = await Promise.all(reservations.map(async (reservation) => { const task = await twilioClient.taskrouter.v1.workspaces(workspaceSid) .tasks(reservation.taskSid) .fetch(); - return { task: task, reservation: reservation} + return { task: task, reservation: reservation } })); - // console.log("DASDASD") - // console.log(tasks) - return new Response(JSON.stringify(tasks), { status: 200 }); } catch (error) { return new Response("something went wrong", { status: 500 }); } +} + +export async function POST(req: NextRequest) { + + try { + const task = req.nextUrl.searchParams.get('taskSid'); + const status = req.nextUrl.searchParams.get('status'); + const reservationSid = req.nextUrl.searchParams.get('reservationSid'); + + client.taskrouter.v1.workspaces(workspaceSid) + .tasks(task) + .reservations(reservationSid) + .update({ reservationStatus: status }) + + return new Response(`updated to ${status}`, { status: 200 }); + + } catch (error) { + return new Response("something went wrong", { status: 500 }); + } } \ No newline at end of file diff --git a/src/app/dashboard/tasks/page.tsx b/src/app/dashboard/tasks/page.tsx index 235ba41..3a03a5a 100644 --- a/src/app/dashboard/tasks/page.tsx +++ b/src/app/dashboard/tasks/page.tsx @@ -31,11 +31,19 @@ export default function Tasks() { friendlyName: session?.user?.name || '', }); + const updateReservation = (reservation: any, status: string) => { + try { + console.log("Updating reservation", reservation, status) + fetch(`/api/reservations?taskSid=${reservation.taskSid}&status=${status}&reservationSid=${reservation.sid}`, { + method: 'POST' + }) + } catch (error) { + console.error("Error updating reservation", error) + } + } + useEffect(() => { const fetchTasks = () => { - // fetch('/api/tasks') - // .then(response => response.json()) - // .then(data => setTasks(data)); fetch('/api/reservations?workerSid=' + session?.employeeNumber) .then(response => response.json()) .then(data => { @@ -96,12 +104,12 @@ export default function Tasks() { ) : null} - + onClick={ () => updateReservation(task.reservation, 'canceled')} + >Dismiss */} ))} From 37125ca7baf0197a5451b0729aaa7c2eb8507988 Mon Sep 17 00:00:00 2001 From: Alex Tsarapkine Date: Wed, 21 Feb 2024 01:09:44 -0500 Subject: [PATCH 09/11] update task list on reject --- src/app/dashboard/tasks/page.tsx | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/app/dashboard/tasks/page.tsx b/src/app/dashboard/tasks/page.tsx index 3a03a5a..572a8fe 100644 --- a/src/app/dashboard/tasks/page.tsx +++ b/src/app/dashboard/tasks/page.tsx @@ -31,6 +31,16 @@ export default function Tasks() { friendlyName: session?.user?.name || '', }); + const fetchTasks = () => { + fetch('/api/reservations?workerSid=' + session?.employeeNumber) + .then(response => response.json()) + .then(data => { + setTasks(data); + setActiveTasks(data.filter((task: any) => task.reservation.reservationStatus === 'accepted' || task.reservation.reservationStatus === 'pending')); + }); + + }; + const updateReservation = (reservation: any, status: string) => { try { console.log("Updating reservation", reservation, status) @@ -40,19 +50,10 @@ export default function Tasks() { } catch (error) { console.error("Error updating reservation", error) } + fetchTasks() } useEffect(() => { - const fetchTasks = () => { - fetch('/api/reservations?workerSid=' + session?.employeeNumber) - .then(response => response.json()) - .then(data => { - setTasks(data); - setActiveTasks(data.filter((task: any) => task.reservation.reservationStatus === 'accepted' || task.reservation.reservationStatus === 'pending')); - }); - - }; - // Fetch tasks immediately and then every 5 seconds fetchTasks(); const intervalId = setInterval(fetchTasks, 5000); From 96bdf48efdece9dcb48503f28b0ef5e66e8bfe69 Mon Sep 17 00:00:00 2001 From: Alex Tsarapkine Date: Wed, 21 Feb 2024 23:59:20 -0500 Subject: [PATCH 10/11] initial working --- src/app/api/enqueue_calls/route.ts | 23 ----- src/app/api/tasks/route.ts | 36 ++++--- src/app/api/voice/route.ts | 3 + src/app/dashboard/page.tsx | 2 +- src/app/dashboard/tasks/page.tsx | 24 +++-- .../(dashboard)/tasks/IncomingCallModal.tsx | 21 ++-- src/lib/hooks/useCalls.ts | 95 +++++++------------ tsconfig.json | 2 +- 8 files changed, 89 insertions(+), 117 deletions(-) delete mode 100644 src/app/api/enqueue_calls/route.ts diff --git a/src/app/api/enqueue_calls/route.ts b/src/app/api/enqueue_calls/route.ts deleted file mode 100644 index 599087a..0000000 --- a/src/app/api/enqueue_calls/route.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import { NextRequest} from "next/server"; - -const accountSid = process.env.TWILIO_ACCOUNT_SID; -const authToken = process.env.TWILIO_AUTH_TOKEN; -const workspaceSid = process.env.TWILIO_WORKSPACE_SID || ''; -const workflowSid = process.env.TWILIO_WORKFLOW_SID; -const client = require('twilio')(accountSid, authToken); - - - -export async function POST(req: NextRequest ) { - try { - const resp = await client.taskrouter.v1.workspaces(workspaceSid).tasks.create({ - workflowSid, - }); - - return new Response(JSON.stringify(resp), { status: 200 }); - } catch (error) { - return new Response("something went wrong", { status: 500}); - } - -} diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 5f1c133..bab1e2f 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -1,23 +1,35 @@ import { NextRequest } from 'next/server'; import { twilioClient } from '@/lib/twilioClient'; -export async function GET(req: NextRequest) { - const workspaceSid = process.env.TWILIO_WORKSPACE_SID || ""; - const workerSid = req.nextUrl.searchParams.get('workerSid'); - if (!workspaceSid) { - throw new Error('TWILIO_WORKSPACE_SID is not set'); - } +const accountSid = process.env.TWILIO_ACCOUNT_SID; +const authToken = process.env.TWILIO_AUTH_TOKEN; +const workspaceSid = process.env.TWILIO_WORKSPACE_SID || ''; +const client = require('twilio')(accountSid, authToken); +export async function POST(req: NextRequest) { try { - const tasks = await twilioClient.taskrouter.v1 - .workspaces(workspaceSid) - .tasks - .list(); + const worker = req.nextUrl.searchParams.get('client'); + const reservation = req.nextUrl.searchParams.get('reservationSid'); + const task = req.nextUrl.searchParams.get('taskSid'); + + console.log(" worker:", worker); + console.log(" reservation:", reservation); + console.log(" task:", task) - console.log(tasks); + client.taskrouter.v1.workspaces(workspaceSid) + .tasks(task) + .reservations(reservation) + .update({ + instruction: 'dequeue', + dequeueFrom: '+16134002002', // The phone number the call is connected from + // to: 'client:atsarapk@uwaterloo.ca' // The client to connect the call to + }) + .then((reservation: { reservationStatus: any; }) => console.log(reservation.reservationStatus)) + .catch((error: any) => console.error(error)); - return new Response(JSON.stringify(tasks), { status: 200 }); + //{"contact_uri":"client:atsarapk@uwaterloo.ca"} + return new Response("dequeued", { status: 200 }); } catch (error) { return new Response("something went wrong", { status: 500 }); } diff --git a/src/app/api/voice/route.ts b/src/app/api/voice/route.ts index b90bc64..89ad415 100644 --- a/src/app/api/voice/route.ts +++ b/src/app/api/voice/route.ts @@ -11,6 +11,7 @@ export async function POST(req: NextRequest) { const queryString = await req.text(); const params = new URLSearchParams(queryString); const bodyTo = params.get("To"); + const bodyFrom = params.get("From") || undefined; // If the request to the /voice endpoint is TO your Twilio Number, // then it is an incoming call towards your Twilio.Device. @@ -18,6 +19,8 @@ export async function POST(req: NextRequest) { // Incoming call resp.say("Please hold"); resp.enqueue({ workflowSid: workflowSid }); + // const dial = resp.dial({ callerId: bodyFrom }); + // dial.client('atsarapk@uwaterloo.ca'); } else if (bodyTo) { // Outgoing call diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index c4e67ce..da358f5 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -7,7 +7,7 @@ export default function Dashboard() {

Dashboard

{/* {}} rejectCall={() => {}} /> */} - + {/* */}
); } diff --git a/src/app/dashboard/tasks/page.tsx b/src/app/dashboard/tasks/page.tsx index 572a8fe..ca6556b 100644 --- a/src/app/dashboard/tasks/page.tsx +++ b/src/app/dashboard/tasks/page.tsx @@ -25,12 +25,6 @@ export default function Tasks() { const [activeTasks, setActiveTasks] = useState([]); const { data: session } = useSession(); - const { inCall, number, makeCall, setNumber, endCall } = useCalls({ - email: session?.user?.email || '', - workerSid: session?.employeeNumber || '', - friendlyName: session?.user?.name || '', - }); - const fetchTasks = () => { fetch('/api/reservations?workerSid=' + session?.employeeNumber) .then(response => response.json()) @@ -38,12 +32,11 @@ export default function Tasks() { setTasks(data); setActiveTasks(data.filter((task: any) => task.reservation.reservationStatus === 'accepted' || task.reservation.reservationStatus === 'pending')); }); - + }; const updateReservation = (reservation: any, status: string) => { try { - console.log("Updating reservation", reservation, status) fetch(`/api/reservations?taskSid=${reservation.taskSid}&status=${status}&reservationSid=${reservation.sid}`, { method: 'POST' }) @@ -53,6 +46,17 @@ export default function Tasks() { fetchTasks() } + const dequeueTask = (reservation: any) => { + try { + fetch(`/api/tasks?taskSid=${reservation.taskSid}&client=${session?.user?.email}&reservationSid=${reservation.sid}`, { + method: 'POST' + }) + } catch (error) { + console.error("Error dequeing reservation", error) + } + fetchTasks() + } + useEffect(() => { // Fetch tasks immediately and then every 5 seconds fetchTasks(); @@ -91,7 +95,7 @@ export default function Tasks() { {task.task.taskChannelUniqueName === 'voice' ? ( @@ -106,7 +110,7 @@ export default function Tasks() { + >Reject {/* + + ) : ( + + )} + + + ); +} \ No newline at end of file