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 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<UIMessage>({
|
||||
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<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(
|
||||
() => pilotLiveLogs.value.length,
|
||||
(len) => {
|
||||
@@ -851,52 +1098,23 @@ const renderedPilotMessages = computed<PilotMessage[]>(() => {
|
||||
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() {
|
||||
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<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();
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ function onSearchInput(event: Event) {
|
||||
<span class="shrink-0 text-[10px] text-base-content/55">{{ formatThreadTime(thread.lastAt) }}</span>
|
||||
</div>
|
||||
<p class="mt-0.5 min-w-0 truncate text-[11px] text-base-content/75">
|
||||
{{ threadChannelLabel(thread) }}
|
||||
{{ thread.lastText || threadChannelLabel(thread) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,39 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
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 password = ref("");
|
||||
const error = ref<string | null>(null);
|
||||
const busy = ref(false);
|
||||
|
||||
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;
|
||||
}
|
||||
const { mutate: doLogin } = useMutation(LoginMutationDocument);
|
||||
|
||||
async function submit() {
|
||||
error.value = null;
|
||||
busy.value = true;
|
||||
try {
|
||||
await gqlFetch<{ login: { ok: boolean } }>(loginMutation, {
|
||||
phone: phone.value,
|
||||
password: password.value,
|
||||
});
|
||||
await doLogin({ phone: phone.value, password: password.value });
|
||||
await navigateTo("/", { replace: true });
|
||||
} catch (e: any) {
|
||||
error.value = e?.data?.message || e?.message || "Login failed";
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
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 = {
|
||||
schema: schemaUrl,
|
||||
schema: "graphql/schema.graphql",
|
||||
documents: ["graphql/operations/**/*.graphql"],
|
||||
generates: {
|
||||
"composables/graphql/generated.ts": {
|
||||
"graphql/generated.ts": {
|
||||
plugins: [
|
||||
"typescript",
|
||||
"typescript-operations",
|
||||
"typed-document-node",
|
||||
"typescript-vue-apollo",
|
||||
],
|
||||
config: {
|
||||
@@ -18,6 +15,7 @@ const config: CodegenConfig = {
|
||||
vueCompositionApiImportFrom: "vue",
|
||||
dedupeFragments: true,
|
||||
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: {
|
||||
httpEndpoint: process.env.GRAPHQL_HTTP_ENDPOINT || "http://localhost:3000/api/graphql",
|
||||
connectToDevTools: process.dev,
|
||||
httpLinkOptions: {
|
||||
credentials: "include",
|
||||
},
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: "cache-and-network",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user