import { ref, computed, watch, type Ref } from "vue"; import { useMutation } from "@vue/apollo-composable"; import { ConfirmLatestChangeSetMutationDocument, RollbackLatestChangeSetMutationDocument, RollbackChangeSetItemsMutationDocument, ChatMessagesQueryDocument, ChatConversationsQueryDocument, ContactsQueryDocument, CommunicationsQueryDocument, ContactInboxesQueryDocument, CalendarQueryDocument, DealsQueryDocument, FeedQueryDocument, PinsQueryDocument, DocumentsQueryDocument, } from "~~/graphql/generated"; import type { PilotMessage, PilotChangeItem } from "~/composables/crm-types"; export function useChangeReview(opts: { pilotMessages: Ref; refetchAllCrmQueries: () => Promise; refetchChatMessages: () => Promise; refetchChatConversations: () => Promise; }) { // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- const activeChangeSetId = ref(""); const activeChangeStep = ref(0); const changeActionBusy = ref(false); // --------------------------------------------------------------------------- // All CRM query docs for refetch // --------------------------------------------------------------------------- const allCrmQueryDocs = [ { query: ContactsQueryDocument }, { query: CommunicationsQueryDocument }, { query: ContactInboxesQueryDocument }, { query: CalendarQueryDocument }, { query: DealsQueryDocument }, { query: FeedQueryDocument }, { query: PinsQueryDocument }, { query: DocumentsQueryDocument }, ]; // --------------------------------------------------------------------------- // Apollo Mutations // --------------------------------------------------------------------------- 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], }); // --------------------------------------------------------------------------- // Computed // --------------------------------------------------------------------------- const latestChangeMessage = computed(() => { return ( [...opts.pilotMessages.value] .reverse() .find((m) => m.role === "assistant" && m.changeSetId && m.changeStatus !== "rolled_back") ?? null ); }); const activeChangeMessage = computed(() => { const targetId = activeChangeSetId.value.trim(); if (!targetId) return latestChangeMessage.value; return ( [...opts.pilotMessages.value] .reverse() .find((m) => m.role === "assistant" && m.changeSetId === targetId) ?? null ); }); const activeChangeItems = computed(() => activeChangeMessage.value?.changeItems ?? []); const activeChangeIndex = computed(() => { const items = activeChangeItems.value; if (!items.length) return 0; return Math.max(0, Math.min(activeChangeStep.value, items.length - 1)); }); const activeChangeItem = computed(() => { const items = activeChangeItems.value; if (!items.length) return null; return items[activeChangeIndex.value] ?? null; }); const reviewActive = computed(() => Boolean(activeChangeSetId.value.trim() && activeChangeItems.value.length > 0)); const activeReviewCalendarEventId = computed(() => { const item = activeChangeItem.value; if (!item || item.entity !== "calendar_event" || !item.entityId) return ""; return item.entityId; }); const activeReviewContactId = computed(() => { const item = activeChangeItem.value; if (!item || item.entity !== "contact_note" || !item.entityId) return ""; return item.entityId; }); const activeReviewDealId = computed(() => { const item = activeChangeItem.value; if (!item || item.entity !== "deal" || !item.entityId) return ""; return item.entityId; }); const activeReviewMessageId = computed(() => { const item = activeChangeItem.value; if (!item || item.entity !== "message" || !item.entityId) return ""; return item.entityId; }); const activeReviewContactDiff = computed(() => { const item = activeChangeItem.value; if (!item || item.entity !== "contact_note" || !item.entityId) return null; return { contactId: item.entityId, before: normalizeChangeText(item.before), after: normalizeChangeText(item.after), }; }); // --------------------------------------------------------------------------- // Text helpers // --------------------------------------------------------------------------- function normalizeChangeText(raw: string | null | undefined) { const text = String(raw ?? "").trim(); if (!text) return ""; try { const parsed = JSON.parse(text) as Record; if (typeof parsed === "object" && parsed) { const candidate = [parsed.description, parsed.summary, parsed.note, parsed.text] .find((value) => typeof value === "string"); if (typeof candidate === "string") return candidate.trim(); } } catch { // No-op: keep original text when it is not JSON payload. } return text; } function describeChangeEntity(entity: string) { if (entity === "contact_note") return "Contact summary"; if (entity === "calendar_event") return "Calendar event"; if (entity === "message") return "Message"; if (entity === "deal") return "Deal"; if (entity === "workspace_document") return "Workspace document"; return entity || "Change"; } function describeChangeAction(action: string) { if (action === "created") return "created"; if (action === "updated") return "updated"; if (action === "deleted") return "archived"; return action || "changed"; } // --------------------------------------------------------------------------- // Review navigation // --------------------------------------------------------------------------- function openChangeReview(changeSetId: string, step = 0) { const targetId = String(changeSetId ?? "").trim(); if (!targetId) return; activeChangeSetId.value = targetId; const items = activeChangeMessage.value?.changeItems ?? []; activeChangeStep.value = items.length ? Math.max(0, Math.min(step, items.length - 1)) : 0; } function goToChangeStep(step: number) { const items = activeChangeItems.value; if (!items.length) return; activeChangeStep.value = Math.max(0, Math.min(step, items.length - 1)); } function goToPreviousChangeStep() { goToChangeStep(activeChangeIndex.value - 1); } function goToNextChangeStep() { goToChangeStep(activeChangeIndex.value + 1); } function finishReview() { activeChangeSetId.value = ""; activeChangeStep.value = 0; } // --------------------------------------------------------------------------- // Highlight helpers // --------------------------------------------------------------------------- function isReviewHighlightedEvent(eventId: string) { return Boolean(reviewActive.value && activeReviewCalendarEventId.value && activeReviewCalendarEventId.value === eventId); } function isReviewHighlightedContact(contactId: string) { return Boolean(reviewActive.value && activeReviewContactId.value && activeReviewContactId.value === contactId); } function isReviewHighlightedDeal(dealId: string) { return Boolean(reviewActive.value && activeReviewDealId.value && activeReviewDealId.value === dealId); } function isReviewHighlightedMessage(messageId: string) { return Boolean(reviewActive.value && activeReviewMessageId.value && activeReviewMessageId.value === messageId); } // --------------------------------------------------------------------------- // Change execution // --------------------------------------------------------------------------- async function confirmLatestChangeSet() { if (changeActionBusy.value || !latestChangeMessage.value?.changeSetId) return; changeActionBusy.value = true; try { await doConfirmLatestChangeSet(); } finally { changeActionBusy.value = false; } } async function rollbackLatestChangeSet() { if (changeActionBusy.value || !latestChangeMessage.value?.changeSetId) return; changeActionBusy.value = true; try { await doRollbackLatestChangeSet(); activeChangeSetId.value = ""; activeChangeStep.value = 0; } finally { changeActionBusy.value = false; } } async function rollbackSelectedChangeItems() { const targetChangeSetId = activeChangeMessage.value?.changeSetId?.trim() || activeChangeSetId.value.trim(); const itemIds = activeChangeItems.value.filter((item) => !item.rolledBack).map((item) => item.id); if (changeActionBusy.value || !targetChangeSetId || itemIds.length === 0) return; changeActionBusy.value = true; try { await doRollbackChangeSetItems({ changeSetId: targetChangeSetId, itemIds }); } finally { changeActionBusy.value = false; } } async function rollbackChangeItemById(itemId: string) { const item = activeChangeItems.value.find((entry) => entry.id === itemId); const targetChangeSetId = activeChangeMessage.value?.changeSetId?.trim() || activeChangeSetId.value.trim(); if (!item || item.rolledBack || !targetChangeSetId || changeActionBusy.value) return; changeActionBusy.value = true; try { await doRollbackChangeSetItems({ changeSetId: targetChangeSetId, itemIds: [itemId] }); } finally { changeActionBusy.value = false; } } // --------------------------------------------------------------------------- // Watcher: clamp step when change items list changes // --------------------------------------------------------------------------- watch( () => activeChangeMessage.value?.changeSetId, () => { if (!activeChangeSetId.value.trim()) return; const maxIndex = Math.max(0, (activeChangeItems.value.length || 1) - 1); if (activeChangeStep.value > maxIndex) activeChangeStep.value = maxIndex; }, ); // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- return { activeChangeSetId, activeChangeStep, changeActionBusy, reviewActive, activeChangeItems, activeChangeItem, activeChangeIndex, openChangeReview, goToChangeStep, goToPreviousChangeStep, goToNextChangeStep, finishReview, isReviewHighlightedEvent, isReviewHighlightedContact, isReviewHighlightedDeal, isReviewHighlightedMessage, activeReviewCalendarEventId, activeReviewContactId, activeReviewDealId, activeReviewMessageId, activeReviewContactDiff, confirmLatestChangeSet, rollbackLatestChangeSet, rollbackSelectedChangeItems, rollbackChangeItemById, describeChangeEntity, describeChangeAction, normalizeChangeText, }; }