From 013a908d92b738b128d793c40933661f575b252c Mon Sep 17 00:00:00 2001 From: DMleadgen Date: Thu, 16 Apr 2026 14:05:12 -0600 Subject: [PATCH] feat: improve ghl conversation sync and inbox actions --- app/admin/conversations/page.tsx | 98 ++++++-- .../conversations/[id]/messages/route.ts | 49 ++++ .../admin/conversations/[id]/sync/route.ts | 40 +++ convex/crm.ts | 230 ++++++++++++++++++ convex/ghlMirror.ts | 42 ++++ lib/server/admin-auth.ts | 23 ++ 6 files changed, 462 insertions(+), 20 deletions(-) create mode 100644 app/api/admin/conversations/[id]/messages/route.ts create mode 100644 app/api/admin/conversations/[id]/sync/route.ts diff --git a/app/admin/conversations/page.tsx b/app/admin/conversations/page.tsx index 1bfafa54..ba2a6f28 100644 --- a/app/admin/conversations/page.tsx +++ b/app/admin/conversations/page.tsx @@ -1,5 +1,5 @@ import Link from "next/link" -import { fetchQuery } from "convex/nextjs" +import { fetchAction, fetchQuery } from "convex/nextjs" import { MessageSquare, Phone, Search } from "lucide-react" import { api } from "@/convex/_generated/api" import { Badge } from "@/components/ui/badge" @@ -20,6 +20,7 @@ type PageProps = { channel?: "call" | "sms" | "chat" | "unknown" status?: "open" | "closed" | "archived" conversationId?: string + error?: string page?: string }> } @@ -147,15 +148,31 @@ export default async function AdminConversationsPage({ }) : null - const timeline = detail + const hydratedDetail = + detail && + detail.messages.length === 0 && + detail.conversation.ghlConversationId + ? await fetchAction(api.crm.hydrateConversationHistory, { + conversationId: detail.conversation.id, + }).then(async (result) => { + if (result?.imported) { + return await fetchQuery(api.crm.getAdminConversationDetail, { + conversationId: detail.conversation.id, + }) + } + return detail + }) + : detail + + const timeline = hydratedDetail ? [ - ...detail.messages.map((message: any) => ({ + ...hydratedDetail.messages.map((message: any) => ({ id: `message-${message.id}`, type: "message" as const, timestamp: message.sentAt || 0, message, })), - ...detail.recordings.map((recording: any) => ({ + ...hydratedDetail.recordings.map((recording: any) => ({ id: `recording-${recording.id}`, type: "recording" as const, timestamp: recording.startedAt || recording.endedAt || 0, @@ -310,51 +327,71 @@ export default async function AdminConversationsPage({
- {detail ? ( + {hydratedDetail ? (

- {detail.contact?.name || - detail.conversation.title || + {hydratedDetail.contact?.name || + hydratedDetail.conversation.title || "Conversation"}

- {detail.contact?.secondaryLine || - detail.contact?.email || - detail.contact?.phone ? ( + {hydratedDetail.contact?.secondaryLine || + hydratedDetail.contact?.email || + hydratedDetail.contact?.phone ? (

- {detail.contact?.secondaryLine || - detail.contact?.phone || - detail.contact?.email} + {hydratedDetail.contact?.secondaryLine || + hydratedDetail.contact?.phone || + hydratedDetail.contact?.email}

) : null}
- {detail.conversation.channel} + {hydratedDetail.conversation.channel} - {detail.conversation.status} + {hydratedDetail.conversation.status} {timeline.filter((item) => item.type === "message").length}{" "} messages - {detail.recordings.length ? ( + {hydratedDetail.recordings.length ? ( - {detail.recordings.length} recording - {detail.recordings.length === 1 ? "" : "s"} + {hydratedDetail.recordings.length} recording + {hydratedDetail.recordings.length === 1 ? "" : "s"} ) : null}
Last activity:{" "} - {formatTimestamp(detail.conversation.lastMessageAt)} + {formatTimestamp(hydratedDetail.conversation.lastMessageAt)}
+
+
+ +
+ {params.error === "send" ? ( +

+ Rocky could not send that message through GHL. +

+ ) : null} + {params.error === "sync" ? ( +

+ Rocky could not refresh that conversation from GHL. +

+ ) : null} +
@@ -362,7 +399,8 @@ export default async function AdminConversationsPage({ {timeline.length === 0 ? (
No messages or recordings have been mirrored into this - conversation yet. + conversation yet. Use refresh history to pull the latest + thread from GHL.
) : ( timeline.map((item: any) => { @@ -435,6 +473,26 @@ export default async function AdminConversationsPage({ )}
+
+
+