Each composable now owns its types and exports them. Other composables import types from the owning composable. Deleted centralized crm-types.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
173 lines
5.2 KiB
TypeScript
173 lines
5.2 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 { CalendarEvent } from "~/composables/useCalendar";
|
|
import { dayKey, formatDay, formatTime } from "~/composables/useCalendar";
|
|
|
|
export type FeedCard = {
|
|
id: string;
|
|
at: string;
|
|
contact: string;
|
|
text: string;
|
|
proposal: {
|
|
title: string;
|
|
details: string[];
|
|
key: "create_followup" | "open_comm" | "call" | "draft_message" | "run_summary" | "prepare_question";
|
|
};
|
|
decision: "pending" | "accepted" | "rejected";
|
|
decisionNote?: string;
|
|
};
|
|
|
|
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,
|
|
};
|
|
}
|