From f1fb2fbfa63af5a8f611ad80bd99f11ad4834030 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:04:49 +0700 Subject: [PATCH] feat(chat): add outbound delivery statuses to omnichat thread UI --- frontend/app.vue | 47 ++++++++++ frontend/graphql/operations/dashboard.graphql | 1 + frontend/server/graphql/schema.ts | 88 +++++++++++++++++++ 3 files changed, 136 insertions(+) diff --git a/frontend/app.vue b/frontend/app.vue index 85725e7..7f32b32 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -86,6 +86,7 @@ type CommItem = { audioUrl?: string; duration?: string; transcript?: string[]; + deliveryStatus?: "PENDING" | "SENT" | "DELIVERED" | "READ" | "FAILED" | string | null; }; type CommPin = { @@ -3941,6 +3942,24 @@ function channelIcon(channel: "All" | CommItem["channel"]) { return "phone"; } +function messageDeliveryUiState(item: CommItem): "none" | "sending" | "sent" | "delivered" | "failed" { + if (item.kind !== "message" || item.direction !== "out") return "none"; + const rawStatus = String(item.deliveryStatus ?? "").toUpperCase(); + if (rawStatus === "FAILED") return "failed"; + if (rawStatus === "READ" || rawStatus === "DELIVERED") return "delivered"; + if (rawStatus === "SENT") return "sent"; + return "sending"; +} + +function messageDeliveryLabel(item: CommItem) { + const state = messageDeliveryUiState(item); + if (state === "failed") return "Delivery failed"; + if (state === "delivered") return "Delivered"; + if (state === "sent") return "Sent"; + if (state === "sending") return "Sending"; + return ""; +} + function makeId(prefix: string) { return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}`; } @@ -5599,6 +5618,34 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") {{ formatStamp(entry.item.at) }} + + + + ✓ + + + ✓✓ + + + ! + +
diff --git a/frontend/graphql/operations/dashboard.graphql b/frontend/graphql/operations/dashboard.graphql index 1c3b7b5..0823882 100644 --- a/frontend/graphql/operations/dashboard.graphql +++ b/frontend/graphql/operations/dashboard.graphql @@ -23,6 +23,7 @@ query DashboardQuery { audioUrl duration transcript + deliveryStatus } calendar { id diff --git a/frontend/server/graphql/schema.ts b/frontend/server/graphql/schema.ts index f56b9c2..1d896ce 100644 --- a/frontend/server/graphql/schema.ts +++ b/frontend/server/graphql/schema.ts @@ -357,6 +357,48 @@ async function getDashboard(auth: AuthContext | null) { }), ]); + let omniMessagesRaw: Array<{ + id: string; + contactId: string; + channel: string; + direction: string; + text: string; + status: string; + occurredAt: Date; + updatedAt: Date; + }> = []; + + if (communicationsRaw.length) { + const contactIds = [...new Set(communicationsRaw.map((row) => row.contactId))]; + const minOccurredAt = communicationsRaw[0]?.occurredAt ?? new Date(); + const maxOccurredAt = communicationsRaw[communicationsRaw.length - 1]?.occurredAt ?? new Date(); + const fromOccurredAt = new Date(minOccurredAt.getTime() - 5 * 60 * 1000); + const toOccurredAt = new Date(maxOccurredAt.getTime() + 5 * 60 * 1000); + + omniMessagesRaw = await prisma.omniMessage.findMany({ + where: { + teamId: ctx.teamId, + contactId: { in: contactIds }, + occurredAt: { + gte: fromOccurredAt, + lte: toOccurredAt, + }, + }, + select: { + id: true, + contactId: true, + channel: true, + direction: true, + text: true, + status: true, + occurredAt: true, + updatedAt: true, + }, + orderBy: [{ occurredAt: "asc" }, { updatedAt: "asc" }], + take: 5000, + }); + } + const channelsByContactId = new Map