From ac9c50b47dfeff8957060cf56b103dc0bd170a0b Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:02:58 +0700 Subject: [PATCH] feat: remove CommunicationsQuery, load messages on-demand only - Remove bulk CommunicationsQuery from useContacts (was loading ALL messages for ALL contacts on init) - Rebuild commThreads from contacts + contactInboxes using the new lastMessageText field from Phase 1 - Per-contact messages now load on-demand via getClientTimeline - Remove commItems from useWorkspaceRouting, use clientTimelineItems Co-Authored-By: Claude Opus 4.6 --- .../components/workspace/CrmWorkspaceApp.vue | 61 ++++++------------- frontend/app/composables/useContacts.ts | 34 ++--------- .../app/composables/useWorkspaceRouting.ts | 8 +-- 3 files changed, 28 insertions(+), 75 deletions(-) diff --git a/frontend/app/components/workspace/CrmWorkspaceApp.vue b/frontend/app/components/workspace/CrmWorkspaceApp.vue index 58bbea5..e515cab 100644 --- a/frontend/app/components/workspace/CrmWorkspaceApp.vue +++ b/frontend/app/components/workspace/CrmWorkspaceApp.vue @@ -95,7 +95,6 @@ const { // --------------------------------------------------------------------------- const { contacts, - commItems, contactSearch, selectedChannel, sortMode, @@ -110,7 +109,6 @@ const { markAvatarBroken, contactInitials, refetchContacts, - refetchCommunications, } = useContacts({ apolloAuthReady }); // --------------------------------------------------------------------------- @@ -301,7 +299,6 @@ const { async function refetchAllCrmQueries() { await Promise.all([ refetchContacts(), - refetchCommunications(), refetchContactInboxes(), refetchCalendar(), refetchDeals(), @@ -461,59 +458,36 @@ const pilotHeaderPhrases = [ const pilotHeaderText = ref("Every step moves you forward"); // --------------------------------------------------------------------------- -// Comm Threads (glue: contacts + commItems + contactInboxes) +// Comm Threads (glue: contacts + contactInboxes — no bulk message loading) // --------------------------------------------------------------------------- const commThreads = computed(() => { - const sorted = [...commItems.value].sort((a, b) => a.at.localeCompare(b.at)); - const map = new Map(); - for (const item of sorted) { - if (!map.has(item.contact)) map.set(item.contact, []); - map.get(item.contact)?.push(item); - } - - const contactById = new Map(contacts.value.map((contact) => [contact.id, contact])); const inboxesByContactId = new Map(); for (const inbox of contactInboxes.value) { if (!inboxesByContactId.has(inbox.contactId)) inboxesByContactId.set(inbox.contactId, []); - inboxesByContactId.get(inbox.contactId)?.push(inbox); + inboxesByContactId.get(inbox.contactId)!.push(inbox); } - const contactIds = new Set([ - ...contacts.value.map((contact) => contact.id), - ...contactInboxes.value.map((inbox) => inbox.contactId), - ]); - - return [...contactIds] - .map((contactId) => { - const contact = contactById.get(contactId); - const inboxes = inboxesByContactId.get(contactId) ?? []; - const contactName = contact?.name ?? inboxes[0]?.contactName ?? ""; - const items = map.get(contactName) ?? []; - const last = items[items.length - 1]; + return contacts.value + .map((c) => { + const inboxes = inboxesByContactId.get(c.id) ?? []; const channels = [ ...new Set([ - ...(contact?.channels ?? []), - ...inboxes.map((inbox) => inbox.channel), - ...items.map((item) => item.channel), + ...c.channels, + ...inboxes.map((i) => i.channel), ]), ] as CommItem["channel"][]; - const inboxFallbackLast = inboxes - .map((inbox) => inbox.lastMessageAt || inbox.updatedAt) - .filter(Boolean) - .sort() - .at(-1); return { - id: contactId, - contact: contactName, - avatar: contact?.avatar ?? "", + id: c.id, + contact: c.name, + avatar: c.avatar, channels, - lastAt: last?.at ?? contact?.lastContactAt ?? inboxFallbackLast ?? "", - lastText: last?.text ?? "No messages yet", - items, + lastAt: c.lastContactAt, + lastText: c.lastMessageText || "No messages yet", + items: [] as CommItem[], }; }) - .filter((thread) => thread.contact) + .filter((t) => t.contact) .sort((a, b) => b.lastAt.localeCompare(a.lastAt)); }); @@ -722,7 +696,7 @@ const routing = useWorkspaceRouting({ commThreads, contacts, deals, - commItems, + clientTimelineItems, activeChangeMessage, activeChangeItem, activeChangeItems, @@ -958,8 +932,9 @@ const selectedContactEvents = computed(() => { const selectedContactRecentMessages = computed(() => { if (!selectedContact.value) return []; - return commItems.value - .filter((item) => item.contact === selectedContact.value?.name && item.kind === "message") + return clientTimelineItems.value + .filter((entry) => entry.contentType === "message" && entry.message) + .map((entry) => entry.message!) .sort((a, b) => b.at.localeCompare(a.at)) .slice(0, 8); }); diff --git a/frontend/app/composables/useContacts.ts b/frontend/app/composables/useContacts.ts index 634c548..344c253 100644 --- a/frontend/app/composables/useContacts.ts +++ b/frontend/app/composables/useContacts.ts @@ -1,9 +1,6 @@ import { ref, computed, watch, watchEffect, type ComputedRef } from "vue"; import { useQuery } from "@vue/apollo-composable"; -import { - ContactsQueryDocument, - CommunicationsQueryDocument, -} from "~~/graphql/generated"; +import { ContactsQueryDocument } from "~~/graphql/generated"; export type Contact = { id: string; @@ -11,6 +8,8 @@ export type Contact = { avatar: string; channels: string[]; lastContactAt: string; + lastMessageText: string; + lastMessageChannel: string; description: string; }; @@ -41,32 +40,13 @@ export function useContacts(opts: { apolloAuthReady: ComputedRef }) { { enabled: opts.apolloAuthReady }, ); - const { result: communicationsResult, refetch: refetchCommunications } = useQuery( - CommunicationsQueryDocument, - null, - { enabled: opts.apolloAuthReady }, - ); - const contacts = ref([]); - const commItems = ref([]); watch( - [() => contactsResult.value?.contacts, () => communicationsResult.value?.communications], - ([rawContacts, rawComms]) => { + () => contactsResult.value?.contacts, + (rawContacts) => { if (!rawContacts) return; - const contactsList = [...rawContacts] as Contact[]; - const commsList = (rawComms ?? []) as CommItem[]; - - const byName = new Map>(); - for (const item of commsList) { - if (!byName.has(item.contact)) byName.set(item.contact, new Set()); - byName.get(item.contact)?.add(item.channel); - } - contacts.value = contactsList.map((c) => ({ - ...c, - channels: Array.from(byName.get(c.name) ?? c.channels ?? []), - })); - commItems.value = commsList; + contacts.value = [...rawContacts] as Contact[]; }, { immediate: true }, ); @@ -166,7 +146,6 @@ export function useContacts(opts: { apolloAuthReady: ComputedRef }) { return { contacts, - commItems, contactSearch, selectedChannel, sortMode, @@ -181,6 +160,5 @@ export function useContacts(opts: { apolloAuthReady: ComputedRef }) { markAvatarBroken, contactInitials, refetchContacts, - refetchCommunications, }; } diff --git a/frontend/app/composables/useWorkspaceRouting.ts b/frontend/app/composables/useWorkspaceRouting.ts index b34dda3..db33f82 100644 --- a/frontend/app/composables/useWorkspaceRouting.ts +++ b/frontend/app/composables/useWorkspaceRouting.ts @@ -31,7 +31,7 @@ export function useWorkspaceRouting(opts: { commThreads: ComputedRef<{ id: string; [key: string]: any }[]>; contacts: Ref<{ id: string; name: string; [key: string]: any }[]>; deals: Ref<{ id: string; contact: string; [key: string]: any }[]>; - commItems: Ref<{ id: string; contact: string; [key: string]: any }[]>; + clientTimelineItems: Ref<{ id: string; contactId: string; contentType: string; message?: { contact: string } | null; [key: string]: any }[]>; activeChangeMessage: ComputedRef<{ changeSetId?: string | null; changeItems?: PilotChangeItem[] | null } | null>; activeChangeItem: ComputedRef; activeChangeItems: ComputedRef; @@ -340,9 +340,9 @@ export function useWorkspaceRouting(opts: { if (item.entity === "message" && item.entityId) { opts.peopleLeftMode.value = "contacts"; opts.peopleListMode.value = "contacts"; - const message = opts.commItems.value.find((entry) => entry.id === item.entityId); - if (message?.contact) { - opts.openCommunicationThread(message.contact); + const timelineEntry = opts.clientTimelineItems.value.find((entry) => entry.contentType === "message" && entry.message && entry.id === item.entityId); + if (timelineEntry?.message?.contact) { + opts.openCommunicationThread(timelineEntry.message.contact); } opts.focusedCalendarEventId.value = ""; syncPathFromUi(push);