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:
189
frontend/app/composables/useDocuments.ts
Normal file
189
frontend/app/composables/useDocuments.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { ref, computed, watch, watchEffect, type ComputedRef } from "vue";
|
||||
import { useQuery, useMutation } from "@vue/apollo-composable";
|
||||
import {
|
||||
DocumentsQueryDocument,
|
||||
CreateWorkspaceDocumentDocument,
|
||||
DeleteWorkspaceDocumentDocument,
|
||||
} from "~~/graphql/generated";
|
||||
import type { WorkspaceDocument, DocumentSortMode, ClientTimelineItem } from "~/composables/crm-types";
|
||||
import { safeTrim } from "~/composables/crm-types";
|
||||
import { formatDocumentScope } from "~/composables/useWorkspaceDocuments";
|
||||
|
||||
export function useDocuments(opts: { apolloAuthReady: ComputedRef<boolean> }) {
|
||||
const { result: documentsResult, refetch: refetchDocuments } = useQuery(
|
||||
DocumentsQueryDocument,
|
||||
null,
|
||||
{ enabled: opts.apolloAuthReady },
|
||||
);
|
||||
|
||||
const { mutate: doCreateWorkspaceDocument } = useMutation(CreateWorkspaceDocumentDocument, {
|
||||
refetchQueries: [{ query: DocumentsQueryDocument }],
|
||||
});
|
||||
const { mutate: doDeleteWorkspaceDocument } = useMutation(DeleteWorkspaceDocumentDocument, {
|
||||
refetchQueries: [{ query: DocumentsQueryDocument }],
|
||||
});
|
||||
|
||||
const documents = ref<WorkspaceDocument[]>([]);
|
||||
const documentSearch = ref("");
|
||||
const documentSortMode = ref<DocumentSortMode>("updatedAt");
|
||||
const selectedDocumentId = ref(documents.value[0]?.id ?? "");
|
||||
const documentDeletingId = ref("");
|
||||
|
||||
const documentSortOptions: Array<{ value: DocumentSortMode; label: string }> = [
|
||||
{ value: "updatedAt", label: "Updated" },
|
||||
{ value: "title", label: "Title" },
|
||||
{ value: "owner", label: "Owner" },
|
||||
];
|
||||
|
||||
watch(() => documentsResult.value?.documents, (v) => {
|
||||
if (v) documents.value = v as WorkspaceDocument[];
|
||||
}, { immediate: true });
|
||||
|
||||
const filteredDocuments = computed(() => {
|
||||
const query = documentSearch.value.trim().toLowerCase();
|
||||
|
||||
const list = documents.value
|
||||
.filter((item) => {
|
||||
if (!query) return true;
|
||||
const haystack = [item.title, item.summary, item.owner, formatDocumentScope(item.scope), item.body].join(" ").toLowerCase();
|
||||
return haystack.includes(query);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (documentSortMode.value === "title") return a.title.localeCompare(b.title);
|
||||
if (documentSortMode.value === "owner") return a.owner.localeCompare(b.owner);
|
||||
return b.updatedAt.localeCompare(a.updatedAt);
|
||||
});
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (!filteredDocuments.value.length) {
|
||||
selectedDocumentId.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filteredDocuments.value.some((item) => item.id === selectedDocumentId.value)) {
|
||||
const first = filteredDocuments.value[0];
|
||||
if (first) selectedDocumentId.value = first.id;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedDocument = computed(() => documents.value.find((item) => item.id === selectedDocumentId.value));
|
||||
|
||||
function updateSelectedDocumentBody(value: string) {
|
||||
if (!selectedDocument.value) return;
|
||||
selectedDocument.value.body = value;
|
||||
}
|
||||
|
||||
function openDocumentsTab(opts2: { setTab: (tab: string) => void; syncPath: (push: boolean) => void }, push = false) {
|
||||
opts2.setTab("documents");
|
||||
if (!selectedDocumentId.value && filteredDocuments.value.length) {
|
||||
const first = filteredDocuments.value[0];
|
||||
if (first) selectedDocumentId.value = first.id;
|
||||
}
|
||||
opts2.syncPath(push);
|
||||
}
|
||||
|
||||
async function deleteWorkspaceDocumentById(
|
||||
documentIdInput: string,
|
||||
clientTimelineItems: { value: ClientTimelineItem[] },
|
||||
) {
|
||||
const documentId = safeTrim(documentIdInput);
|
||||
if (!documentId) return;
|
||||
if (documentDeletingId.value === documentId) return;
|
||||
|
||||
const target = documents.value.find((doc) => doc.id === documentId);
|
||||
const targetLabel = safeTrim(target?.title) || "this document";
|
||||
if (process.client && !window.confirm(`Delete ${targetLabel}?`)) return;
|
||||
|
||||
documentDeletingId.value = documentId;
|
||||
try {
|
||||
await doDeleteWorkspaceDocument({ id: documentId });
|
||||
documents.value = documents.value.filter((doc) => doc.id !== documentId);
|
||||
clientTimelineItems.value = clientTimelineItems.value.filter((item) => {
|
||||
const isDocumentEntry = String(item.contentType).toLowerCase() === "document";
|
||||
if (!isDocumentEntry) return true;
|
||||
return item.contentId !== documentId && item.document?.id !== documentId;
|
||||
});
|
||||
if (selectedDocumentId.value === documentId) {
|
||||
selectedDocumentId.value = "";
|
||||
}
|
||||
} finally {
|
||||
if (documentDeletingId.value === documentId) {
|
||||
documentDeletingId.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createCommDocument(
|
||||
threadContact: { id: string; contact: string } | undefined,
|
||||
draftText: string,
|
||||
commDocumentForm: { value: { title: string } },
|
||||
authDisplayName: string,
|
||||
additionalCallbacks: {
|
||||
buildScope: (contactId: string, contactName: string) => string;
|
||||
onSuccess: (created: WorkspaceDocument | null) => void;
|
||||
},
|
||||
) {
|
||||
if (!threadContact) return false;
|
||||
|
||||
const summary = draftText.trim();
|
||||
if (!summary) return false;
|
||||
|
||||
const title = safeTrim(commDocumentForm.value.title)
|
||||
|| buildCommDocumentTitle(summary, threadContact.contact);
|
||||
const scope = additionalCallbacks.buildScope(threadContact.id, threadContact.contact);
|
||||
const body = summary;
|
||||
|
||||
try {
|
||||
const res = await doCreateWorkspaceDocument({
|
||||
input: {
|
||||
title,
|
||||
owner: authDisplayName,
|
||||
scope,
|
||||
summary,
|
||||
body,
|
||||
},
|
||||
});
|
||||
|
||||
const created = res?.data?.createWorkspaceDocument;
|
||||
if (created) {
|
||||
documents.value = [created as WorkspaceDocument, ...documents.value.filter((doc) => doc.id !== created.id)];
|
||||
selectedDocumentId.value = created.id;
|
||||
} else {
|
||||
selectedDocumentId.value = "";
|
||||
}
|
||||
additionalCallbacks.onSuccess((created as WorkspaceDocument) ?? null);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildCommDocumentTitle(text: string, contact: string) {
|
||||
const cleaned = text.replace(/\s+/g, " ").trim();
|
||||
if (cleaned) {
|
||||
const sentence = cleaned.split(/[.!?\n]/)[0]?.trim() ?? "";
|
||||
if (sentence) return sentence.slice(0, 120);
|
||||
}
|
||||
return `Документ для ${contact}`;
|
||||
}
|
||||
|
||||
return {
|
||||
documents,
|
||||
documentSearch,
|
||||
documentSortMode,
|
||||
selectedDocumentId,
|
||||
documentDeletingId,
|
||||
documentSortOptions,
|
||||
selectedDocument,
|
||||
filteredDocuments,
|
||||
updateSelectedDocumentBody,
|
||||
createCommDocument,
|
||||
buildCommDocumentTitle,
|
||||
deleteWorkspaceDocumentById,
|
||||
openDocumentsTab,
|
||||
refetchDocuments,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user