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:
305
frontend/app/composables/useChangeReview.ts
Normal file
305
frontend/app/composables/useChangeReview.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user