189 lines
8.5 KiB
TypeScript
189 lines
8.5 KiB
TypeScript
import Link from "next/link";
|
|
import { notFound } from "next/navigation";
|
|
import { fetchQuery } from "convex/nextjs";
|
|
import { ArrowLeft, ExternalLink, Phone } from "lucide-react";
|
|
import { api } from "@/convex/_generated/api";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
formatPhoneCallDuration,
|
|
formatPhoneCallTimestamp,
|
|
normalizePhoneFromIdentity,
|
|
} from "@/lib/phone-calls";
|
|
|
|
type PageProps = {
|
|
params: Promise<{
|
|
id: string;
|
|
}>;
|
|
};
|
|
|
|
export default async function AdminCallDetailPage({ params }: PageProps) {
|
|
const { id } = await params;
|
|
const detail = await fetchQuery(api.voiceSessions.getAdminPhoneCallDetail, {
|
|
callId: id,
|
|
});
|
|
|
|
if (!detail) {
|
|
notFound();
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="space-y-8">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
<div className="space-y-2">
|
|
<Link href="/admin/calls" className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Back to calls
|
|
</Link>
|
|
<h1 className="text-4xl font-bold tracking-tight text-balance">Phone Call Detail</h1>
|
|
<p className="text-muted-foreground">
|
|
{normalizePhoneFromIdentity(detail.call.participantIdentity) || detail.call.participantIdentity}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Phone className="h-5 w-5" />
|
|
Call Status
|
|
</CardTitle>
|
|
<CardDescription>Operational detail for this direct phone session.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Started</p>
|
|
<p className="font-medium">{formatPhoneCallTimestamp(detail.call.startedAt)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Room</p>
|
|
<p className="font-medium break-all">{detail.call.roomName}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Duration</p>
|
|
<p className="font-medium">{formatPhoneCallDuration(detail.call.durationMs)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Participant Identity</p>
|
|
<p className="font-medium break-all">{detail.call.participantIdentity || "Unknown"}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Call Status</p>
|
|
<Badge className="mt-1" variant={detail.call.callStatus === "failed" ? "destructive" : detail.call.callStatus === "started" ? "secondary" : "default"}>
|
|
{detail.call.callStatus}
|
|
</Badge>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Jessica Answered</p>
|
|
<p className="font-medium">{detail.call.answered ? "Yes" : "No"}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Lead Outcome</p>
|
|
<p className="font-medium">{detail.call.leadOutcome}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Email Summary</p>
|
|
<p className="font-medium">{detail.call.notificationStatus}</p>
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Summary</p>
|
|
<p className="text-sm whitespace-pre-wrap">{detail.call.summaryText || "No summary available yet."}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Recording Status</p>
|
|
<p className="font-medium">{detail.call.recordingStatus || "Unavailable"}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Transcript Turns</p>
|
|
<p className="font-medium">{detail.call.transcriptTurnCount}</p>
|
|
</div>
|
|
{detail.call.recordingUrl ? (
|
|
<div className="md:col-span-2">
|
|
<Link href={detail.call.recordingUrl} target="_blank" className="inline-flex items-center gap-2 text-sm text-primary hover:underline">
|
|
Open recording
|
|
<ExternalLink className="h-4 w-4" />
|
|
</Link>
|
|
</div>
|
|
) : null}
|
|
{detail.call.notificationError ? (
|
|
<div className="md:col-span-2">
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Email Error</p>
|
|
<p className="text-sm text-destructive">{detail.call.notificationError}</p>
|
|
</div>
|
|
) : null}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Linked Lead</CardTitle>
|
|
<CardDescription>
|
|
{detail.linkedLead ? "Lead created from this phone call." : "No lead was created from this call."}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{detail.linkedLead ? (
|
|
<>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Contact</p>
|
|
<p className="font-medium">
|
|
{detail.linkedLead.firstName} {detail.linkedLead.lastName}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Lead Type</p>
|
|
<p className="font-medium">{detail.linkedLead.type}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Email</p>
|
|
<p className="font-medium break-all">{detail.linkedLead.email}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Phone</p>
|
|
<p className="font-medium">{detail.linkedLead.phone}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">Message</p>
|
|
<p className="text-sm whitespace-pre-wrap">{detail.linkedLead.message || "—"}</p>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">Jessica handled the call, but it did not result in a submitted lead.</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Transcript</CardTitle>
|
|
<CardDescription>Complete mirrored transcript for this phone call.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{detail.turns.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">No transcript turns were captured for this call.</p>
|
|
) : (
|
|
detail.turns.map((turn: any) => (
|
|
<div key={turn.id} className="rounded-lg border p-3">
|
|
<div className="mb-1 flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
|
<span className="uppercase tracking-wide">{turn.role}</span>
|
|
<span>{formatPhoneCallTimestamp(turn.createdAt)}</span>
|
|
</div>
|
|
<p className="whitespace-pre-wrap text-sm">{turn.text}</p>
|
|
</div>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const metadata = {
|
|
title: "Phone Call Detail | Admin",
|
|
description: "Review a mirrored direct phone call transcript and linked lead details",
|
|
};
|