Files
clientsflow/frontend/app/composables/useFeed.ts
Ruslan Bakiev a4d8d81de9 refactor: decompose CrmWorkspaceApp.vue into 15 composables
Split the 6000+ line monolithic component into modular composables:
- crm-types.ts: shared types and utility functions
- useAuth, useContacts, useContactInboxes, useCalendar, useDeals,
  useDocuments, useFeed, useTimeline, usePilotChat, useCallAudio,
  usePins, useChangeReview, useCrmRealtime, useWorkspaceRouting
CrmWorkspaceApp.vue is now a thin orchestrator (~2500 lines) that
wires composables together with glue code, keeping template and
styles intact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:05:01 +07:00

158 lines
4.9 KiB
TypeScript

import { ref, watch, type ComputedRef } from "vue";
import { useQuery, useMutation } from "@vue/apollo-composable";
import {
FeedQueryDocument,
UpdateFeedDecisionMutationDocument,
CreateCalendarEventMutationDocument,
CreateCommunicationMutationDocument,
LogPilotNoteMutationDocument,
CalendarQueryDocument,
CommunicationsQueryDocument,
ContactInboxesQueryDocument,
ChatMessagesQueryDocument,
ChatConversationsQueryDocument,
} from "~~/graphql/generated";
import type { FeedCard, CalendarEvent } from "~/composables/crm-types";
import { dayKey, formatDay, formatTime } from "~/composables/crm-types";
export function useFeed(opts: {
apolloAuthReady: ComputedRef<boolean>;
onCreateFollowup: (card: FeedCard, event: CalendarEvent) => void;
onOpenComm: (card: FeedCard) => void;
}) {
const { result: feedResult, refetch: refetchFeed } = useQuery(
FeedQueryDocument,
null,
{ enabled: opts.apolloAuthReady },
);
const { mutate: doUpdateFeedDecision } = useMutation(UpdateFeedDecisionMutationDocument, {
refetchQueries: [{ query: FeedQueryDocument }],
});
const { mutate: doCreateCalendarEvent } = useMutation(CreateCalendarEventMutationDocument, {
refetchQueries: [{ query: CalendarQueryDocument }],
});
const { mutate: doCreateCommunication } = useMutation(CreateCommunicationMutationDocument, {
refetchQueries: [{ query: CommunicationsQueryDocument }, { query: ContactInboxesQueryDocument }],
});
const { mutate: doLogPilotNote } = useMutation(LogPilotNoteMutationDocument);
const feedCards = ref<FeedCard[]>([]);
watch(() => feedResult.value?.feed, (v) => {
if (v) feedCards.value = v as FeedCard[];
}, { immediate: true });
function pushPilotNote(text: string) {
doLogPilotNote({ text })
.then(() => {})
.catch(() => {});
}
async function executeFeedAction(card: FeedCard) {
const key = card.proposal.key;
if (key === "create_followup") {
const start = new Date();
start.setMinutes(start.getMinutes() + 30);
start.setSeconds(0, 0);
const end = new Date(start);
end.setMinutes(end.getMinutes() + 30);
const res = await doCreateCalendarEvent({
input: {
title: `Follow-up: ${card.contact.split(" ")[0] ?? "Contact"}`,
start: start.toISOString(),
end: end.toISOString(),
contact: card.contact,
note: "Created from feed action.",
},
});
const created = res?.data?.createCalendarEvent as CalendarEvent | undefined;
if (created) {
opts.onCreateFollowup(card, created);
}
return `Event created: Follow-up · ${formatDay(start.toISOString())} ${formatTime(start.toISOString())} · ${card.contact}`;
}
if (key === "open_comm") {
opts.onOpenComm(card);
return `Opened ${card.contact} communication thread.`;
}
if (key === "call") {
await doCreateCommunication({
input: {
contact: card.contact,
channel: "Phone",
kind: "call",
direction: "out",
text: "Call started from feed",
durationSec: 0,
},
});
opts.onOpenComm(card);
return `Call event created and ${card.contact} chat opened.`;
}
if (key === "draft_message") {
await doCreateCommunication({
input: {
contact: card.contact,
channel: "Email",
kind: "message",
direction: "out",
text: "Draft: onboarding plan + two slots for tomorrow.",
},
});
opts.onOpenComm(card);
return `Draft message added to ${card.contact} communications.`;
}
if (key === "run_summary") {
return "Call summary prepared: 5 next steps sent to Pilot.";
}
if (key === "prepare_question") {
await doCreateCommunication({
input: {
contact: card.contact,
channel: "Telegram",
kind: "message",
direction: "out",
text: "Draft: can you confirm your decision date for this cycle?",
},
});
opts.onOpenComm(card);
return `Question about decision date added to ${card.contact} chat.`;
}
return "Action completed.";
}
async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") {
card.decision = decision;
if (decision === "rejected") {
const note = "Rejected. Nothing created.";
card.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 doUpdateFeedDecision({ id: card.id, decision: "accepted", decisionNote: result });
pushPilotNote(`[${card.contact}] ${result}`);
}
return {
feedCards,
decideFeedCard,
executeFeedAction,
pushPilotNote,
refetchFeed,
};
}