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>
158 lines
4.9 KiB
TypeScript
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,
|
|
};
|
|
}
|