diff --git a/src/app/api/reservations/route.ts b/src/app/api/reservations/route.ts index a7bd201..2759a4c 100644 --- a/src/app/api/reservations/route.ts +++ b/src/app/api/reservations/route.ts @@ -7,6 +7,7 @@ const authToken = process.env.TWILIO_AUTH_TOKEN; const client = require('twilio')(accountSid, authToken); +// TODO: Update to follow proper REST api standards. // get all reservations assigned to a worker, and the task associated with each reservation export async function GET(req: NextRequest) { const workerSid = req.nextUrl.searchParams.get('workerSid'); @@ -39,9 +40,9 @@ export async function GET(req: NextRequest) { } } +// TODO: Update to follow proper REST api standards. // Update reservation status export async function POST(req: NextRequest) { - try { const task = req.nextUrl.searchParams.get('taskSid'); const status = req.nextUrl.searchParams.get('status'); diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 0dfcf80..76a42a9 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -7,6 +7,7 @@ const authToken = process.env.TWILIO_AUTH_TOKEN; const workspaceSid = process.env.TWILIO_WORKSPACE_SID || ''; const client = require('twilio')(accountSid, authToken); +// TODO: Update to follow proper REST api standards. // Dequeue a reservation to connect the call export async function POST(req: NextRequest) { try { diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index ed3f94a..83883fd 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -6,18 +6,26 @@ import Appbar from "../../components/appbar"; import Sidebar from "../../components/sidebar"; import "../../styles/globals.css"; - - +import useCalls from '@/lib/hooks/useCalls'; +import CallsContext from '@/contexts/CallsContext'; export default function Layout({ children, }: { children: React.ReactNode; }) { const { data: session, status } = useSession(); + + const calls = useCalls({ + email: session?.user?.email || "", + workerSid: session?.employeeNumber || "", + friendlyName: session?.user?.name || "", + }); + if (status === 'loading') { return Loading...; } else if (session) { - const isProgramManager = true; // TODO + + const isProgramManager = true; // TODO: Just check session token groups property and see if its contains 'admin' (name not yet determined) let initials = "AA"; if (session.user?.name) { const parts = session.user.name.split(' '); @@ -27,8 +35,10 @@ export default function Layout({ initials = parts[0][0].toUpperCase(); } } + return (
+
+
+
); } else { diff --git a/src/app/dashboard/tasks/page.tsx b/src/app/dashboard/tasks/page.tsx index 3c892a7..feb1b30 100644 --- a/src/app/dashboard/tasks/page.tsx +++ b/src/app/dashboard/tasks/page.tsx @@ -1,56 +1,27 @@ "use client" -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useContext } from 'react'; import { useSession } from "next-auth/react"; import { Button } from '../../../components/ui/button'; import React from 'react'; import { formatPhoneNumber, formatTimeWithUnits } from '../../../lib/utils'; - +import CallsContext from '@/contexts/CallsContext'; export default function Tasks() { - const [tasks, setTasks] = useState([]); - const [activeTasks, setActiveTasks] = useState([]); - const { data: session } = useSession(); - - const fetchTasks = useCallback(() => { - 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')); - }); - }, [session]); - - const updateReservation = (reservation: any, status: string) => { - try { - fetch(`/api/reservations?taskSid=${reservation.taskSid}&status=${status}&reservationSid=${reservation.sid}`, { - method: 'POST' - }) - } catch (error) { - console.error("Error updating reservation", error) - } - fetchTasks() - } - - const dequeueTask = (task: any, number: string) => { - try { - fetch(`/api/tasks?taskSid=${task.reservation.taskSid}&reservationSid=${task.reservation.sid}&number=${number}`, { - method: 'POST' - }) - } catch (error) { - console.error("Error dequeing reservation", error) - } - fetchTasks() - } - - useEffect(() => { - // Fetch tasks immediately and then every 2 seconds - fetchTasks(); - const intervalId = setInterval(fetchTasks, 2000); - // Clear interval on component unmount - return () => clearInterval(intervalId); - }, [fetchTasks]); + const { + inCall, + number, + makeCall, + setNumber, + endCall, + incomingCall, + acceptCall, + rejectCall, + activeTasks, + updateReservation, + dequeueTask + } = useContext(CallsContext); return (
diff --git a/src/components/appbar/Logo.tsx b/src/components/appbar/Logo.tsx index 583029e..01804d9 100644 --- a/src/components/appbar/Logo.tsx +++ b/src/components/appbar/Logo.tsx @@ -4,12 +4,20 @@ import Image from 'next/image'; export default function Logo() { return (
- connie logo + connie logo
asa logo
); diff --git a/src/components/appbar/NotificationsCard.tsx b/src/components/appbar/NotificationsCard.tsx index 68f9fec..fe32f75 100644 --- a/src/components/appbar/NotificationsCard.tsx +++ b/src/components/appbar/NotificationsCard.tsx @@ -1,40 +1,19 @@ import React, { useCallback, useEffect } from "react"; import { formatPhoneNumber, formatTimeWithUnits } from "../../lib/utils"; -import { useSession } from "next-auth/react"; import { MessageSquare, PhoneIncoming } from "lucide-react"; interface NotificationsCardProps { activeTasks: any; - setActiveTasks: (tasks: any) => void; } -export default function NotificationsCard({ activeTasks, setActiveTasks }: NotificationsCardProps) { - const { data: session } = useSession(); - - const fetchTasks = useCallback(() => { - fetch('/api/reservations?workerSid=' + session?.employeeNumber) - .then(response => response.json()) - .then(data => { - setActiveTasks(data.filter((task: any) => task.reservation.reservationStatus === 'accepted' || task.reservation.reservationStatus === 'pending')); - }); - }, [session, setActiveTasks]); - - useEffect(() => { - // Fetch tasks immediately and then every 2 seconds - fetchTasks(); - const intervalId = setInterval(fetchTasks, 2000); - - // Clear interval on component unmount - return () => clearInterval(intervalId); - }, [fetchTasks]); +export default function NotificationsCard({ activeTasks }: NotificationsCardProps) { return ( -
-

Notifications

- {activeTasks && Array.isArray(activeTasks) && activeTasks - .map((task: any) => ( +

Notifications

+ {activeTasks?.length > 0 ? ( + activeTasks.map((task: any) => (
{task.task.taskChannelUniqueName === 'voice' ? : } @@ -54,7 +33,10 @@ export default function NotificationsCard({ activeTasks, setActiveTasks }: Notif

{formatTimeWithUnits(task.task.age)}

- ))} + )) + ) : ( +

No Notifications

+ )}
) } diff --git a/src/components/appbar/index.tsx b/src/components/appbar/index.tsx index 53bda43..fd7d479 100644 --- a/src/components/appbar/index.tsx +++ b/src/components/appbar/index.tsx @@ -1,6 +1,6 @@ "use client"; import Link from "next/link"; -import { useState } from "react"; +import { useState, useContext } from "react"; import { cn } from "../../lib/utils"; import { Menubar } from "../../components/ui/menubar"; @@ -43,6 +43,7 @@ import AgentStatus from "./AgentStatus"; import ClientOnly from "../ClientOnly"; import { useSession } from "next-auth/react"; import IncomingCallModal from "../(dashboard)/tasks/IncomingCallModal"; +import CallsContext from "@/contexts/CallsContext"; interface AppbarProps extends React.HTMLAttributes { initials: string; @@ -64,14 +65,11 @@ export default function Appbar({ endCall, incomingCall, acceptCall, - rejectCall - } = useCalls({ - email: session?.user?.email || '', - workerSid: session?.employeeNumber || '', - friendlyName: session?.user?.name || '', - }); + rejectCall, + activeTasks + } = useContext(CallsContext); - const [activeTasks, setActiveTasks] = useState([]); + // TODO: move this to useCalls hook return (
- {/* TODO replace with Availability status toggle component */} + {/* TODO : Link status component? I dont think it was linked */}
-
+
diff --git a/src/contexts/CallsContext.tsx b/src/contexts/CallsContext.tsx new file mode 100644 index 0000000..673037c --- /dev/null +++ b/src/contexts/CallsContext.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +export type CallsContextType = { + inCall: boolean; + number: string; + makeCall: (number: string) => Promise; + setNumber: React.Dispatch>; + endCall: () => void; + incomingCall: boolean; + acceptCall: () => void; + rejectCall: () => void; + updateReservation: (reservation: any, status: string) => void; + activeTasks: any[]; + dequeueTask: (task: any, number: string) => void; + }; + + +const CallsContext = React.createContext({ + inCall: false, + number: "", + makeCall: async () => {}, + setNumber: () => {}, + endCall: () => {}, + incomingCall: false, + acceptCall: () => {}, + rejectCall: () => {}, + updateReservation: () => {}, + activeTasks: [], + dequeueTask: () => {} +}); + +export default CallsContext; \ No newline at end of file diff --git a/src/lib/hooks/useCalls.ts b/src/lib/hooks/useCalls.ts index 6a7e58c..9f536ae 100644 --- a/src/lib/hooks/useCalls.ts +++ b/src/lib/hooks/useCalls.ts @@ -35,7 +35,45 @@ export default function useCalls({ const [incomingCall, setIncomingCall] = useState(false); const [inCall, setInCall] = useState(false); const [activityName, setActivityName] = useState("Available"); + const [activeTasks, setActiveTasks] = useState([]); + + const fetchActiveTasks = async () => { + try { + fetch('/api/reservations?workerSid=' + worker.current?.sid) + .then(response => response.json()) + .then(data => { + setActiveTasks(data.filter((task: any) => + // task.reservation.reservationStatus === 'accepted' || + task.reservation.reservationStatus === 'pending')); + }); + } catch (e) { + console.error("Failed to update active tasks, ", e); + } + + } + + const updateReservation = (reservation: any, status: string) => { + console.log("UPDATE IS HIT 1") + try { + fetch(`/api/reservations?taskSid=${reservation.taskSid}&status=${status}&reservationSid=${reservation.sid}`, { + method: 'POST' + }) + } catch (error) { + console.error("Error updating reservation", error) + } + } + + const dequeueTask = (task: any, number: string) => { + console.log("DEQUEUE IS HIT") + try { + fetch(`/api/tasks?taskSid=${task.reservation.taskSid}&reservationSid=${task.reservation.sid}&number=${number}`, { + method: 'POST' + }) + } catch (error) { + console.error("Error dequeing reservation", error) + } + } const initializeWorkerListeners = () => { if (!worker.current) return; @@ -46,10 +84,13 @@ export default function useCalls({ }` ); - // change this to something else + fetchActiveTasks(); + + // TODO: Give option to change music var audio = new Audio('https://assets.mixkit.co/active_storage/sfx/933/933-preview.mp3'); audio.play(); + // TODO: Give option to change notification message if (!("Notification" in window)) { alert("This browser does not support desktop notification"); } else if (Notification.permission === "granted") { @@ -62,69 +103,42 @@ export default function useCalls({ }); } - // taskSid.current = reservation.task.sid; - // console.log(`Task attributes are: ${reservation.task.attributes}`); - - // reservation.on("accepted", async (acceptedReservation: Reservation) => { - // console.log(`Reservation ${acceptedReservation.sid} was accepted.`); - - // try { - // // Turn agent activity status to reserved to prevent agent from receiving incoming calls - // const reservedActivity = agentActivities.current?.find( - // (activity) => activity.friendlyName === "Reserved" - // ); - - // await fetch( - // `/api/workers/?workspaceSid=${process.env.NEXT_PUBLIC_WORKSPACE_SID}&workerSid=${worker.current?.sid}`, - // { - // method: "PUT", - // body: JSON.stringify({ - // activitySid: reservedActivity?.sid, - // }), - // } - // ) - // .then(async (data) => { - // setActivityName(reservedActivity?.friendlyName ?? activityName); - // }) - // .catch((e) => alert("Failed to update activity name")); - // } catch (e) { - // console.log(e); - // } - // }); - - // reservation.on("canceled", (acceptedReservation: Reservation) => { - // console.log(`Reservation ${acceptedReservation.sid} was canceled.`); - // setIncomingCall(false); - // call.current?.disconnect(); - // }); - }); + reservation.on("accepted", async (acceptedReservation: Reservation) => { + console.log(`Reservation ${acceptedReservation.sid} was accepted.`); + fetchActiveTasks(); - // /** - // * This section handles any errors during the websocket connection - // */ - // worker.current.on("disconnected", (reservation: Reservation) => { - // alert("You have been disconnected. Please refresh the page to reconnect"); - // }); - - // worker.current.on("error", (reservation: Reservation) => { - // console.log( - // `Reservation ${reservation.sid} has been created for ${ - // worker.current!.sid - // }` - // ); - - // alert("You have been disconnected. Please refresh the page to reconnect"); - // }); - - // worker.current.on("tokenExpired", (reservation: Reservation) => { - // console.log( - // `Reservation ${reservation.sid} has been created for ${ - // worker.current!.sid - // }` - // ); - - // alert("You have been disconnected. Please refresh the page to reconnect"); - // }); + // try { + // // Turn agent activity status to reserved to prevent agent from receiving incoming calls + // const reservedActivity = agentActivities.current?.find( + // (activity) => activity.friendlyName === "Reserved" + // ); + + // await fetch( + // `/api/workers/?workspaceSid=${process.env.NEXT_PUBLIC_WORKSPACE_SID}&workerSid=${worker.current?.sid}`, + // { + // method: "PUT", + // body: JSON.stringify({ + // activitySid: reservedActivity?.sid, + // }), + // } + // ) + // .then(async (data) => { + // setActivityName(reservedActivity?.friendlyName ?? activityName); + // }) + // .catch((e) => alert("Failed to update activity name")); + // } catch (e) { + // console.log(e); + // } + }); + + reservation.on("canceled", (acceptedReservation: Reservation) => { + fetchActiveTasks(); + console.log(`Reservation ${acceptedReservation.sid} was canceled.`); + setIncomingCall(false); + call.current?.disconnect(); + }); + + }); }; const initializeDeviceListeners = () => { @@ -134,11 +148,6 @@ export default function useCalls({ 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); }); @@ -159,22 +168,6 @@ export default function useCalls({ }); incomingCall.on("disconnect", () => { - - // try { - // fetch(`/api/reservations?taskSid=${currentTask?.reservation?.taskSid}&status=completed&reservationSid=${currentTask?.reservation.sid}`, { - // method: 'PUT' - // }) - // } catch (error) { - // console.error("Error updating reservation", error) - // } - // try { - // fetch(`/api/tasks?taskSid=${currentTask?.reservation?.taskSid}&status=completed&reservationSid=${currentTask?.reservation.sid}`, { - // method: 'PUT' - // }) - // } catch (error) { - // console.error("Error updating reservation", error) - // } - // setCurrentTask(null); setInCall(false); setNumber(""); }); @@ -193,7 +186,6 @@ export default function useCalls({ !initialized && workerSid !== undefined ) { - console.log("USEEFFECT,", email) checkEmail.current = email; const initializeCalls = async () => { await Promise.all([ @@ -206,26 +198,10 @@ export default function useCalls({ await initializeWorker(workerSid, email, friendlyName).then( async (newWorker) => { worker.current = newWorker!; - console.log("NEWORKUS") initializeWorkerListeners(); - - // await fetch( - // `/api/workers/?workspaceSid=${ - // process.env.NEXT_PUBLIC_WORKSPACE_SID - // }&workerSid=${newWorker!.sid}` - // ).then(async (data) => { - // const worker = await data.json(); - // setActivityName(worker.worker.activityName); - // }); } ), - // await fetch( - // `/api/activities/?workspaceSid=${process.env.NEXT_PUBLIC_WORKSPACE_SID}` - // ).then(async (data) => { - // const activities = await data.json(); - // agentActivities.current = activities.activities; - // }), ]).then(() => setInitialized(true)); }; @@ -258,27 +234,6 @@ export default function useCalls({ 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" - // ); - - // await fetch( - // `/api/workers/?workspaceSid=${process.env.NEXT_PUBLIC_WORKSPACE_SID}&workerSid=${worker.current?.sid}`, - // { - // method: "PUT", - // body: JSON.stringify({ - // activitySid: reservedActivity?.sid, - // }), - // } - // ) - // .then(async (data) => { - // setActivityName(reservedActivity?.friendlyName ?? activityName); - // }) - // .catch((e) => alert("Failed to update activity name")); - setInCall(true); // Check if I should use this @@ -349,6 +304,9 @@ export default function useCalls({ endCall, rejectCall, disconnectEverything, + activeTasks, + updateReservation, + dequeueTask }; }