360 lines
13 KiB
TypeScript
360 lines
13 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">
|
|
{detail.call.contactDisplayName ||
|
|
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">
|
|
Caller Phone
|
|
</p>
|
|
<p className="font-medium">
|
|
{detail.call.callerPhone ||
|
|
normalizePhoneFromIdentity(detail.call.participantIdentity) ||
|
|
"Unknown"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
Company
|
|
</p>
|
|
<p className="font-medium">{detail.call.contactCompany || "—"}</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>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
Reminder
|
|
</p>
|
|
<p className="font-medium">
|
|
{detail.call.reminderStatus || "none"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
Warm Transfer
|
|
</p>
|
|
<p className="font-medium">
|
|
{detail.call.warmTransferStatus || "none"}
|
|
</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.reminderStartAt ? (
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
Reminder Time
|
|
</p>
|
|
<p className="font-medium">
|
|
{formatPhoneCallTimestamp(detail.call.reminderStartAt)}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
{detail.call.warmTransferFailureReason ? (
|
|
<div>
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
Transfer Detail
|
|
</p>
|
|
<p className="font-medium">
|
|
{detail.call.warmTransferFailureReason}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
{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}
|
|
{detail.call.reminderCalendarHtmlLink ? (
|
|
<div className="md:col-span-2">
|
|
<Link
|
|
href={detail.call.reminderCalendarHtmlLink}
|
|
target="_blank"
|
|
className="inline-flex items-center gap-2 text-sm text-primary hover:underline"
|
|
>
|
|
Open reminder
|
|
<ExternalLink className="h-4 w-4" />
|
|
</Link>
|
|
</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>
|
|
)}
|
|
{detail.contactProfile ? (
|
|
<div className="border-t pt-3">
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
Contact Profile
|
|
</p>
|
|
<p className="font-medium">
|
|
{detail.contactProfile.displayName ||
|
|
[detail.contactProfile.firstName, detail.contactProfile.lastName]
|
|
.filter(Boolean)
|
|
.join(" ") ||
|
|
"Known caller"}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{detail.contactProfile.company || detail.contactProfile.email || "No company or email yet"}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
</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",
|
|
}
|