diff --git a/frontend/app/components/workspace/CrmWorkspaceApp.vue b/frontend/app/components/workspace/CrmWorkspaceApp.vue index b3671fe..5c4dc1f 100644 --- a/frontend/app/components/workspace/CrmWorkspaceApp.vue +++ b/frontend/app/components/workspace/CrmWorkspaceApp.vue @@ -9,35 +9,38 @@ import CrmDocumentsPanel from "~~/app/components/workspace/documents/CrmDocument import CrmWorkspaceTopbar from "~~/app/components/workspace/header/CrmWorkspaceTopbar.vue"; import CrmPilotSidebar from "~~/app/components/workspace/pilot/CrmPilotSidebar.vue"; import CrmChangeReviewOverlay from "~~/app/components/workspace/review/CrmChangeReviewOverlay.vue"; -import meQuery from "~~/graphql/operations/me.graphql?raw"; -import chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw"; -import contactsQuery from "~~/graphql/operations/contacts.graphql?raw"; -import communicationsQuery from "~~/graphql/operations/communications.graphql?raw"; -import contactInboxesQuery from "~~/graphql/operations/contact-inboxes.graphql?raw"; -import calendarQuery from "~~/graphql/operations/calendar.graphql?raw"; -import dealsQuery from "~~/graphql/operations/deals.graphql?raw"; -import feedQuery from "~~/graphql/operations/feed.graphql?raw"; -import pinsQuery from "~~/graphql/operations/pins.graphql?raw"; -import documentsQuery from "~~/graphql/operations/documents.graphql?raw"; -import getClientTimelineQuery from "~~/graphql/operations/get-client-timeline.graphql?raw"; -import logoutMutation from "~~/graphql/operations/logout.graphql?raw"; -import logPilotNoteMutation from "~~/graphql/operations/log-pilot-note.graphql?raw"; -import createCalendarEventMutation from "~~/graphql/operations/create-calendar-event.graphql?raw"; -import archiveCalendarEventMutation from "~~/graphql/operations/archive-calendar-event.graphql?raw"; -import createCommunicationMutation from "~~/graphql/operations/create-communication.graphql?raw"; -import createWorkspaceDocumentMutation from "~~/graphql/operations/create-workspace-document.graphql?raw"; -import deleteWorkspaceDocumentMutation from "~~/graphql/operations/delete-workspace-document.graphql?raw"; -import updateCommunicationTranscriptMutation from "~~/graphql/operations/update-communication-transcript.graphql?raw"; -import updateFeedDecisionMutation from "~~/graphql/operations/update-feed-decision.graphql?raw"; -import chatConversationsQuery from "~~/graphql/operations/chat-conversations.graphql?raw"; -import createChatConversationMutation from "~~/graphql/operations/create-chat-conversation.graphql?raw"; -import selectChatConversationMutation from "~~/graphql/operations/select-chat-conversation.graphql?raw"; -import archiveChatConversationMutation from "~~/graphql/operations/archive-chat-conversation.graphql?raw"; -import toggleContactPinMutation from "~~/graphql/operations/toggle-contact-pin.graphql?raw"; -import setContactInboxHiddenMutation from "~~/graphql/operations/set-contact-inbox-hidden.graphql?raw"; -import confirmLatestChangeSetMutation from "~~/graphql/operations/confirm-latest-change-set.graphql?raw"; -import rollbackLatestChangeSetMutation from "~~/graphql/operations/rollback-latest-change-set.graphql?raw"; -import rollbackChangeSetItemsMutation from "~~/graphql/operations/rollback-change-set-items.graphql?raw"; +import { useQuery, useMutation } from "@vue/apollo-composable"; +import { + MeQueryDocument, + ChatMessagesQueryDocument, + ChatConversationsQueryDocument, + ContactsQueryDocument, + CommunicationsQueryDocument, + ContactInboxesQueryDocument, + CalendarQueryDocument, + DealsQueryDocument, + FeedQueryDocument, + PinsQueryDocument, + DocumentsQueryDocument, + GetClientTimelineQueryDocument, + LogoutMutationDocument, + LogPilotNoteMutationDocument, + CreateCalendarEventMutationDocument, + ArchiveCalendarEventMutationDocument, + CreateCommunicationMutationDocument, + CreateWorkspaceDocumentDocument, + DeleteWorkspaceDocumentDocument, + UpdateCommunicationTranscriptMutationDocument, + UpdateFeedDecisionMutationDocument, + CreateChatConversationMutationDocument, + SelectChatConversationMutationDocument, + ArchiveChatConversationMutationDocument, + ToggleContactPinMutationDocument, + SetContactInboxHiddenDocument, + ConfirmLatestChangeSetMutationDocument, + RollbackLatestChangeSetMutationDocument, + RollbackChangeSetItemsMutationDocument, +} from "~~/graphql/generated"; import { buildContactDocumentScope, formatDocumentScope, @@ -515,7 +518,7 @@ const pilotChat = new AiChat({ livePilotUserText.value = ""; livePilotAssistantText.value = ""; pilotLiveLogs.value = []; - await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData()]); + await Promise.all([refetchChatMessages(), refetchChatConversations(), refetchAllCrmQueries()]); }, onError: () => { if (livePilotUserText.value) { @@ -552,6 +555,250 @@ let crmRealtimeRefreshInFlight = false; let crmRealtimeReconnectAttempt = 0; let clientTimelineRequestToken = 0; +// --------------------------------------------------------------------------- +// Apollo Queries +// --------------------------------------------------------------------------- +const apolloAuthReady = computed(() => !!authMe.value); + +const { result: meResult, refetch: refetchMe, loading: meLoading } = useQuery( + MeQueryDocument, + null, + { fetchPolicy: "network-only" }, +); + +const { result: chatMessagesResult, refetch: refetchChatMessages } = useQuery( + ChatMessagesQueryDocument, + null, + { enabled: apolloAuthReady }, +); + +const { result: chatConversationsResult, refetch: refetchChatConversations } = useQuery( + ChatConversationsQueryDocument, + null, + { enabled: apolloAuthReady }, +); + +const { result: contactsResult, refetch: refetchContacts } = useQuery( + ContactsQueryDocument, + null, + { enabled: apolloAuthReady }, +); + +const { result: communicationsResult, refetch: refetchCommunications } = useQuery( + CommunicationsQueryDocument, + null, + { enabled: apolloAuthReady }, +); + +const { result: contactInboxesResult, refetch: refetchContactInboxes } = useQuery( + ContactInboxesQueryDocument, + null, + { enabled: apolloAuthReady }, +); + +const { result: calendarResult, refetch: refetchCalendar } = useQuery( + CalendarQueryDocument, + null, + { enabled: apolloAuthReady }, +); + +const { result: dealsResult, refetch: refetchDeals } = useQuery( + DealsQueryDocument, + null, + { enabled: apolloAuthReady }, +); + +const { result: feedResult, refetch: refetchFeed } = useQuery( + FeedQueryDocument, + null, + { enabled: apolloAuthReady }, +); + +const { result: pinsResult, refetch: refetchPins } = useQuery( + PinsQueryDocument, + null, + { enabled: apolloAuthReady }, +); + +const { result: documentsResult, refetch: refetchDocuments } = useQuery( + DocumentsQueryDocument, + null, + { enabled: apolloAuthReady }, +); + +const timelineContactId = ref(""); +const timelineLimit = ref(500); + +const { result: timelineResult, refetch: refetchTimeline } = useQuery( + GetClientTimelineQueryDocument, + () => ({ contactId: timelineContactId.value, limit: timelineLimit.value }), + { enabled: computed(() => !!timelineContactId.value && apolloAuthReady.value) }, +); + +// --------------------------------------------------------------------------- +// Apollo Mutations +// --------------------------------------------------------------------------- +const allCrmQueryDocs = [ + { query: ContactsQueryDocument }, + { query: CommunicationsQueryDocument }, + { query: ContactInboxesQueryDocument }, + { query: CalendarQueryDocument }, + { query: DealsQueryDocument }, + { query: FeedQueryDocument }, + { query: PinsQueryDocument }, + { query: DocumentsQueryDocument }, +]; + +const { mutate: doLogout } = useMutation(LogoutMutationDocument); +const { mutate: doLogPilotNote } = useMutation(LogPilotNoteMutationDocument); +const { mutate: doCreateCalendarEvent } = useMutation(CreateCalendarEventMutationDocument, { + refetchQueries: [{ query: CalendarQueryDocument }], +}); +const { mutate: doArchiveCalendarEvent } = useMutation(ArchiveCalendarEventMutationDocument, { + refetchQueries: [{ query: CalendarQueryDocument }], +}); +const { mutate: doCreateCommunication } = useMutation(CreateCommunicationMutationDocument, { + refetchQueries: [{ query: CommunicationsQueryDocument }, { query: ContactInboxesQueryDocument }], +}); +const { mutate: doCreateWorkspaceDocument } = useMutation(CreateWorkspaceDocumentDocument, { + refetchQueries: [{ query: DocumentsQueryDocument }], +}); +const { mutate: doDeleteWorkspaceDocument } = useMutation(DeleteWorkspaceDocumentDocument, { + refetchQueries: [{ query: DocumentsQueryDocument }], +}); +const { mutate: doUpdateCommunicationTranscript } = useMutation(UpdateCommunicationTranscriptMutationDocument, { + refetchQueries: [{ query: CommunicationsQueryDocument }], +}); +const { mutate: doUpdateFeedDecision } = useMutation(UpdateFeedDecisionMutationDocument, { + refetchQueries: [{ query: FeedQueryDocument }], +}); +const { mutate: doCreateChatConversation } = useMutation(CreateChatConversationMutationDocument, { + refetchQueries: [{ query: MeQueryDocument }, { query: ChatMessagesQueryDocument }, { query: ChatConversationsQueryDocument }], +}); +const { mutate: doSelectChatConversation } = useMutation(SelectChatConversationMutationDocument, { + refetchQueries: [{ query: MeQueryDocument }, { query: ChatMessagesQueryDocument }, { query: ChatConversationsQueryDocument }], +}); +const { mutate: doArchiveChatConversation } = useMutation(ArchiveChatConversationMutationDocument, { + refetchQueries: [{ query: MeQueryDocument }, { query: ChatMessagesQueryDocument }, { query: ChatConversationsQueryDocument }], +}); +const { mutate: doToggleContactPin } = useMutation(ToggleContactPinMutationDocument, { + refetchQueries: [{ query: PinsQueryDocument }], +}); +const { mutate: doSetContactInboxHidden } = useMutation(SetContactInboxHiddenDocument, { + refetchQueries: [{ query: ContactInboxesQueryDocument }], + update: (cache, _result, { variables }) => { + if (!variables) return; + const existing = cache.readQuery({ query: ContactInboxesQueryDocument }) as { contactInboxes?: ContactInbox[] } | null; + if (!existing?.contactInboxes) return; + cache.writeQuery({ + query: ContactInboxesQueryDocument, + data: { + contactInboxes: existing.contactInboxes.map((inbox) => + inbox.id === variables.inboxId ? { ...inbox, isHidden: variables.hidden } : inbox, + ), + }, + }); + }, +}); +const { mutate: doConfirmLatestChangeSet } = useMutation(ConfirmLatestChangeSetMutationDocument, { + refetchQueries: [{ query: ChatMessagesQueryDocument }], +}); +const { mutate: doRollbackLatestChangeSet } = useMutation(RollbackLatestChangeSetMutationDocument, { + refetchQueries: [{ query: ChatMessagesQueryDocument }, { query: ChatConversationsQueryDocument }, ...allCrmQueryDocs], +}); +const { mutate: doRollbackChangeSetItems } = useMutation(RollbackChangeSetItemsMutationDocument, { + refetchQueries: [{ query: ChatMessagesQueryDocument }, { query: ChatConversationsQueryDocument }, ...allCrmQueryDocs], +}); + +// --------------------------------------------------------------------------- +// Apollo → Ref Watchers (bridge Apollo reactive results to existing refs) +// --------------------------------------------------------------------------- +function syncPilotChatFromHistoryBridge(messages: PilotMessage[]) { + // forward-declared; actual function is defined later + syncPilotChatFromHistory(messages); +} + +watch(() => meResult.value?.me, (me) => { + if (me) authMe.value = me as typeof authMe.value; +}, { immediate: true }); + +watch(() => chatMessagesResult.value?.chatMessages, (v) => { + if (v) { + pilotMessages.value = v as PilotMessage[]; + syncPilotChatFromHistoryBridge(pilotMessages.value); + } +}, { immediate: true }); + +watch(() => chatConversationsResult.value?.chatConversations, (v) => { + if (v) chatConversations.value = v as ChatConversation[]; +}, { immediate: true }); + +watch( + [() => contactsResult.value?.contacts, () => communicationsResult.value?.communications], + ([rawContacts, rawComms]) => { + 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; + }, + { immediate: true }, +); + +watch(() => contactInboxesResult.value?.contactInboxes, (v) => { + if (v) contactInboxes.value = v as ContactInbox[]; +}, { immediate: true }); + +watch(() => calendarResult.value?.calendar, (v) => { + if (v) calendarEvents.value = v as CalendarEvent[]; +}, { immediate: true }); + +watch(() => dealsResult.value?.deals, (v) => { + if (v) deals.value = v as Deal[]; +}, { immediate: true }); + +watch(() => feedResult.value?.feed, (v) => { + if (v) feedCards.value = v as FeedCard[]; +}, { immediate: true }); + +watch(() => pinsResult.value?.pins, (v) => { + if (v) commPins.value = v as CommPin[]; +}, { immediate: true }); + +watch(() => documentsResult.value?.documents, (v) => { + if (v) documents.value = v as WorkspaceDocument[]; +}, { immediate: true }); + +watch(() => timelineResult.value?.getClientTimeline, (v) => { + if (v) clientTimelineItems.value = v as ClientTimelineItem[]; +}, { immediate: true }); + +// --------------------------------------------------------------------------- +// Refetch helpers (replace old gqlFetch-based functions) +// --------------------------------------------------------------------------- +async function refetchAllCrmQueries() { + await Promise.all([ + refetchContacts(), + refetchCommunications(), + refetchContactInboxes(), + refetchCalendar(), + refetchDeals(), + refetchFeed(), + refetchPins(), + refetchDocuments(), + ]); + await refreshSelectedClientTimeline(); +} + watch( () => pilotLiveLogs.value.length, (len) => { @@ -851,52 +1098,23 @@ const renderedPilotMessages = computed(() => { return items; }); -async function gqlFetch(query: string, variables?: Record) { - const headers = process.server ? useRequestHeaders(["cookie"]) : undefined; - const result = await $fetch<{ data?: TData; errors?: Array<{ message: string }> }>("/api/graphql", { - method: "POST", - headers, - body: { query, variables }, - }); - - if (result.errors?.length) { - throw new Error(result.errors[0]?.message || "GraphQL request failed"); - } - - if (!result.data) { - throw new Error("GraphQL returned empty payload"); - } - - return result.data; -} - async function loadPilotMessages() { - const data = await gqlFetch<{ chatMessages: PilotMessage[] }>(chatMessagesQuery); - pilotMessages.value = data.chatMessages ?? []; - syncPilotChatFromHistory(pilotMessages.value); + await refetchChatMessages(); } async function loadChatConversations() { chatThreadsLoading.value = true; try { - const data = await gqlFetch<{ chatConversations: ChatConversation[] }>(chatConversationsQuery); - chatConversations.value = data.chatConversations ?? []; + await refetchChatConversations(); } finally { chatThreadsLoading.value = false; } } async function loadMe() { - const data = await gqlFetch<{ - me: { - user: { id: string; phone: string; name: string }; - team: { id: string; name: string }; - conversation: { id: string; title: string }; - }; - }>( - meQuery, - ); - authMe.value = data.me; + const result = await refetchMe(); + const me = result?.data?.me; + if (me) authMe.value = me as typeof authMe.value; } const authResolved = ref(false); @@ -941,8 +1159,7 @@ async function createNewChatConversation() { chatThreadPickerOpen.value = false; chatCreating.value = true; try { - await gqlFetch<{ createChatConversation: ChatConversation }>(createChatConversationMutation); - await Promise.all([loadMe(), loadPilotMessages(), loadChatConversations()]); + await doCreateChatConversation(); } finally { chatCreating.value = false; } @@ -953,8 +1170,7 @@ async function switchChatConversation(id: string) { chatThreadPickerOpen.value = false; chatSwitching.value = true; try { - await gqlFetch<{ selectChatConversation: { ok: boolean } }>(selectChatConversationMutation, { id }); - await Promise.all([loadMe(), loadPilotMessages(), loadChatConversations()]); + await doSelectChatConversation({ id }); } finally { chatSwitching.value = false; } @@ -964,15 +1180,14 @@ async function archiveChatConversation(id: string) { if (!id || chatArchivingId.value) return; chatArchivingId.value = id; try { - await gqlFetch<{ archiveChatConversation: { ok: boolean } }>(archiveChatConversationMutation, { id }); - await Promise.all([loadMe(), loadPilotMessages(), loadChatConversations()]); + await doArchiveChatConversation({ id }); } finally { chatArchivingId.value = ""; } } async function logout() { - await gqlFetch<{ logout: { ok: boolean } }>(logoutMutation); + await doLogout(); stopCrmRealtime(); stopPilotBackgroundPolling(); authMe.value = null; @@ -991,63 +1206,20 @@ async function logout() { } async function refreshCrmData() { - const [ - contactsData, - communicationsData, - contactInboxesData, - calendarData, - dealsData, - feedData, - pinsData, - documentsData, - ] = await Promise.all([ - gqlFetch<{ contacts: Contact[] }>(contactsQuery), - gqlFetch<{ communications: CommItem[] }>(communicationsQuery), - gqlFetch<{ contactInboxes: ContactInbox[] }>(contactInboxesQuery), - gqlFetch<{ calendar: CalendarEvent[] }>(calendarQuery), - gqlFetch<{ deals: Deal[] }>(dealsQuery), - gqlFetch<{ feed: FeedCard[] }>(feedQuery), - gqlFetch<{ pins: CommPin[] }>(pinsQuery), - gqlFetch<{ documents: WorkspaceDocument[] }>(documentsQuery), - ]); - - contacts.value = contactsData.contacts ?? []; - commItems.value = communicationsData.communications ?? []; - contactInboxes.value = contactInboxesData.contactInboxes ?? []; - calendarEvents.value = calendarData.calendar ?? []; - deals.value = dealsData.deals ?? []; - feedCards.value = feedData.feed ?? []; - commPins.value = pinsData.pins ?? []; - documents.value = documentsData.documents ?? []; - - // Derive channels per contact from communication items. - const byName = new Map>(); - for (const item of commItems.value) { - if (!byName.has(item.contact)) byName.set(item.contact, new Set()); - byName.get(item.contact)?.add(item.channel); - } - contacts.value = contacts.value.map((c) => ({ - ...c, - channels: Array.from(byName.get(c.name) ?? []), - })); - - await refreshSelectedClientTimeline(); + await refetchAllCrmQueries(); } async function loadClientTimeline(contactId: string, limit = 500) { const normalizedContactId = String(contactId ?? "").trim(); if (!normalizedContactId) { clientTimelineItems.value = []; + timelineContactId.value = ""; return; } - const requestToken = ++clientTimelineRequestToken; - const data = await gqlFetch<{ getClientTimeline: ClientTimelineItem[] }>(getClientTimelineQuery, { - contactId: normalizedContactId, - limit, - }); - if (requestToken !== clientTimelineRequestToken) return; - clientTimelineItems.value = data.getClientTimeline ?? []; + timelineContactId.value = normalizedContactId; + timelineLimit.value = limit; + await refetchTimeline(); } async function refreshSelectedClientTimeline() { @@ -1075,7 +1247,7 @@ async function runCrmRealtimeRefresh() { if (!authMe.value || crmRealtimeRefreshInFlight) return; crmRealtimeRefreshInFlight = true; try { - await Promise.all([refreshCrmData(), loadTelegramConnectStatus()]); + await Promise.all([refetchAllCrmQueries(), loadTelegramConnectStatus()]); } catch { // ignore transient realtime refresh errors } finally { @@ -1211,7 +1383,7 @@ async function sendPilotText(rawText: string) { livePilotUserText.value = ""; livePilotAssistantText.value = ""; pilotSending.value = false; - await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData()]); + await Promise.all([refetchChatMessages(), refetchChatConversations(), refetchAllCrmQueries()]); } } @@ -1962,8 +2134,7 @@ async function confirmLatestChangeSet() { if (changeActionBusy.value || !latestChangeMessage.value?.changeSetId) return; changeActionBusy.value = true; try { - await gqlFetch<{ confirmLatestChangeSet: { ok: boolean } }>(confirmLatestChangeSetMutation); - await loadPilotMessages(); + await doConfirmLatestChangeSet(); } finally { changeActionBusy.value = false; } @@ -1973,8 +2144,7 @@ async function rollbackLatestChangeSet() { if (changeActionBusy.value || !latestChangeMessage.value?.changeSetId) return; changeActionBusy.value = true; try { - await gqlFetch<{ rollbackLatestChangeSet: { ok: boolean } }>(rollbackLatestChangeSetMutation); - await Promise.all([loadPilotMessages(), refreshCrmData(), loadChatConversations()]); + await doRollbackLatestChangeSet(); activeChangeSetId.value = ""; activeChangeStep.value = 0; setPeopleLeftMode("contacts"); @@ -1990,11 +2160,7 @@ async function rollbackSelectedChangeItems() { changeActionBusy.value = true; try { - await gqlFetch<{ rollbackChangeSetItems: { ok: boolean } }>(rollbackChangeSetItemsMutation, { - changeSetId: targetChangeSetId, - itemIds, - }); - await Promise.all([loadPilotMessages(), refreshCrmData(), loadChatConversations()]); + await doRollbackChangeSetItems({ changeSetId: targetChangeSetId, itemIds }); } finally { changeActionBusy.value = false; } @@ -2007,11 +2173,7 @@ async function rollbackChangeItemById(itemId: string) { changeActionBusy.value = true; try { - await gqlFetch<{ rollbackChangeSetItems: { ok: boolean } }>(rollbackChangeSetItemsMutation, { - changeSetId: targetChangeSetId, - itemIds: [itemId], - }); - await Promise.all([loadPilotMessages(), refreshCrmData(), loadChatConversations()]); + await doRollbackChangeSetItems({ changeSetId: targetChangeSetId, itemIds: [itemId] }); } finally { changeActionBusy.value = false; } @@ -3172,9 +3334,7 @@ async function deleteWorkspaceDocumentById(documentIdInput: string) { documentDeletingId.value = documentId; try { - await gqlFetch<{ deleteWorkspaceDocument: { ok: boolean; id: string } }>(deleteWorkspaceDocumentMutation, { - id: documentId, - }); + await doDeleteWorkspaceDocument({ id: documentId }); documents.value = documents.value.filter((doc) => doc.id !== documentId); clientTimelineItems.value = clientTimelineItems.value.filter((item) => { const isDocumentEntry = String(item.contentType).toLowerCase() === "document"; @@ -3794,13 +3954,12 @@ async function archiveEventManually(event: CalendarEvent) { eventCloseSaving.value = { ...eventCloseSaving.value, [eventId]: true }; eventCloseError.value = { ...eventCloseError.value, [eventId]: "" }; try { - await gqlFetch<{ archiveCalendarEvent: CalendarEvent }>(archiveCalendarEventMutation, { + await doArchiveCalendarEvent({ input: { id: eventId, archiveNote: archiveNote || undefined, }, }); - await refreshCrmData(); eventCloseOpen.value = { ...eventCloseOpen.value, [eventId]: false }; eventCloseDraft.value = { ...eventCloseDraft.value, [eventId]: "" }; } catch (error: any) { @@ -3817,11 +3976,7 @@ async function togglePinnedText(contact: string, value: string) { if (!contactName || !text) return; commPinToggling.value = true; try { - await gqlFetch<{ toggleContactPin: { ok: boolean; pinned: boolean } }>(toggleContactPinMutation, { - contact: contactName, - text, - }); - await refreshCrmData(); + await doToggleContactPin({ contact: contactName, text }); } finally { commPinToggling.value = false; } @@ -4149,11 +4304,7 @@ async function transcribeCallItem(item: CommItem) { }); const text = await transcribeAudioBlob(audioBlob); callTranscriptText.value[itemId] = text || "(empty transcript)"; - await gqlFetch<{ updateCommunicationTranscript: { ok: boolean; id: string } }>(updateCommunicationTranscriptMutation, { - id: itemId, - transcript: text ? [text] : [], - }); - await refreshCrmData(); + await doUpdateCommunicationTranscript({ id: itemId, transcript: text ? [text] : [] }); } catch (error: any) { callTranscriptError.value[itemId] = String(error?.message ?? error ?? "Transcription failed"); } finally { @@ -4215,11 +4366,7 @@ async function setInboxHidden(inboxId: string, hidden: boolean) { if (!id || isInboxToggleLoading(id)) return; inboxToggleLoadingById.value = { ...inboxToggleLoadingById.value, [id]: true }; try { - await gqlFetch<{ setContactInboxHidden: { ok: boolean } }>(setContactInboxHiddenMutation, { - inboxId: id, - hidden, - }); - await refreshCrmData(); + await doSetContactInboxHidden({ inboxId: id, hidden }); } finally { inboxToggleLoadingById.value = { ...inboxToggleLoadingById.value, [id]: false }; } @@ -4249,8 +4396,8 @@ function makeId(prefix: string) { function pushPilotNote(text: string) { // Fire-and-forget: log assistant note to the same conversation. - gqlFetch<{ logPilotNote: { ok: boolean } }>(logPilotNoteMutation, { text }) - .then(() => Promise.all([loadPilotMessages(), loadChatConversations()])) + doLogPilotNote({ text }) + .then(() => Promise.all([refetchChatMessages(), refetchChatConversations()])) .catch(() => {}); } @@ -4409,7 +4556,7 @@ async function createCommEvent() { commEventSaving.value = true; commEventError.value = ""; try { - const res = await gqlFetch<{ createCalendarEvent: CalendarEvent }>(createCalendarEventMutation, { + const res = await doCreateCalendarEvent({ input: { title, start: start.toISOString(), @@ -4420,7 +4567,9 @@ async function createCommEvent() { archiveNote: commEventMode.value === "logged" ? note : undefined, }, }); - calendarEvents.value = [res.createCalendarEvent, ...calendarEvents.value]; + if (res?.data?.createCalendarEvent) { + calendarEvents.value = [res.data.createCalendarEvent as CalendarEvent, ...calendarEvents.value]; + } selectedDateKey.value = dayKey(start); calendarCursor.value = new Date(start.getFullYear(), start.getMonth(), 1); commDraft.value = ""; @@ -4449,7 +4598,7 @@ async function createCommDocument() { commEventSaving.value = true; commEventError.value = ""; try { - const res = await gqlFetch<{ createWorkspaceDocument: WorkspaceDocument }>(createWorkspaceDocumentMutation, { + const res = await doCreateWorkspaceDocument({ input: { title, owner: authDisplayName.value, @@ -4459,8 +4608,13 @@ async function createCommDocument() { }, }); - documents.value = [res.createWorkspaceDocument, ...documents.value.filter((doc) => doc.id !== res.createWorkspaceDocument.id)]; - selectedDocumentId.value = res.createWorkspaceDocument.id; + const created = res?.data?.createWorkspaceDocument; + if (created) { + documents.value = [created as WorkspaceDocument, ...documents.value.filter((doc) => doc.id !== created.id)]; + selectedDocumentId.value = created.id; + } else { + selectedDocumentId.value = ""; + } contactRightPanelMode.value = "documents"; commDraft.value = ""; commComposerMode.value = "message"; @@ -4482,7 +4636,7 @@ async function sendCommMessage() { const channel = commSendChannel.value; if (!channel) return; - await gqlFetch<{ createCommunication: { ok: boolean; id: string } }>(createCommunicationMutation, { + await doCreateCommunication({ input: { contact: selectedCommThread.value.contact, channel, @@ -4493,7 +4647,6 @@ async function sendCommMessage() { }); commDraft.value = ""; - await refreshCrmData(); openCommunicationThread(selectedCommThread.value.contact); } finally { commSending.value = false; @@ -4535,7 +4688,7 @@ async function executeFeedAction(card: FeedCard) { const end = new Date(start); end.setMinutes(end.getMinutes() + 30); - const res = await gqlFetch<{ createCalendarEvent: CalendarEvent }>(createCalendarEventMutation, { + const res = await doCreateCalendarEvent({ input: { title: `Follow-up: ${card.contact.split(" ")[0] ?? "Contact"}`, start: start.toISOString(), @@ -4544,7 +4697,9 @@ async function executeFeedAction(card: FeedCard) { note: "Created from feed action.", }, }); - calendarEvents.value = [res.createCalendarEvent, ...calendarEvents.value]; + if (res?.data?.createCalendarEvent) { + calendarEvents.value = [res.data.createCalendarEvent as CalendarEvent, ...calendarEvents.value]; + } selectedDateKey.value = dayKey(start); calendarCursor.value = new Date(start.getFullYear(), start.getMonth(), 1); @@ -4558,7 +4713,7 @@ async function executeFeedAction(card: FeedCard) { } if (key === "call") { - await gqlFetch<{ createCommunication: { ok: boolean; id: string } }>(createCommunicationMutation, { + await doCreateCommunication({ input: { contact: card.contact, channel: "Phone", @@ -4568,13 +4723,12 @@ async function executeFeedAction(card: FeedCard) { durationSec: 0, }, }); - await refreshCrmData(); openCommunicationThread(card.contact); return `Call event created and ${card.contact} chat opened.`; } if (key === "draft_message") { - await gqlFetch<{ createCommunication: { ok: boolean; id: string } }>(createCommunicationMutation, { + await doCreateCommunication({ input: { contact: card.contact, channel: "Email", @@ -4583,7 +4737,6 @@ async function executeFeedAction(card: FeedCard) { text: "Draft: onboarding plan + two slots for tomorrow.", }, }); - await refreshCrmData(); openCommunicationThread(card.contact); return `Draft message added to ${card.contact} communications.`; } @@ -4593,7 +4746,7 @@ async function executeFeedAction(card: FeedCard) { } if (key === "prepare_question") { - await gqlFetch<{ createCommunication: { ok: boolean; id: string } }>(createCommunicationMutation, { + await doCreateCommunication({ input: { contact: card.contact, channel: "Telegram", @@ -4602,7 +4755,6 @@ async function executeFeedAction(card: FeedCard) { text: "Draft: can you confirm your decision date for this cycle?", }, }); - await refreshCrmData(); openCommunicationThread(card.contact); return `Question about decision date added to ${card.contact} chat.`; } @@ -4616,22 +4768,14 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") if (decision === "rejected") { const note = "Rejected. Nothing created."; card.decisionNote = note; - await gqlFetch<{ updateFeedDecision: { ok: boolean; id: string } }>(updateFeedDecisionMutation, { - id: card.id, - decision: "rejected", - decisionNote: note, - }); + await doUpdateFeedDecision({ id: card.id, decision: "rejected", decisionNote: note }); pushPilotNote(`[${card.contact}] recommendation rejected: ${card.proposal.title}`); return; } const result = await executeFeedAction(card); card.decisionNote = result; - await gqlFetch<{ updateFeedDecision: { ok: boolean; id: string } }>(updateFeedDecisionMutation, { - id: card.id, - decision: "accepted", - decisionNote: result, - }); + await doUpdateFeedDecision({ id: card.id, decision: "accepted", decisionNote: result }); pushPilotNote(`[${card.contact}] ${result}`); } diff --git a/frontend/app/components/workspace/communications/CrmCommunicationsListSidebar.vue b/frontend/app/components/workspace/communications/CrmCommunicationsListSidebar.vue index 161505c..52120c9 100644 --- a/frontend/app/components/workspace/communications/CrmCommunicationsListSidebar.vue +++ b/frontend/app/components/workspace/communications/CrmCommunicationsListSidebar.vue @@ -146,7 +146,7 @@ function onSearchInput(event: Event) { {{ formatThreadTime(thread.lastAt) }}

- {{ threadChannelLabel(thread) }} + {{ thread.lastText || threadChannelLabel(thread) }}

diff --git a/frontend/app/pages/login.vue b/frontend/app/pages/login.vue index 377e7e1..b9c1840 100644 --- a/frontend/app/pages/login.vue +++ b/frontend/app/pages/login.vue @@ -1,39 +1,20 @@