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:
85
frontend/app/composables/useContactInboxes.ts
Normal file
85
frontend/app/composables/useContactInboxes.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { ref, watch, type ComputedRef } from "vue";
|
||||
import { useQuery, useMutation } from "@vue/apollo-composable";
|
||||
import {
|
||||
ContactInboxesQueryDocument,
|
||||
SetContactInboxHiddenDocument,
|
||||
} from "~~/graphql/generated";
|
||||
import type { ContactInbox } from "~/composables/crm-types";
|
||||
|
||||
export function useContactInboxes(opts: { apolloAuthReady: ComputedRef<boolean> }) {
|
||||
const { result: contactInboxesResult, refetch: refetchContactInboxes } = useQuery(
|
||||
ContactInboxesQueryDocument,
|
||||
null,
|
||||
{ enabled: opts.apolloAuthReady },
|
||||
);
|
||||
|
||||
const { mutate: doSetContactInboxHidden } = useMutation(SetContactInboxHiddenDocument, {
|
||||
refetchQueries: [{ query: ContactInboxesQueryDocument }],
|
||||
update: (cache, _result, { variables }) => {
|
||||
if (!variables) return;
|
||||
const existing = cache.readQuery({ query: ContactInboxesQueryDocument }) as { contactInboxes?: ContactInbox[] } | null;
|
||||
if (!existing?.contactInboxes) return;
|
||||
cache.writeQuery({
|
||||
query: ContactInboxesQueryDocument,
|
||||
data: {
|
||||
contactInboxes: existing.contactInboxes.map((inbox) =>
|
||||
inbox.id === variables.inboxId ? { ...inbox, isHidden: variables.hidden } : inbox,
|
||||
),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const contactInboxes = ref<ContactInbox[]>([]);
|
||||
const inboxToggleLoadingById = ref<Record<string, boolean>>({});
|
||||
|
||||
watch(() => contactInboxesResult.value?.contactInboxes, (v) => {
|
||||
if (v) contactInboxes.value = v as ContactInbox[];
|
||||
}, { immediate: true });
|
||||
|
||||
function isInboxToggleLoading(inboxId: string) {
|
||||
return Boolean(inboxToggleLoadingById.value[inboxId]);
|
||||
}
|
||||
|
||||
async function setInboxHidden(inboxId: string, hidden: boolean) {
|
||||
const id = String(inboxId ?? "").trim();
|
||||
if (!id || isInboxToggleLoading(id)) return;
|
||||
inboxToggleLoadingById.value = { ...inboxToggleLoadingById.value, [id]: true };
|
||||
try {
|
||||
await doSetContactInboxHidden({ inboxId: id, hidden });
|
||||
} catch (e: unknown) {
|
||||
console.error("[setInboxHidden] mutation failed:", e);
|
||||
} finally {
|
||||
inboxToggleLoadingById.value = { ...inboxToggleLoadingById.value, [id]: false };
|
||||
}
|
||||
}
|
||||
|
||||
function threadInboxes(thread: { id: string }) {
|
||||
return contactInboxes.value
|
||||
.filter((inbox) => inbox.contactId === thread.id)
|
||||
.sort((a, b) => {
|
||||
const aTime = a.lastMessageAt || a.updatedAt;
|
||||
const bTime = b.lastMessageAt || b.updatedAt;
|
||||
return bTime.localeCompare(aTime);
|
||||
});
|
||||
}
|
||||
|
||||
function formatInboxLabel(inbox: ContactInbox) {
|
||||
const title = String(inbox.title ?? "").trim();
|
||||
if (title) return `${inbox.channel} · ${title}`;
|
||||
const source = String(inbox.sourceExternalId ?? "").trim();
|
||||
if (!source) return inbox.channel;
|
||||
const tail = source.length > 18 ? source.slice(-18) : source;
|
||||
return `${inbox.channel} · ${tail}`;
|
||||
}
|
||||
|
||||
return {
|
||||
contactInboxes,
|
||||
inboxToggleLoadingById,
|
||||
setInboxHidden,
|
||||
isInboxToggleLoading,
|
||||
threadInboxes,
|
||||
formatInboxLabel,
|
||||
refetchContactInboxes,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user