Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Airtable inbound #125

Draft
wants to merge 11 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion src/app/api/crm_example/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ export async function GET(request: Request) {

const url = new URL(request.url);
const queryParams = new URLSearchParams(url.search);
const phone: string = queryParams.get("phone") || "1234567890";
const phone: string | null = queryParams.get("phone");

if (!phone) {
return new Response("Invalid phone number", {
status: 400,
});
}

const clientInfo = await table.getClientByPhone(phone);

Expand Down
14 changes: 14 additions & 0 deletions src/app/api/eventcallback/route.ts
Original file line number Diff line number Diff line change
@@ -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' } });
}
57 changes: 57 additions & 0 deletions src/app/api/reservations/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 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();

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 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 });
}
}
36 changes: 36 additions & 0 deletions src/app/api/tasks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextRequest } from 'next/server';
import { twilioClient } from '@/lib/twilioClient';


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 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)

client.taskrouter.v1.workspaces(workspaceSid)
.tasks(task)
.reservations(reservation)
.update({
instruction: 'dequeue',
dequeueFrom: '+16134002002', // The phone number the call is connected from
// to: 'client:[email protected]' // The client to connect the call to
})
.then((reservation: { reservationStatus: any; }) => console.log(reservation.reservationStatus))
.catch((error: any) => console.error(error));

//{"contact_uri":"client:[email protected]"}
return new Response("dequeued", { status: 200 });
} catch (error) {
return new Response("something went wrong", { status: 500 });
}
}
8 changes: 7 additions & 1 deletion src/app/api/voice/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@ 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();

try {
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.
if (bodyTo == callerId) {
// Incoming call
const dial = resp.dial();
resp.say("Please hold");
resp.enqueue({ workflowSid: workflowSid });
// const dial = resp.dial({ callerId: bodyFrom });
// dial.client('[email protected]');

} else if (bodyTo) {
// Outgoing call
const dial = resp.dial({ callerId });
Expand Down
13 changes: 9 additions & 4 deletions src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ export default function Layout({
if (status === 'loading') {
return <React.Fragment>Loading...</React.Fragment>;
} 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 (
<div className="h-screen w-screen">
<Appbar initials={initials} isProgramManager={isProgramManager} />
Expand Down
6 changes: 6 additions & 0 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
"use client"
import IncomingCallModal from "@/components/(dashboard)/tasks/IncomingCallModal";
import NotificationsCard from "@/components/appbar/NotificationsCard";

export default function Dashboard() {
return (
<main>
<h1>Dashboard</h1>
{/* <IncomingCallModal number="1234567890" acceptCall={() => {}} rejectCall={() => {}} /> */}
{/* <NotificationsCard /> */}
</main>
);
}
Expand Down
122 changes: 120 additions & 2 deletions src/app/dashboard/tasks/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,125 @@
"use client"

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));
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();

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 {
fetch(`/api/reservations?taskSid=${reservation.taskSid}&status=${status}&reservationSid=${reservation.sid}`, {
method: 'POST'
})
} catch (error) {
console.error("Error updating reservation", error)
}
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();
const intervalId = setInterval(fetchTasks, 5000);

// Clear interval on component unmount
return () => clearInterval(intervalId);
}, []);

return (
<main>
<h1>Tasks</h1>
<h2 className="text-4xl font-semibold mb-4 mt-4">Tasks</h2>
<h3 className="text-lg font-semibold">See all unresolved communications with clients here.</h3>
<h3 className="italic font-normal mt-4">{activeTasks.length} outstanding task{activeTasks.length == 1 ? "" : "s"}</h3>
<table className="mt-8">
<thead>
<tr className="text-left text-lg font-semibold">
<th className="min-w-[20rem]">Task</th>
<th className="min-w-[14rem]">Initiated</th>
<th className="min-w-[14rem]">Actions</th>
</tr>
</thead>
<tbody>
{activeTasks && Array.isArray(activeTasks) && activeTasks
.map((task: any) => (
<tr key={task.task.sid} >
<td>
{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}
</td>
<td>{formatTime(task.task.age)}</td>
<td>
{task.task.taskChannelUniqueName === 'voice' ? (
<Button
className="bg-[#334155] hover:bg-[#2D3A4C]/90 w-fit mr-2"
onClick={() => dequeueTask(task.reservation)}
>
Call
</Button>
) : task.task.taskChannelUniqueName === 'chat' ? (
<Button
className="bg-[#334155] hover:bg-[#2D3A4C]/90 w-fit mr-2"
onClick={() => console.log('Chat mode')}
>
Chat
</Button>
) : null}
<Button
className="bg-[#F1F5F9] hover:bg-[#D8DCE0]/90 w-fit mr-2 text-black"
onClick={ () => updateReservation(task.reservation, 'rejected') }
>Reject</Button>
{/* <Button
className="bg-[#F1F5F9] hover:bg-[#D8DCE0]/90 w-fit text-black"
onClick={ () => updateReservation(task.reservation, 'canceled')}
>Dismiss</Button> */}
</td>
</tr>
))}
</tbody>
</table>
</main>
);
}
}
46 changes: 46 additions & 0 deletions src/components/(dashboard)/tasks/IncomingCallModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Button } from '@/components/ui/button';
import { formatPhoneNumber } from '@/lib/utils';
import { CircleUser } from 'lucide-react';

export default function IncomingCallModal({
number,
acceptCall,
rejectCall,
}: {
number: string;
acceptCall: () => void;
rejectCall: () => void;
}) {
return (
<div className="fixed z-10 top-0 left-0 w-full h-full bg-black bg-opacity-50 flex justify-center items-center">
<div className="bg-white p-4 rounded-sm">
<section className="flex flex-col items-center justify-between p-8 gap-y-12">
<article className="flex flex-col gap-y-4 items-center">
<CircleUser size="100" color="#D9D9D9" />
<div className="text-2xl font-light">
{formatPhoneNumber(number)}
</div>
<div className="text-lg font-lgiht text-[#6B7280]">
Incoming Call
</div>
</article>

<article className="flex flex-row gap-x-10">
<Button
className="rounded-md w-[120px] h-12 bg-[#22E4AC] text-white hover:bg-[#1BB88A]"
onClick={() => acceptCall()}
>
Accept Call
</Button>
<button
className="rounded-md w-[120px] h-12 bg-[#F93C5C] text-white hover:bg-[#E03652]"
onClick={() => rejectCall()}
>
Reject Call
</button>
</article>
</section>
</div>
</div>
);
}
Loading