Rocky_Mountain_Vending/app/admin/conversations/[id]/page.tsx

241 lines
8.3 KiB
TypeScript

import Link from "next/link"
import { notFound } from "next/navigation"
import { fetchQuery } from "convex/nextjs"
import { ArrowLeft, ExternalLink, MessageSquare } from "lucide-react"
import { api } from "@/convex/_generated/api"
import { Badge } from "@/components/ui/badge"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
type PageProps = {
params: Promise<{
id: string
}>
}
function formatTimestamp(value?: number) {
if (!value) {
return "—"
}
return new Date(value).toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})
}
function formatDuration(value?: number) {
if (!value) {
return "—"
}
const totalSeconds = Math.round(value / 1000)
const minutes = Math.floor(totalSeconds / 60)
const seconds = totalSeconds % 60
return `${minutes}:${String(seconds).padStart(2, "0")}`
}
export default async function AdminConversationDetailPage({
params,
}: PageProps) {
const { id } = await params
const detail = await fetchQuery(api.crm.getAdminConversationDetail, {
conversationId: id,
})
if (!detail) {
notFound()
}
return (
<div className="container mx-auto px-4 py-8">
<div className="space-y-8">
<div className="space-y-2">
<Link
href="/admin/conversations"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
Back to conversations
</Link>
<h1 className="text-4xl font-bold tracking-tight text-balance">
{detail.conversation.title || "Conversation Detail"}
</h1>
<p className="text-muted-foreground">
Unified thread for Rocky-owned conversation management.
</p>
</div>
<div className="grid gap-6 lg:grid-cols-[0.95fr_1.05fr]">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
Conversation Status
</CardTitle>
<CardDescription>
Channel, ownership, and sync metadata.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">
Channel
</p>
<Badge className="mt-1" variant="outline">
{detail.conversation.channel}
</Badge>
</div>
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">
Status
</p>
<Badge className="mt-1" variant="secondary">
{detail.conversation.status}
</Badge>
</div>
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">
Contact
</p>
<p className="font-medium">
{detail.contact?.name || "Unlinked"}
</p>
</div>
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">
Started
</p>
<p className="font-medium">
{formatTimestamp(detail.conversation.startedAt)}
</p>
</div>
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">
Last Activity
</p>
<p className="font-medium">
{formatTimestamp(detail.conversation.lastMessageAt)}
</p>
</div>
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">
GHL Conversation ID
</p>
<p className="font-medium break-all">
{detail.conversation.ghlConversationId || "—"}
</p>
</div>
{detail.conversation.summaryText ? (
<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.conversation.summaryText}
</p>
</div>
) : null}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Recordings & Leads</CardTitle>
<CardDescription>
Call artifacts and related lead outcomes for this thread.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{detail.recordings.map((recording: any) => (
<div key={recording.id} className="rounded-lg border p-3">
<div className="flex items-center justify-between gap-3">
<Badge variant="outline">
{recording.recordingStatus || "recording"}
</Badge>
<span className="text-xs text-muted-foreground">
{formatDuration(recording.durationMs)}
</span>
</div>
{recording.recordingUrl ? (
<Link
href={recording.recordingUrl}
target="_blank"
className="mt-2 inline-flex items-center gap-2 text-sm text-primary hover:underline"
>
Open recording
<ExternalLink className="h-4 w-4" />
</Link>
) : null}
{recording.transcriptionText ? (
<p className="mt-2 text-sm text-muted-foreground whitespace-pre-wrap">
{recording.transcriptionText}
</p>
) : null}
</div>
))}
{detail.leads.map((lead: any) => (
<div key={lead.id} className="rounded-lg border p-3">
<div className="flex items-center justify-between gap-3">
<p className="font-medium">{lead.type}</p>
<Badge variant="secondary">{lead.status}</Badge>
</div>
<p className="mt-2 text-sm text-muted-foreground whitespace-pre-wrap">
{lead.message || lead.intent || "—"}
</p>
</div>
))}
{detail.recordings.length === 0 && detail.leads.length === 0 ? (
<p className="text-sm text-muted-foreground">
No recordings or linked leads for this conversation yet.
</p>
) : null}
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Messages</CardTitle>
<CardDescription>
Full backend-owned thread history for this conversation.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{detail.messages.length === 0 ? (
<p className="text-sm text-muted-foreground">
No messages have been mirrored into this conversation yet.
</p>
) : (
detail.messages.map((message: any) => (
<div key={message.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">
{message.channel} {message.direction}
</span>
<span>{formatTimestamp(message.sentAt)}</span>
</div>
<p className="whitespace-pre-wrap text-sm">{message.body}</p>
</div>
))
)}
</CardContent>
</Card>
</div>
</div>
)
}
export const metadata = {
title: "Conversation Detail | Admin",
description: "Review a Rocky conversation thread, recordings, and leads",
}