refactor: migrate CRM data layer from manual gqlFetch to Apollo Client
Replace custom gqlFetch() with proper Apollo useQuery/useMutation hooks powered by codegen-generated TypedDocumentNode types. Key changes: - Add GraphQL SDL schema file and codegen config for typescript-vue-apollo - Replace all 28 raw .graphql imports with generated typed documents - Add 12 useQuery() hooks with cache-and-network fetch policy - Add 17 useMutation() hooks with surgical refetchQueries per mutation - Optimistic cache update for setContactInboxHidden (instant archive UX) - Fix contact list subtitle: show lastText instead of channel name - Migrate login page from gqlFetch to useMutation - WebSocket realtime now calls Apollo refetch instead of full data reload Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,35 +9,38 @@ import CrmDocumentsPanel from "~~/app/components/workspace/documents/CrmDocument
|
|||||||
import CrmWorkspaceTopbar from "~~/app/components/workspace/header/CrmWorkspaceTopbar.vue";
|
import CrmWorkspaceTopbar from "~~/app/components/workspace/header/CrmWorkspaceTopbar.vue";
|
||||||
import CrmPilotSidebar from "~~/app/components/workspace/pilot/CrmPilotSidebar.vue";
|
import CrmPilotSidebar from "~~/app/components/workspace/pilot/CrmPilotSidebar.vue";
|
||||||
import CrmChangeReviewOverlay from "~~/app/components/workspace/review/CrmChangeReviewOverlay.vue";
|
import CrmChangeReviewOverlay from "~~/app/components/workspace/review/CrmChangeReviewOverlay.vue";
|
||||||
import meQuery from "~~/graphql/operations/me.graphql?raw";
|
import { useQuery, useMutation } from "@vue/apollo-composable";
|
||||||
import chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw";
|
import {
|
||||||
import contactsQuery from "~~/graphql/operations/contacts.graphql?raw";
|
MeQueryDocument,
|
||||||
import communicationsQuery from "~~/graphql/operations/communications.graphql?raw";
|
ChatMessagesQueryDocument,
|
||||||
import contactInboxesQuery from "~~/graphql/operations/contact-inboxes.graphql?raw";
|
ChatConversationsQueryDocument,
|
||||||
import calendarQuery from "~~/graphql/operations/calendar.graphql?raw";
|
ContactsQueryDocument,
|
||||||
import dealsQuery from "~~/graphql/operations/deals.graphql?raw";
|
CommunicationsQueryDocument,
|
||||||
import feedQuery from "~~/graphql/operations/feed.graphql?raw";
|
ContactInboxesQueryDocument,
|
||||||
import pinsQuery from "~~/graphql/operations/pins.graphql?raw";
|
CalendarQueryDocument,
|
||||||
import documentsQuery from "~~/graphql/operations/documents.graphql?raw";
|
DealsQueryDocument,
|
||||||
import getClientTimelineQuery from "~~/graphql/operations/get-client-timeline.graphql?raw";
|
FeedQueryDocument,
|
||||||
import logoutMutation from "~~/graphql/operations/logout.graphql?raw";
|
PinsQueryDocument,
|
||||||
import logPilotNoteMutation from "~~/graphql/operations/log-pilot-note.graphql?raw";
|
DocumentsQueryDocument,
|
||||||
import createCalendarEventMutation from "~~/graphql/operations/create-calendar-event.graphql?raw";
|
GetClientTimelineQueryDocument,
|
||||||
import archiveCalendarEventMutation from "~~/graphql/operations/archive-calendar-event.graphql?raw";
|
LogoutMutationDocument,
|
||||||
import createCommunicationMutation from "~~/graphql/operations/create-communication.graphql?raw";
|
LogPilotNoteMutationDocument,
|
||||||
import createWorkspaceDocumentMutation from "~~/graphql/operations/create-workspace-document.graphql?raw";
|
CreateCalendarEventMutationDocument,
|
||||||
import deleteWorkspaceDocumentMutation from "~~/graphql/operations/delete-workspace-document.graphql?raw";
|
ArchiveCalendarEventMutationDocument,
|
||||||
import updateCommunicationTranscriptMutation from "~~/graphql/operations/update-communication-transcript.graphql?raw";
|
CreateCommunicationMutationDocument,
|
||||||
import updateFeedDecisionMutation from "~~/graphql/operations/update-feed-decision.graphql?raw";
|
CreateWorkspaceDocumentDocument,
|
||||||
import chatConversationsQuery from "~~/graphql/operations/chat-conversations.graphql?raw";
|
DeleteWorkspaceDocumentDocument,
|
||||||
import createChatConversationMutation from "~~/graphql/operations/create-chat-conversation.graphql?raw";
|
UpdateCommunicationTranscriptMutationDocument,
|
||||||
import selectChatConversationMutation from "~~/graphql/operations/select-chat-conversation.graphql?raw";
|
UpdateFeedDecisionMutationDocument,
|
||||||
import archiveChatConversationMutation from "~~/graphql/operations/archive-chat-conversation.graphql?raw";
|
CreateChatConversationMutationDocument,
|
||||||
import toggleContactPinMutation from "~~/graphql/operations/toggle-contact-pin.graphql?raw";
|
SelectChatConversationMutationDocument,
|
||||||
import setContactInboxHiddenMutation from "~~/graphql/operations/set-contact-inbox-hidden.graphql?raw";
|
ArchiveChatConversationMutationDocument,
|
||||||
import confirmLatestChangeSetMutation from "~~/graphql/operations/confirm-latest-change-set.graphql?raw";
|
ToggleContactPinMutationDocument,
|
||||||
import rollbackLatestChangeSetMutation from "~~/graphql/operations/rollback-latest-change-set.graphql?raw";
|
SetContactInboxHiddenDocument,
|
||||||
import rollbackChangeSetItemsMutation from "~~/graphql/operations/rollback-change-set-items.graphql?raw";
|
ConfirmLatestChangeSetMutationDocument,
|
||||||
|
RollbackLatestChangeSetMutationDocument,
|
||||||
|
RollbackChangeSetItemsMutationDocument,
|
||||||
|
} from "~~/graphql/generated";
|
||||||
import {
|
import {
|
||||||
buildContactDocumentScope,
|
buildContactDocumentScope,
|
||||||
formatDocumentScope,
|
formatDocumentScope,
|
||||||
@@ -515,7 +518,7 @@ const pilotChat = new AiChat<UIMessage>({
|
|||||||
livePilotUserText.value = "";
|
livePilotUserText.value = "";
|
||||||
livePilotAssistantText.value = "";
|
livePilotAssistantText.value = "";
|
||||||
pilotLiveLogs.value = [];
|
pilotLiveLogs.value = [];
|
||||||
await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData()]);
|
await Promise.all([refetchChatMessages(), refetchChatConversations(), refetchAllCrmQueries()]);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
if (livePilotUserText.value) {
|
if (livePilotUserText.value) {
|
||||||
@@ -552,6 +555,250 @@ let crmRealtimeRefreshInFlight = false;
|
|||||||
let crmRealtimeReconnectAttempt = 0;
|
let crmRealtimeReconnectAttempt = 0;
|
||||||
let clientTimelineRequestToken = 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<string, Set<string>>();
|
||||||
|
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(
|
watch(
|
||||||
() => pilotLiveLogs.value.length,
|
() => pilotLiveLogs.value.length,
|
||||||
(len) => {
|
(len) => {
|
||||||
@@ -851,52 +1098,23 @@ const renderedPilotMessages = computed<PilotMessage[]>(() => {
|
|||||||
return items;
|
return items;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function gqlFetch<TData>(query: string, variables?: Record<string, unknown>) {
|
|
||||||
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() {
|
async function loadPilotMessages() {
|
||||||
const data = await gqlFetch<{ chatMessages: PilotMessage[] }>(chatMessagesQuery);
|
await refetchChatMessages();
|
||||||
pilotMessages.value = data.chatMessages ?? [];
|
|
||||||
syncPilotChatFromHistory(pilotMessages.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadChatConversations() {
|
async function loadChatConversations() {
|
||||||
chatThreadsLoading.value = true;
|
chatThreadsLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const data = await gqlFetch<{ chatConversations: ChatConversation[] }>(chatConversationsQuery);
|
await refetchChatConversations();
|
||||||
chatConversations.value = data.chatConversations ?? [];
|
|
||||||
} finally {
|
} finally {
|
||||||
chatThreadsLoading.value = false;
|
chatThreadsLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMe() {
|
async function loadMe() {
|
||||||
const data = await gqlFetch<{
|
const result = await refetchMe();
|
||||||
me: {
|
const me = result?.data?.me;
|
||||||
user: { id: string; phone: string; name: string };
|
if (me) authMe.value = me as typeof authMe.value;
|
||||||
team: { id: string; name: string };
|
|
||||||
conversation: { id: string; title: string };
|
|
||||||
};
|
|
||||||
}>(
|
|
||||||
meQuery,
|
|
||||||
);
|
|
||||||
authMe.value = data.me;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const authResolved = ref(false);
|
const authResolved = ref(false);
|
||||||
@@ -941,8 +1159,7 @@ async function createNewChatConversation() {
|
|||||||
chatThreadPickerOpen.value = false;
|
chatThreadPickerOpen.value = false;
|
||||||
chatCreating.value = true;
|
chatCreating.value = true;
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ createChatConversation: ChatConversation }>(createChatConversationMutation);
|
await doCreateChatConversation();
|
||||||
await Promise.all([loadMe(), loadPilotMessages(), loadChatConversations()]);
|
|
||||||
} finally {
|
} finally {
|
||||||
chatCreating.value = false;
|
chatCreating.value = false;
|
||||||
}
|
}
|
||||||
@@ -953,8 +1170,7 @@ async function switchChatConversation(id: string) {
|
|||||||
chatThreadPickerOpen.value = false;
|
chatThreadPickerOpen.value = false;
|
||||||
chatSwitching.value = true;
|
chatSwitching.value = true;
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ selectChatConversation: { ok: boolean } }>(selectChatConversationMutation, { id });
|
await doSelectChatConversation({ id });
|
||||||
await Promise.all([loadMe(), loadPilotMessages(), loadChatConversations()]);
|
|
||||||
} finally {
|
} finally {
|
||||||
chatSwitching.value = false;
|
chatSwitching.value = false;
|
||||||
}
|
}
|
||||||
@@ -964,15 +1180,14 @@ async function archiveChatConversation(id: string) {
|
|||||||
if (!id || chatArchivingId.value) return;
|
if (!id || chatArchivingId.value) return;
|
||||||
chatArchivingId.value = id;
|
chatArchivingId.value = id;
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ archiveChatConversation: { ok: boolean } }>(archiveChatConversationMutation, { id });
|
await doArchiveChatConversation({ id });
|
||||||
await Promise.all([loadMe(), loadPilotMessages(), loadChatConversations()]);
|
|
||||||
} finally {
|
} finally {
|
||||||
chatArchivingId.value = "";
|
chatArchivingId.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await gqlFetch<{ logout: { ok: boolean } }>(logoutMutation);
|
await doLogout();
|
||||||
stopCrmRealtime();
|
stopCrmRealtime();
|
||||||
stopPilotBackgroundPolling();
|
stopPilotBackgroundPolling();
|
||||||
authMe.value = null;
|
authMe.value = null;
|
||||||
@@ -991,63 +1206,20 @@ async function logout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshCrmData() {
|
async function refreshCrmData() {
|
||||||
const [
|
await refetchAllCrmQueries();
|
||||||
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<string, Set<string>>();
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadClientTimeline(contactId: string, limit = 500) {
|
async function loadClientTimeline(contactId: string, limit = 500) {
|
||||||
const normalizedContactId = String(contactId ?? "").trim();
|
const normalizedContactId = String(contactId ?? "").trim();
|
||||||
if (!normalizedContactId) {
|
if (!normalizedContactId) {
|
||||||
clientTimelineItems.value = [];
|
clientTimelineItems.value = [];
|
||||||
|
timelineContactId.value = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestToken = ++clientTimelineRequestToken;
|
timelineContactId.value = normalizedContactId;
|
||||||
const data = await gqlFetch<{ getClientTimeline: ClientTimelineItem[] }>(getClientTimelineQuery, {
|
timelineLimit.value = limit;
|
||||||
contactId: normalizedContactId,
|
await refetchTimeline();
|
||||||
limit,
|
|
||||||
});
|
|
||||||
if (requestToken !== clientTimelineRequestToken) return;
|
|
||||||
clientTimelineItems.value = data.getClientTimeline ?? [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshSelectedClientTimeline() {
|
async function refreshSelectedClientTimeline() {
|
||||||
@@ -1075,7 +1247,7 @@ async function runCrmRealtimeRefresh() {
|
|||||||
if (!authMe.value || crmRealtimeRefreshInFlight) return;
|
if (!authMe.value || crmRealtimeRefreshInFlight) return;
|
||||||
crmRealtimeRefreshInFlight = true;
|
crmRealtimeRefreshInFlight = true;
|
||||||
try {
|
try {
|
||||||
await Promise.all([refreshCrmData(), loadTelegramConnectStatus()]);
|
await Promise.all([refetchAllCrmQueries(), loadTelegramConnectStatus()]);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore transient realtime refresh errors
|
// ignore transient realtime refresh errors
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1211,7 +1383,7 @@ async function sendPilotText(rawText: string) {
|
|||||||
livePilotUserText.value = "";
|
livePilotUserText.value = "";
|
||||||
livePilotAssistantText.value = "";
|
livePilotAssistantText.value = "";
|
||||||
pilotSending.value = false;
|
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;
|
if (changeActionBusy.value || !latestChangeMessage.value?.changeSetId) return;
|
||||||
changeActionBusy.value = true;
|
changeActionBusy.value = true;
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ confirmLatestChangeSet: { ok: boolean } }>(confirmLatestChangeSetMutation);
|
await doConfirmLatestChangeSet();
|
||||||
await loadPilotMessages();
|
|
||||||
} finally {
|
} finally {
|
||||||
changeActionBusy.value = false;
|
changeActionBusy.value = false;
|
||||||
}
|
}
|
||||||
@@ -1973,8 +2144,7 @@ async function rollbackLatestChangeSet() {
|
|||||||
if (changeActionBusy.value || !latestChangeMessage.value?.changeSetId) return;
|
if (changeActionBusy.value || !latestChangeMessage.value?.changeSetId) return;
|
||||||
changeActionBusy.value = true;
|
changeActionBusy.value = true;
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ rollbackLatestChangeSet: { ok: boolean } }>(rollbackLatestChangeSetMutation);
|
await doRollbackLatestChangeSet();
|
||||||
await Promise.all([loadPilotMessages(), refreshCrmData(), loadChatConversations()]);
|
|
||||||
activeChangeSetId.value = "";
|
activeChangeSetId.value = "";
|
||||||
activeChangeStep.value = 0;
|
activeChangeStep.value = 0;
|
||||||
setPeopleLeftMode("contacts");
|
setPeopleLeftMode("contacts");
|
||||||
@@ -1990,11 +2160,7 @@ async function rollbackSelectedChangeItems() {
|
|||||||
|
|
||||||
changeActionBusy.value = true;
|
changeActionBusy.value = true;
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ rollbackChangeSetItems: { ok: boolean } }>(rollbackChangeSetItemsMutation, {
|
await doRollbackChangeSetItems({ changeSetId: targetChangeSetId, itemIds });
|
||||||
changeSetId: targetChangeSetId,
|
|
||||||
itemIds,
|
|
||||||
});
|
|
||||||
await Promise.all([loadPilotMessages(), refreshCrmData(), loadChatConversations()]);
|
|
||||||
} finally {
|
} finally {
|
||||||
changeActionBusy.value = false;
|
changeActionBusy.value = false;
|
||||||
}
|
}
|
||||||
@@ -2007,11 +2173,7 @@ async function rollbackChangeItemById(itemId: string) {
|
|||||||
|
|
||||||
changeActionBusy.value = true;
|
changeActionBusy.value = true;
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ rollbackChangeSetItems: { ok: boolean } }>(rollbackChangeSetItemsMutation, {
|
await doRollbackChangeSetItems({ changeSetId: targetChangeSetId, itemIds: [itemId] });
|
||||||
changeSetId: targetChangeSetId,
|
|
||||||
itemIds: [itemId],
|
|
||||||
});
|
|
||||||
await Promise.all([loadPilotMessages(), refreshCrmData(), loadChatConversations()]);
|
|
||||||
} finally {
|
} finally {
|
||||||
changeActionBusy.value = false;
|
changeActionBusy.value = false;
|
||||||
}
|
}
|
||||||
@@ -3172,9 +3334,7 @@ async function deleteWorkspaceDocumentById(documentIdInput: string) {
|
|||||||
|
|
||||||
documentDeletingId.value = documentId;
|
documentDeletingId.value = documentId;
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ deleteWorkspaceDocument: { ok: boolean; id: string } }>(deleteWorkspaceDocumentMutation, {
|
await doDeleteWorkspaceDocument({ id: documentId });
|
||||||
id: documentId,
|
|
||||||
});
|
|
||||||
documents.value = documents.value.filter((doc) => doc.id !== documentId);
|
documents.value = documents.value.filter((doc) => doc.id !== documentId);
|
||||||
clientTimelineItems.value = clientTimelineItems.value.filter((item) => {
|
clientTimelineItems.value = clientTimelineItems.value.filter((item) => {
|
||||||
const isDocumentEntry = String(item.contentType).toLowerCase() === "document";
|
const isDocumentEntry = String(item.contentType).toLowerCase() === "document";
|
||||||
@@ -3794,13 +3954,12 @@ async function archiveEventManually(event: CalendarEvent) {
|
|||||||
eventCloseSaving.value = { ...eventCloseSaving.value, [eventId]: true };
|
eventCloseSaving.value = { ...eventCloseSaving.value, [eventId]: true };
|
||||||
eventCloseError.value = { ...eventCloseError.value, [eventId]: "" };
|
eventCloseError.value = { ...eventCloseError.value, [eventId]: "" };
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ archiveCalendarEvent: CalendarEvent }>(archiveCalendarEventMutation, {
|
await doArchiveCalendarEvent({
|
||||||
input: {
|
input: {
|
||||||
id: eventId,
|
id: eventId,
|
||||||
archiveNote: archiveNote || undefined,
|
archiveNote: archiveNote || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await refreshCrmData();
|
|
||||||
eventCloseOpen.value = { ...eventCloseOpen.value, [eventId]: false };
|
eventCloseOpen.value = { ...eventCloseOpen.value, [eventId]: false };
|
||||||
eventCloseDraft.value = { ...eventCloseDraft.value, [eventId]: "" };
|
eventCloseDraft.value = { ...eventCloseDraft.value, [eventId]: "" };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -3817,11 +3976,7 @@ async function togglePinnedText(contact: string, value: string) {
|
|||||||
if (!contactName || !text) return;
|
if (!contactName || !text) return;
|
||||||
commPinToggling.value = true;
|
commPinToggling.value = true;
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ toggleContactPin: { ok: boolean; pinned: boolean } }>(toggleContactPinMutation, {
|
await doToggleContactPin({ contact: contactName, text });
|
||||||
contact: contactName,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
await refreshCrmData();
|
|
||||||
} finally {
|
} finally {
|
||||||
commPinToggling.value = false;
|
commPinToggling.value = false;
|
||||||
}
|
}
|
||||||
@@ -4149,11 +4304,7 @@ async function transcribeCallItem(item: CommItem) {
|
|||||||
});
|
});
|
||||||
const text = await transcribeAudioBlob(audioBlob);
|
const text = await transcribeAudioBlob(audioBlob);
|
||||||
callTranscriptText.value[itemId] = text || "(empty transcript)";
|
callTranscriptText.value[itemId] = text || "(empty transcript)";
|
||||||
await gqlFetch<{ updateCommunicationTranscript: { ok: boolean; id: string } }>(updateCommunicationTranscriptMutation, {
|
await doUpdateCommunicationTranscript({ id: itemId, transcript: text ? [text] : [] });
|
||||||
id: itemId,
|
|
||||||
transcript: text ? [text] : [],
|
|
||||||
});
|
|
||||||
await refreshCrmData();
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
callTranscriptError.value[itemId] = String(error?.message ?? error ?? "Transcription failed");
|
callTranscriptError.value[itemId] = String(error?.message ?? error ?? "Transcription failed");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -4215,11 +4366,7 @@ async function setInboxHidden(inboxId: string, hidden: boolean) {
|
|||||||
if (!id || isInboxToggleLoading(id)) return;
|
if (!id || isInboxToggleLoading(id)) return;
|
||||||
inboxToggleLoadingById.value = { ...inboxToggleLoadingById.value, [id]: true };
|
inboxToggleLoadingById.value = { ...inboxToggleLoadingById.value, [id]: true };
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ setContactInboxHidden: { ok: boolean } }>(setContactInboxHiddenMutation, {
|
await doSetContactInboxHidden({ inboxId: id, hidden });
|
||||||
inboxId: id,
|
|
||||||
hidden,
|
|
||||||
});
|
|
||||||
await refreshCrmData();
|
|
||||||
} finally {
|
} finally {
|
||||||
inboxToggleLoadingById.value = { ...inboxToggleLoadingById.value, [id]: false };
|
inboxToggleLoadingById.value = { ...inboxToggleLoadingById.value, [id]: false };
|
||||||
}
|
}
|
||||||
@@ -4249,8 +4396,8 @@ function makeId(prefix: string) {
|
|||||||
|
|
||||||
function pushPilotNote(text: string) {
|
function pushPilotNote(text: string) {
|
||||||
// Fire-and-forget: log assistant note to the same conversation.
|
// Fire-and-forget: log assistant note to the same conversation.
|
||||||
gqlFetch<{ logPilotNote: { ok: boolean } }>(logPilotNoteMutation, { text })
|
doLogPilotNote({ text })
|
||||||
.then(() => Promise.all([loadPilotMessages(), loadChatConversations()]))
|
.then(() => Promise.all([refetchChatMessages(), refetchChatConversations()]))
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4409,7 +4556,7 @@ async function createCommEvent() {
|
|||||||
commEventSaving.value = true;
|
commEventSaving.value = true;
|
||||||
commEventError.value = "";
|
commEventError.value = "";
|
||||||
try {
|
try {
|
||||||
const res = await gqlFetch<{ createCalendarEvent: CalendarEvent }>(createCalendarEventMutation, {
|
const res = await doCreateCalendarEvent({
|
||||||
input: {
|
input: {
|
||||||
title,
|
title,
|
||||||
start: start.toISOString(),
|
start: start.toISOString(),
|
||||||
@@ -4420,7 +4567,9 @@ async function createCommEvent() {
|
|||||||
archiveNote: commEventMode.value === "logged" ? note : undefined,
|
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);
|
selectedDateKey.value = dayKey(start);
|
||||||
calendarCursor.value = new Date(start.getFullYear(), start.getMonth(), 1);
|
calendarCursor.value = new Date(start.getFullYear(), start.getMonth(), 1);
|
||||||
commDraft.value = "";
|
commDraft.value = "";
|
||||||
@@ -4449,7 +4598,7 @@ async function createCommDocument() {
|
|||||||
commEventSaving.value = true;
|
commEventSaving.value = true;
|
||||||
commEventError.value = "";
|
commEventError.value = "";
|
||||||
try {
|
try {
|
||||||
const res = await gqlFetch<{ createWorkspaceDocument: WorkspaceDocument }>(createWorkspaceDocumentMutation, {
|
const res = await doCreateWorkspaceDocument({
|
||||||
input: {
|
input: {
|
||||||
title,
|
title,
|
||||||
owner: authDisplayName.value,
|
owner: authDisplayName.value,
|
||||||
@@ -4459,8 +4608,13 @@ async function createCommDocument() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
documents.value = [res.createWorkspaceDocument, ...documents.value.filter((doc) => doc.id !== res.createWorkspaceDocument.id)];
|
const created = res?.data?.createWorkspaceDocument;
|
||||||
selectedDocumentId.value = res.createWorkspaceDocument.id;
|
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";
|
contactRightPanelMode.value = "documents";
|
||||||
commDraft.value = "";
|
commDraft.value = "";
|
||||||
commComposerMode.value = "message";
|
commComposerMode.value = "message";
|
||||||
@@ -4482,7 +4636,7 @@ async function sendCommMessage() {
|
|||||||
const channel = commSendChannel.value;
|
const channel = commSendChannel.value;
|
||||||
if (!channel) return;
|
if (!channel) return;
|
||||||
|
|
||||||
await gqlFetch<{ createCommunication: { ok: boolean; id: string } }>(createCommunicationMutation, {
|
await doCreateCommunication({
|
||||||
input: {
|
input: {
|
||||||
contact: selectedCommThread.value.contact,
|
contact: selectedCommThread.value.contact,
|
||||||
channel,
|
channel,
|
||||||
@@ -4493,7 +4647,6 @@ async function sendCommMessage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
commDraft.value = "";
|
commDraft.value = "";
|
||||||
await refreshCrmData();
|
|
||||||
openCommunicationThread(selectedCommThread.value.contact);
|
openCommunicationThread(selectedCommThread.value.contact);
|
||||||
} finally {
|
} finally {
|
||||||
commSending.value = false;
|
commSending.value = false;
|
||||||
@@ -4535,7 +4688,7 @@ async function executeFeedAction(card: FeedCard) {
|
|||||||
const end = new Date(start);
|
const end = new Date(start);
|
||||||
end.setMinutes(end.getMinutes() + 30);
|
end.setMinutes(end.getMinutes() + 30);
|
||||||
|
|
||||||
const res = await gqlFetch<{ createCalendarEvent: CalendarEvent }>(createCalendarEventMutation, {
|
const res = await doCreateCalendarEvent({
|
||||||
input: {
|
input: {
|
||||||
title: `Follow-up: ${card.contact.split(" ")[0] ?? "Contact"}`,
|
title: `Follow-up: ${card.contact.split(" ")[0] ?? "Contact"}`,
|
||||||
start: start.toISOString(),
|
start: start.toISOString(),
|
||||||
@@ -4544,7 +4697,9 @@ async function executeFeedAction(card: FeedCard) {
|
|||||||
note: "Created from feed action.",
|
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);
|
selectedDateKey.value = dayKey(start);
|
||||||
calendarCursor.value = new Date(start.getFullYear(), start.getMonth(), 1);
|
calendarCursor.value = new Date(start.getFullYear(), start.getMonth(), 1);
|
||||||
@@ -4558,7 +4713,7 @@ async function executeFeedAction(card: FeedCard) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (key === "call") {
|
if (key === "call") {
|
||||||
await gqlFetch<{ createCommunication: { ok: boolean; id: string } }>(createCommunicationMutation, {
|
await doCreateCommunication({
|
||||||
input: {
|
input: {
|
||||||
contact: card.contact,
|
contact: card.contact,
|
||||||
channel: "Phone",
|
channel: "Phone",
|
||||||
@@ -4568,13 +4723,12 @@ async function executeFeedAction(card: FeedCard) {
|
|||||||
durationSec: 0,
|
durationSec: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await refreshCrmData();
|
|
||||||
openCommunicationThread(card.contact);
|
openCommunicationThread(card.contact);
|
||||||
return `Call event created and ${card.contact} chat opened.`;
|
return `Call event created and ${card.contact} chat opened.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "draft_message") {
|
if (key === "draft_message") {
|
||||||
await gqlFetch<{ createCommunication: { ok: boolean; id: string } }>(createCommunicationMutation, {
|
await doCreateCommunication({
|
||||||
input: {
|
input: {
|
||||||
contact: card.contact,
|
contact: card.contact,
|
||||||
channel: "Email",
|
channel: "Email",
|
||||||
@@ -4583,7 +4737,6 @@ async function executeFeedAction(card: FeedCard) {
|
|||||||
text: "Draft: onboarding plan + two slots for tomorrow.",
|
text: "Draft: onboarding plan + two slots for tomorrow.",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await refreshCrmData();
|
|
||||||
openCommunicationThread(card.contact);
|
openCommunicationThread(card.contact);
|
||||||
return `Draft message added to ${card.contact} communications.`;
|
return `Draft message added to ${card.contact} communications.`;
|
||||||
}
|
}
|
||||||
@@ -4593,7 +4746,7 @@ async function executeFeedAction(card: FeedCard) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (key === "prepare_question") {
|
if (key === "prepare_question") {
|
||||||
await gqlFetch<{ createCommunication: { ok: boolean; id: string } }>(createCommunicationMutation, {
|
await doCreateCommunication({
|
||||||
input: {
|
input: {
|
||||||
contact: card.contact,
|
contact: card.contact,
|
||||||
channel: "Telegram",
|
channel: "Telegram",
|
||||||
@@ -4602,7 +4755,6 @@ async function executeFeedAction(card: FeedCard) {
|
|||||||
text: "Draft: can you confirm your decision date for this cycle?",
|
text: "Draft: can you confirm your decision date for this cycle?",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await refreshCrmData();
|
|
||||||
openCommunicationThread(card.contact);
|
openCommunicationThread(card.contact);
|
||||||
return `Question about decision date added to ${card.contact} chat.`;
|
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") {
|
if (decision === "rejected") {
|
||||||
const note = "Rejected. Nothing created.";
|
const note = "Rejected. Nothing created.";
|
||||||
card.decisionNote = note;
|
card.decisionNote = note;
|
||||||
await gqlFetch<{ updateFeedDecision: { ok: boolean; id: string } }>(updateFeedDecisionMutation, {
|
await doUpdateFeedDecision({ id: card.id, decision: "rejected", decisionNote: note });
|
||||||
id: card.id,
|
|
||||||
decision: "rejected",
|
|
||||||
decisionNote: note,
|
|
||||||
});
|
|
||||||
pushPilotNote(`[${card.contact}] recommendation rejected: ${card.proposal.title}`);
|
pushPilotNote(`[${card.contact}] recommendation rejected: ${card.proposal.title}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await executeFeedAction(card);
|
const result = await executeFeedAction(card);
|
||||||
card.decisionNote = result;
|
card.decisionNote = result;
|
||||||
await gqlFetch<{ updateFeedDecision: { ok: boolean; id: string } }>(updateFeedDecisionMutation, {
|
await doUpdateFeedDecision({ id: card.id, decision: "accepted", decisionNote: result });
|
||||||
id: card.id,
|
|
||||||
decision: "accepted",
|
|
||||||
decisionNote: result,
|
|
||||||
});
|
|
||||||
pushPilotNote(`[${card.contact}] ${result}`);
|
pushPilotNote(`[${card.contact}] ${result}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ function onSearchInput(event: Event) {
|
|||||||
<span class="shrink-0 text-[10px] text-base-content/55">{{ formatThreadTime(thread.lastAt) }}</span>
|
<span class="shrink-0 text-[10px] text-base-content/55">{{ formatThreadTime(thread.lastAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 min-w-0 truncate text-[11px] text-base-content/75">
|
<p class="mt-0.5 min-w-0 truncate text-[11px] text-base-content/75">
|
||||||
{{ threadChannelLabel(thread) }}
|
{{ thread.lastText || threadChannelLabel(thread) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,39 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
|
import CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
|
||||||
import loginMutation from "~~/graphql/operations/login.graphql?raw";
|
import { useMutation } from "@vue/apollo-composable";
|
||||||
|
import { LoginMutationDocument } from "~~/graphql/generated";
|
||||||
|
|
||||||
const phone = ref("");
|
const phone = ref("");
|
||||||
const password = ref("");
|
const password = ref("");
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const busy = ref(false);
|
const busy = ref(false);
|
||||||
|
|
||||||
async function gqlFetch<TData>(query: string, variables?: Record<string, unknown>) {
|
const { mutate: doLogin } = useMutation(LoginMutationDocument);
|
||||||
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 submit() {
|
async function submit() {
|
||||||
error.value = null;
|
error.value = null;
|
||||||
busy.value = true;
|
busy.value = true;
|
||||||
try {
|
try {
|
||||||
await gqlFetch<{ login: { ok: boolean } }>(loginMutation, {
|
await doLogin({ phone: phone.value, password: password.value });
|
||||||
phone: phone.value,
|
|
||||||
password: password.value,
|
|
||||||
});
|
|
||||||
await navigateTo("/", { replace: true });
|
await navigateTo("/", { replace: true });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.data?.message || e?.message || "Login failed";
|
error.value = e?.data?.message || e?.message || "Login failed";
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import type { CodegenConfig } from "@graphql-codegen/cli";
|
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||||
|
|
||||||
const schemaUrl = process.env.GRAPHQL_SCHEMA_URL || process.env.GRAPHQL_HTTP_ENDPOINT || "http://localhost:3000/api/graphql";
|
|
||||||
|
|
||||||
const config: CodegenConfig = {
|
const config: CodegenConfig = {
|
||||||
schema: schemaUrl,
|
schema: "graphql/schema.graphql",
|
||||||
documents: ["graphql/operations/**/*.graphql"],
|
documents: ["graphql/operations/**/*.graphql"],
|
||||||
generates: {
|
generates: {
|
||||||
"composables/graphql/generated.ts": {
|
"graphql/generated.ts": {
|
||||||
plugins: [
|
plugins: [
|
||||||
"typescript",
|
"typescript",
|
||||||
"typescript-operations",
|
"typescript-operations",
|
||||||
"typed-document-node",
|
|
||||||
"typescript-vue-apollo",
|
"typescript-vue-apollo",
|
||||||
],
|
],
|
||||||
config: {
|
config: {
|
||||||
@@ -18,6 +15,7 @@ const config: CodegenConfig = {
|
|||||||
vueCompositionApiImportFrom: "vue",
|
vueCompositionApiImportFrom: "vue",
|
||||||
dedupeFragments: true,
|
dedupeFragments: true,
|
||||||
namingConvention: "keep",
|
namingConvention: "keep",
|
||||||
|
useTypeImports: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
1689
frontend/graphql/generated.ts
Normal file
1689
frontend/graphql/generated.ts
Normal file
File diff suppressed because it is too large
Load Diff
269
frontend/graphql/schema.graphql
Normal file
269
frontend/graphql/schema.graphql
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
type Query {
|
||||||
|
me: MePayload!
|
||||||
|
chatMessages: [PilotMessage!]!
|
||||||
|
chatConversations: [Conversation!]!
|
||||||
|
contacts: [Contact!]!
|
||||||
|
communications: [CommItem!]!
|
||||||
|
contactInboxes: [ContactInbox!]!
|
||||||
|
calendar: [CalendarEvent!]!
|
||||||
|
deals: [Deal!]!
|
||||||
|
feed: [FeedCard!]!
|
||||||
|
pins: [CommPin!]!
|
||||||
|
documents: [WorkspaceDocument!]!
|
||||||
|
getClientTimeline(contactId: ID!, limit: Int): [ClientTimelineItem!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
login(phone: String!, password: String!): MutationResult!
|
||||||
|
logout: MutationResult!
|
||||||
|
createChatConversation(title: String): Conversation!
|
||||||
|
selectChatConversation(id: ID!): MutationResult!
|
||||||
|
archiveChatConversation(id: ID!): MutationResult!
|
||||||
|
sendPilotMessage(text: String!): MutationResult!
|
||||||
|
confirmLatestChangeSet: MutationResult!
|
||||||
|
rollbackLatestChangeSet: MutationResult!
|
||||||
|
rollbackChangeSetItems(changeSetId: ID!, itemIds: [ID!]!): MutationResult!
|
||||||
|
logPilotNote(text: String!): MutationResult!
|
||||||
|
toggleContactPin(contact: String!, text: String!): PinToggleResult!
|
||||||
|
createCalendarEvent(input: CreateCalendarEventInput!): CalendarEvent!
|
||||||
|
archiveCalendarEvent(input: ArchiveCalendarEventInput!): CalendarEvent!
|
||||||
|
createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
|
||||||
|
createWorkspaceDocument(input: CreateWorkspaceDocumentInput!): WorkspaceDocument!
|
||||||
|
deleteWorkspaceDocument(id: ID!): MutationWithIdResult!
|
||||||
|
updateCommunicationTranscript(id: ID!, transcript: [String!]!): MutationWithIdResult!
|
||||||
|
updateFeedDecision(id: ID!, decision: String!, decisionNote: String): MutationWithIdResult!
|
||||||
|
setContactInboxHidden(inboxId: ID!, hidden: Boolean!): MutationResult!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MutationResult {
|
||||||
|
ok: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MutationWithIdResult {
|
||||||
|
ok: Boolean!
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PinToggleResult {
|
||||||
|
ok: Boolean!
|
||||||
|
pinned: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateCalendarEventInput {
|
||||||
|
title: String!
|
||||||
|
start: String!
|
||||||
|
end: String
|
||||||
|
contact: String
|
||||||
|
note: String
|
||||||
|
archived: Boolean
|
||||||
|
archiveNote: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input ArchiveCalendarEventInput {
|
||||||
|
id: ID!
|
||||||
|
archiveNote: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateCommunicationInput {
|
||||||
|
contact: String!
|
||||||
|
channel: String
|
||||||
|
kind: String
|
||||||
|
direction: String
|
||||||
|
text: String
|
||||||
|
audioUrl: String
|
||||||
|
at: String
|
||||||
|
durationSec: Int
|
||||||
|
transcript: [String!]
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateWorkspaceDocumentInput {
|
||||||
|
title: String!
|
||||||
|
owner: String
|
||||||
|
scope: String!
|
||||||
|
summary: String!
|
||||||
|
body: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type MePayload {
|
||||||
|
user: MeUser!
|
||||||
|
team: MeTeam!
|
||||||
|
conversation: Conversation!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MeUser {
|
||||||
|
id: ID!
|
||||||
|
phone: String!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MeTeam {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Conversation {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
lastMessageAt: String
|
||||||
|
lastMessageText: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type PilotMessage {
|
||||||
|
id: ID!
|
||||||
|
role: String!
|
||||||
|
text: String!
|
||||||
|
messageKind: String
|
||||||
|
requestId: String
|
||||||
|
eventType: String
|
||||||
|
phase: String
|
||||||
|
transient: Boolean
|
||||||
|
thinking: [String!]!
|
||||||
|
tools: [String!]!
|
||||||
|
toolRuns: [PilotToolRun!]!
|
||||||
|
changeSetId: String
|
||||||
|
changeStatus: String
|
||||||
|
changeSummary: String
|
||||||
|
changeItems: [PilotChangeItem!]!
|
||||||
|
createdAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PilotChangeItem {
|
||||||
|
id: ID!
|
||||||
|
entity: String!
|
||||||
|
entityId: String
|
||||||
|
action: String!
|
||||||
|
title: String!
|
||||||
|
before: String!
|
||||||
|
after: String!
|
||||||
|
rolledBack: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PilotToolRun {
|
||||||
|
name: String!
|
||||||
|
status: String!
|
||||||
|
input: String!
|
||||||
|
output: String!
|
||||||
|
at: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientTimelineItem {
|
||||||
|
id: ID!
|
||||||
|
contactId: String!
|
||||||
|
contentType: String!
|
||||||
|
contentId: String!
|
||||||
|
datetime: String!
|
||||||
|
message: CommItem
|
||||||
|
calendarEvent: CalendarEvent
|
||||||
|
recommendation: FeedCard
|
||||||
|
document: WorkspaceDocument
|
||||||
|
}
|
||||||
|
|
||||||
|
type Contact {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
avatar: String!
|
||||||
|
channels: [String!]!
|
||||||
|
lastContactAt: String!
|
||||||
|
description: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommItem {
|
||||||
|
id: ID!
|
||||||
|
at: String!
|
||||||
|
contactId: String!
|
||||||
|
contact: String!
|
||||||
|
contactInboxId: String!
|
||||||
|
sourceExternalId: String!
|
||||||
|
sourceTitle: String!
|
||||||
|
channel: String!
|
||||||
|
kind: String!
|
||||||
|
direction: String!
|
||||||
|
text: String!
|
||||||
|
audioUrl: String!
|
||||||
|
duration: String!
|
||||||
|
waveform: [Float!]!
|
||||||
|
transcript: [String!]!
|
||||||
|
deliveryStatus: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContactInbox {
|
||||||
|
id: ID!
|
||||||
|
contactId: String!
|
||||||
|
contactName: String!
|
||||||
|
channel: String!
|
||||||
|
sourceExternalId: String!
|
||||||
|
title: String!
|
||||||
|
isHidden: Boolean!
|
||||||
|
lastMessageAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalendarEvent {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
start: String!
|
||||||
|
end: String!
|
||||||
|
contact: String!
|
||||||
|
note: String!
|
||||||
|
isArchived: Boolean!
|
||||||
|
createdAt: String!
|
||||||
|
archiveNote: String!
|
||||||
|
archivedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Deal {
|
||||||
|
id: ID!
|
||||||
|
contact: String!
|
||||||
|
title: String!
|
||||||
|
stage: String!
|
||||||
|
amount: String!
|
||||||
|
nextStep: String!
|
||||||
|
summary: String!
|
||||||
|
currentStepId: String!
|
||||||
|
steps: [DealStep!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DealStep {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
description: String!
|
||||||
|
status: String!
|
||||||
|
dueAt: String!
|
||||||
|
order: Int!
|
||||||
|
completedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedCard {
|
||||||
|
id: ID!
|
||||||
|
at: String!
|
||||||
|
contact: String!
|
||||||
|
text: String!
|
||||||
|
proposal: FeedProposal!
|
||||||
|
decision: String!
|
||||||
|
decisionNote: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedProposal {
|
||||||
|
title: String!
|
||||||
|
details: [String!]!
|
||||||
|
key: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommPin {
|
||||||
|
id: ID!
|
||||||
|
contact: String!
|
||||||
|
text: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkspaceDocument {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
type: String!
|
||||||
|
owner: String!
|
||||||
|
scope: String!
|
||||||
|
updatedAt: String!
|
||||||
|
summary: String!
|
||||||
|
body: String!
|
||||||
|
}
|
||||||
@@ -27,6 +27,14 @@ export default defineNuxtConfig({
|
|||||||
default: {
|
default: {
|
||||||
httpEndpoint: process.env.GRAPHQL_HTTP_ENDPOINT || "http://localhost:3000/api/graphql",
|
httpEndpoint: process.env.GRAPHQL_HTTP_ENDPOINT || "http://localhost:3000/api/graphql",
|
||||||
connectToDevTools: process.dev,
|
connectToDevTools: process.dev,
|
||||||
|
httpLinkOptions: {
|
||||||
|
credentials: "include",
|
||||||
|
},
|
||||||
|
defaultOptions: {
|
||||||
|
watchQuery: {
|
||||||
|
fetchPolicy: "cache-and-network",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user