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>
This commit is contained in:
157
frontend/app/composables/useFeed.ts
Normal file
157
frontend/app/composables/useFeed.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user