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>
306 lines
11 KiB
TypeScript
306 lines
11 KiB
TypeScript
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<PilotMessage[]>;
|
|
refetchAllCrmQueries: () => Promise<void>;
|
|
refetchChatMessages: () => Promise<any>;
|
|
refetchChatConversations: () => Promise<any>;
|
|
}) {
|
|
// ---------------------------------------------------------------------------
|
|
// 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<string, unknown>;
|
|
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,
|
|
};
|
|
}
|