import Link from "next/link" import { fetchQuery } from "convex/nextjs" import { MessageSquare, Phone, Search } 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 { Input } from "@/components/ui/input" import { ScrollArea } from "@/components/ui/scroll-area" type PageProps = { searchParams: Promise<{ search?: string channel?: "call" | "sms" | "chat" | "unknown" status?: "open" | "closed" | "archived" conversationId?: string page?: string }> } function formatTimestamp(value?: number) { if (!value) { return "—" } return new Date(value).toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }) } function formatSidebarTimestamp(value?: number) { if (!value) { return "" } const date = new Date(value) const now = new Date() const sameDay = date.toDateString() === now.toDateString() return sameDay ? date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", }) : date.toLocaleDateString("en-US", { month: "short", day: "numeric", }) } function formatDuration(value?: number) { if (!value) { return "—" } const totalSeconds = Math.max(0, Math.round(value / 1000)) const minutes = Math.floor(totalSeconds / 60) const seconds = totalSeconds % 60 return `${minutes}:${String(seconds).padStart(2, "0")}` } function getSyncMessage(sync: any) { if (!sync.ghlConfigured) { return "Connect GHL to load contacts and conversations." } if (sync.stages.conversations.status === "running") { return "Conversations are syncing now." } if (sync.stages.conversations.error) { return "Conversations could not be loaded from GHL yet." } if (!sync.latestSyncAt) { return "No conversations yet." } return "Browse contacts and conversations in one inbox." } function getInitials(value?: string) { const text = String(value || "").trim() if (!text) { return "RM" } const parts = text.split(/\s+/).filter(Boolean) if (parts.length === 1) { return parts[0].slice(0, 2).toUpperCase() } return `${parts[0][0] || ""}${parts[1][0] || ""}`.toUpperCase() } function buildConversationHref(params: { search?: string channel?: string status?: string conversationId?: string }) { const nextParams = new URLSearchParams() if (params.search) { nextParams.set("search", params.search) } if (params.channel) { nextParams.set("channel", params.channel) } if (params.status) { nextParams.set("status", params.status) } if (params.conversationId) { nextParams.set("conversationId", params.conversationId) } const query = nextParams.toString() return query ? `/admin/conversations?${query}` : "/admin/conversations" } export default async function AdminConversationsPage({ searchParams, }: PageProps) { const params = await searchParams const search = params.search?.trim() || undefined const data = await fetchQuery(api.crm.listAdminConversations, { search, page: 1, limit: 100, channel: params.channel, status: params.status, }) const selectedConversationId = (params.conversationId && data.items.find((item: any) => item.id === params.conversationId)?.id) || data.items[0]?.id const detail = selectedConversationId ? await fetchQuery(api.crm.getAdminConversationDetail, { conversationId: selectedConversationId, }) : null const timeline = detail ? [ ...detail.messages.map((message: any) => ({ id: `message-${message.id}`, type: "message" as const, timestamp: message.sentAt || 0, message, })), ...detail.recordings.map((recording: any) => ({ id: `recording-${recording.id}`, type: "recording" as const, timestamp: recording.startedAt || recording.endedAt || 0, recording, })), ].sort((a, b) => a.timestamp - b.timestamp) : [] return (
Review calls and messages in one inbox.
Search and pick a conversation to review.
{conversation.displayName}
{conversation.secondaryLine ? ({conversation.secondaryLine}
) : null}{conversation.lastMessagePreview || "No messages or call notes yet."}
{detail.contact?.secondaryLine || detail.contact?.phone || detail.contact?.email}
) : null}{message.body}
Choose a conversation from the left to open the full thread.