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>
190 lines
6.4 KiB
TypeScript
190 lines
6.4 KiB
TypeScript
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,
|
|
};
|
|
}
|