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:
Ruslan Bakiev
2026-02-24 15:05:01 +07:00
parent e5ad3809e0
commit a4d8d81de9
16 changed files with 5643 additions and 4684 deletions

View File

@@ -0,0 +1,205 @@
import { ref, computed, watch } from "vue";
import { useQuery, useMutation } from "@vue/apollo-composable";
import { MeQueryDocument, LogoutMutationDocument } from "~~/graphql/generated";
type TelegramConnectStatus =
| "not_connected"
| "pending_link"
| "pending_business_connection"
| "connected"
| "disabled"
| "no_reply_rights";
type TelegramConnectionSummary = {
businessConnectionId: string;
isEnabled: boolean | null;
canReply: boolean | null;
updatedAt: string;
};
export function useAuth() {
// -------------------------------------------------------------------------
// Auth state
// -------------------------------------------------------------------------
const authMe = ref<{
user: { id: string; phone: string; name: string };
team: { id: string; name: string };
conversation: { id: string; title: string };
} | null>(null);
const authResolved = ref(false);
const apolloAuthReady = computed(() => !!authMe.value);
// -------------------------------------------------------------------------
// Apollo: Me query
// -------------------------------------------------------------------------
const { result: meResult, refetch: refetchMe, loading: meLoading } = useQuery(
MeQueryDocument,
null,
{ fetchPolicy: "network-only" },
);
watch(() => meResult.value?.me, (me) => {
if (me) authMe.value = me as typeof authMe.value;
}, { immediate: true });
// -------------------------------------------------------------------------
// Apollo: Logout mutation
// -------------------------------------------------------------------------
const { mutate: doLogout } = useMutation(LogoutMutationDocument);
// -------------------------------------------------------------------------
// loadMe / logout
// -------------------------------------------------------------------------
async function loadMe() {
const result = await refetchMe();
const me = result?.data?.me;
if (me) authMe.value = me as typeof authMe.value;
}
async function logout() {
await doLogout();
authMe.value = null;
telegramConnectStatus.value = "not_connected";
telegramConnections.value = [];
telegramConnectUrl.value = "";
if (process.client) {
await navigateTo("/login", { replace: true });
}
}
// -------------------------------------------------------------------------
// Telegram connect state
// -------------------------------------------------------------------------
const telegramConnectStatus = ref<TelegramConnectStatus>("not_connected");
const telegramConnectStatusLoading = ref(false);
const telegramConnectBusy = ref(false);
const telegramConnectUrl = ref("");
const telegramConnections = ref<TelegramConnectionSummary[]>([]);
const telegramConnectNotice = ref("");
const telegramStatusLabel = computed(() => {
if (telegramConnectStatusLoading.value) return "Checking";
if (telegramConnectStatus.value === "connected") return "Connected";
if (telegramConnectStatus.value === "pending_link") return "Pending link";
if (telegramConnectStatus.value === "pending_business_connection") return "Waiting business connect";
if (telegramConnectStatus.value === "disabled") return "Disabled";
if (telegramConnectStatus.value === "no_reply_rights") return "No reply rights";
return "Not connected";
});
const telegramStatusBadgeClass = computed(() => {
if (telegramConnectStatus.value === "connected") return "badge-success";
if (telegramConnectStatus.value === "pending_link" || telegramConnectStatus.value === "pending_business_connection") return "badge-warning";
if (telegramConnectStatus.value === "disabled" || telegramConnectStatus.value === "no_reply_rights") return "badge-error";
return "badge-ghost";
});
// -------------------------------------------------------------------------
// Telegram connect functions
// -------------------------------------------------------------------------
async function loadTelegramConnectStatus() {
if (!authMe.value) {
telegramConnectStatus.value = "not_connected";
telegramConnections.value = [];
telegramConnectUrl.value = "";
return;
}
telegramConnectStatusLoading.value = true;
try {
const result = await $fetch<{
ok: boolean;
status: TelegramConnectStatus;
connections?: TelegramConnectionSummary[];
}>("/api/omni/telegram/business/connect/status", {
method: "GET",
});
telegramConnectStatus.value = result?.status ?? "not_connected";
telegramConnections.value = result?.connections ?? [];
} catch {
telegramConnectStatus.value = "not_connected";
telegramConnections.value = [];
} finally {
telegramConnectStatusLoading.value = false;
}
}
async function startTelegramBusinessConnect() {
if (telegramConnectBusy.value) return;
telegramConnectBusy.value = true;
try {
const result = await $fetch<{
ok: boolean;
status: TelegramConnectStatus;
connectUrl: string;
expiresAt: string;
}>("/api/omni/telegram/business/connect/start", { method: "POST" });
telegramConnectStatus.value = result?.status ?? "pending_link";
telegramConnectUrl.value = String(result?.connectUrl ?? "").trim();
if (telegramConnectUrl.value && process.client) {
window.location.href = telegramConnectUrl.value;
}
} catch {
telegramConnectStatus.value = "not_connected";
} finally {
telegramConnectBusy.value = false;
await loadTelegramConnectStatus();
}
}
async function completeTelegramBusinessConnectFromToken(token: string) {
const t = String(token || "").trim();
if (!t) return;
try {
const result = await $fetch<{
ok: boolean;
status: string;
businessConnectionId?: string;
}>("/api/omni/telegram/business/connect/complete", {
method: "POST",
body: { token: t },
});
if (result?.ok) {
telegramConnectStatus.value = "connected";
telegramConnectNotice.value = "Telegram успешно привязан.";
await loadTelegramConnectStatus();
return;
}
if (result?.status === "awaiting_telegram_start") {
telegramConnectNotice.value = "Сначала нажмите Start в Telegram, затем нажмите кнопку в боте снова.";
} else if (result?.status === "invalid_or_expired_token") {
telegramConnectNotice.value = "Ссылка привязки истекла. Нажмите Connect в CRM заново.";
} else {
telegramConnectNotice.value = "Не удалось завершить привязку. Запустите Connect заново.";
}
} catch {
telegramConnectNotice.value = "Ошибка завершения привязки. Попробуйте снова.";
}
}
return {
authMe,
authResolved,
apolloAuthReady,
meLoading,
loadMe,
logout,
// telegram
telegramConnectStatus,
telegramConnectStatusLoading,
telegramConnectBusy,
telegramConnectUrl,
telegramConnections,
telegramConnectNotice,
telegramStatusLabel,
telegramStatusBadgeClass,
loadTelegramConnectStatus,
startTelegramBusinessConnect,
completeTelegramBusinessConnectFromToken,
};
}