import { ref, type Ref, type ComputedRef } from "vue"; import type { TabId, CalendarView, PeopleLeftMode, CalendarEvent, PilotChangeItem } from "~/composables/crm-types"; import { safeTrim, dayKey } from "~/composables/crm-types"; export function useWorkspaceRouting(opts: { selectedTab: Ref; peopleLeftMode: Ref; peopleListMode: Ref<"contacts" | "deals">; selectedContactId: Ref; selectedCommThreadId: Ref; selectedDealId: Ref; selectedChatId: Ref; calendarView: Ref; calendarCursor: Ref; selectedDateKey: Ref; selectedDocumentId: Ref; focusedCalendarEventId: Ref; activeChangeSetId: Ref; activeChangeStep: Ref; // computed refs sortedEvents: ComputedRef; commThreads: ComputedRef<{ id: string; [key: string]: any }[]>; contacts: Ref<{ id: string; name: string; [key: string]: any }[]>; deals: Ref<{ id: string; contact: string; [key: string]: any }[]>; commItems: Ref<{ id: string; contact: string; [key: string]: any }[]>; activeChangeMessage: ComputedRef<{ changeSetId?: string | null; changeItems?: PilotChangeItem[] | null } | null>; activeChangeItem: ComputedRef; activeChangeItems: ComputedRef; activeChangeIndex: ComputedRef; authMe: Ref<{ conversation: { id: string } } | null>; // functions from outside pickDate: (key: string) => void; openCommunicationThread: (contact: string) => void; completeTelegramBusinessConnectFromToken: (token: string) => void; }) { const uiPathSyncLocked = ref(false); let popstateHandler: (() => void) | null = null; // --------------------------------------------------------------------------- // Calendar route helpers (internal) // --------------------------------------------------------------------------- function calendarCursorToken(date: Date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); return `${y}-${m}`; } function calendarRouteToken(view: CalendarView) { if (view === "day" || view === "week") { return opts.selectedDateKey.value; } if (view === "year") { return String(opts.calendarCursor.value.getFullYear()); } return calendarCursorToken(opts.calendarCursor.value); } function parseCalendarCursorToken(token: string | null | undefined) { const text = String(token ?? "").trim(); const m = text.match(/^(\d{4})-(\d{2})$/); if (!m) return null; const year = Number(m[1]); const month = Number(m[2]); if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) return null; return new Date(year, month - 1, 1); } function parseCalendarDateToken(token: string | null | undefined) { const text = String(token ?? "").trim(); const m = text.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (!m) return null; const year = Number(m[1]); const month = Number(m[2]); const day = Number(m[3]); if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return null; if (month < 1 || month > 12 || day < 1 || day > 31) return null; const parsed = new Date(year, month - 1, day); if (Number.isNaN(parsed.getTime())) return null; return parsed; } function parseCalendarYearToken(token: string | null | undefined) { const text = String(token ?? "").trim(); const m = text.match(/^(\d{4})$/); if (!m) return null; const year = Number(m[1]); if (!Number.isFinite(year)) return null; return year; } // --------------------------------------------------------------------------- // Core routing functions // --------------------------------------------------------------------------- function normalizedConversationId() { return safeTrim(opts.selectedChatId.value || opts.authMe.value?.conversation.id || "pilot"); } function withReviewQuery(path: string) { const reviewSet = opts.activeChangeSetId.value.trim(); if (!reviewSet) return path; const params = new URLSearchParams(); params.set("reviewSet", reviewSet); params.set("reviewStep", String(Math.max(1, opts.activeChangeStep.value + 1))); return `${path}?${params.toString()}`; } function currentUiPath() { if (opts.selectedTab.value === "documents") { const docId = opts.selectedDocumentId.value.trim(); if (docId) { return withReviewQuery(`/documents/${encodeURIComponent(docId)}`); } return withReviewQuery("/documents"); } if (opts.peopleLeftMode.value === "calendar") { if (opts.focusedCalendarEventId.value.trim()) { return withReviewQuery(`/calendar/event/${encodeURIComponent(opts.focusedCalendarEventId.value.trim())}`); } return withReviewQuery(`/calendar/${encodeURIComponent(opts.calendarView.value)}/${encodeURIComponent(calendarRouteToken(opts.calendarView.value))}`); } if (opts.peopleListMode.value === "deals" && opts.selectedDealId.value.trim()) { return withReviewQuery(`/deal/${encodeURIComponent(opts.selectedDealId.value.trim())}`); } if (opts.selectedContactId.value.trim()) { return withReviewQuery(`/contact/${encodeURIComponent(opts.selectedContactId.value.trim())}`); } return withReviewQuery(`/chat/${encodeURIComponent(normalizedConversationId())}`); } function syncPathFromUi(push = false) { if (process.server) return; const nextPath = currentUiPath(); const currentPath = `${window.location.pathname}${window.location.search}`; if (nextPath === currentPath) return; if (push) { window.history.pushState({}, "", nextPath); } else { window.history.replaceState({}, "", nextPath); } } function applyPathToUi(pathname: string, search = "") { const path = String(pathname || "/").trim() || "/"; const params = new URLSearchParams(String(search || "")); const reviewSet = (params.get("reviewSet") ?? "").trim(); const reviewStep = Number(params.get("reviewStep") ?? "1"); if (reviewSet) { opts.activeChangeSetId.value = reviewSet; opts.activeChangeStep.value = Number.isFinite(reviewStep) && reviewStep > 0 ? reviewStep - 1 : 0; } else { opts.activeChangeSetId.value = ""; opts.activeChangeStep.value = 0; } const calendarEventMatch = path.match(/^\/calendar\/event\/([^/]+)\/?$/i); if (calendarEventMatch) { const rawEventId = decodeURIComponent(calendarEventMatch[1] ?? "").trim(); opts.selectedTab.value = "communications"; opts.peopleLeftMode.value = "calendar"; const event = opts.sortedEvents.value.find((x) => x.id === rawEventId); if (event) { opts.pickDate(event.start.slice(0, 10)); } opts.focusedCalendarEventId.value = rawEventId; return; } const calendarMatch = path.match(/^\/calendar\/([^/]+)\/([^/]+)\/?$/i); if (calendarMatch) { const rawView = decodeURIComponent(calendarMatch[1] ?? "").trim(); const rawCursor = decodeURIComponent(calendarMatch[2] ?? "").trim(); const view = (["day", "week", "month", "year", "agenda"] as CalendarView[]).includes(rawView as CalendarView) ? (rawView as CalendarView) : "month"; const cursorByMonth = parseCalendarCursorToken(rawCursor); const cursorByDate = parseCalendarDateToken(rawCursor); const cursorByYear = parseCalendarYearToken(rawCursor); opts.selectedTab.value = "communications"; opts.peopleLeftMode.value = "calendar"; opts.focusedCalendarEventId.value = ""; opts.calendarView.value = view; if (view === "day" || view === "week") { const parsed = cursorByDate; if (parsed) { opts.selectedDateKey.value = dayKey(parsed); opts.calendarCursor.value = new Date(parsed.getFullYear(), parsed.getMonth(), 1); } } else if (view === "year") { if (cursorByYear) { opts.calendarCursor.value = new Date(cursorByYear, 0, 1); opts.selectedDateKey.value = dayKey(new Date(cursorByYear, 0, 1)); } } else if (cursorByMonth) { opts.calendarCursor.value = cursorByMonth; opts.selectedDateKey.value = dayKey(cursorByMonth); } return; } const documentsMatch = path.match(/^\/documents(?:\/([^/]+))?\/?$/i); if (documentsMatch) { const rawDocumentId = decodeURIComponent(documentsMatch[1] ?? "").trim(); opts.selectedTab.value = "documents"; opts.focusedCalendarEventId.value = ""; if (rawDocumentId) opts.selectedDocumentId.value = rawDocumentId; return; } const contactMatch = path.match(/^\/contact\/([^/]+)\/?$/i); if (contactMatch) { const rawContactId = decodeURIComponent(contactMatch[1] ?? "").trim(); opts.selectedTab.value = "communications"; opts.peopleLeftMode.value = "contacts"; opts.peopleListMode.value = "contacts"; if (rawContactId) { opts.selectedContactId.value = rawContactId; const linkedThread = opts.commThreads.value.find((thread) => thread.id === rawContactId); if (linkedThread) opts.selectedCommThreadId.value = linkedThread.id; } opts.focusedCalendarEventId.value = ""; return; } const dealMatch = path.match(/^\/deal\/([^/]+)\/?$/i); if (dealMatch) { const rawDealId = decodeURIComponent(dealMatch[1] ?? "").trim(); opts.selectedTab.value = "communications"; opts.peopleLeftMode.value = "contacts"; opts.peopleListMode.value = "deals"; if (rawDealId) { opts.selectedDealId.value = rawDealId; const linkedDeal = opts.deals.value.find((deal) => deal.id === rawDealId); const linkedContact = linkedDeal ? opts.contacts.value.find((contact) => contact.name === linkedDeal.contact) : null; if (linkedContact) { opts.selectedContactId.value = linkedContact.id; opts.selectedCommThreadId.value = linkedContact.id; } } opts.focusedCalendarEventId.value = ""; return; } const chatMatch = path.match(/^\/chat\/([^/]+)\/?$/i); if (chatMatch) { const rawChatId = decodeURIComponent(chatMatch[1] ?? "").trim(); opts.selectedTab.value = "communications"; opts.peopleLeftMode.value = "contacts"; opts.peopleListMode.value = "contacts"; opts.focusedCalendarEventId.value = ""; if (rawChatId) opts.selectedChatId.value = rawChatId; return; } const changesMatch = path.match(/^\/changes\/([^/]+)(?:\/step\/(\d+))?\/?$/i); if (changesMatch) { const rawId = decodeURIComponent(changesMatch[1] ?? "").trim(); const rawStep = Number(changesMatch[2] ?? "1"); if (rawId) { opts.activeChangeSetId.value = rawId; opts.activeChangeStep.value = Number.isFinite(rawStep) && rawStep > 0 ? rawStep - 1 : 0; } opts.selectedTab.value = "communications"; opts.peopleLeftMode.value = "contacts"; opts.peopleListMode.value = "contacts"; opts.focusedCalendarEventId.value = ""; return; } opts.selectedTab.value = "communications"; opts.peopleLeftMode.value = "contacts"; opts.peopleListMode.value = "contacts"; opts.focusedCalendarEventId.value = ""; } function applyReviewStepToUi(push = false) { const item = opts.activeChangeItem.value; if (!item) { syncPathFromUi(push); return; } opts.selectedTab.value = "communications"; if (item.entity === "calendar_event" && item.entityId) { opts.peopleLeftMode.value = "calendar"; opts.calendarView.value = "month"; const event = opts.sortedEvents.value.find((x) => x.id === item.entityId); if (event) { opts.pickDate(event.start.slice(0, 10)); } opts.focusedCalendarEventId.value = item.entityId; syncPathFromUi(push); return; } if (item.entity === "contact_note" && item.entityId) { opts.peopleLeftMode.value = "contacts"; opts.peopleListMode.value = "contacts"; opts.selectedContactId.value = item.entityId; const thread = opts.commThreads.value.find((entry) => entry.id === item.entityId); if (thread) opts.selectedCommThreadId.value = thread.id; opts.focusedCalendarEventId.value = ""; syncPathFromUi(push); return; } if (item.entity === "deal" && item.entityId) { opts.peopleLeftMode.value = "contacts"; opts.peopleListMode.value = "deals"; opts.selectedDealId.value = item.entityId; const deal = opts.deals.value.find((entry) => entry.id === item.entityId); if (deal) { const contact = opts.contacts.value.find((entry) => entry.name === deal.contact); if (contact) { opts.selectedContactId.value = contact.id; opts.selectedCommThreadId.value = contact.id; } } opts.focusedCalendarEventId.value = ""; syncPathFromUi(push); return; } if (item.entity === "message" && item.entityId) { opts.peopleLeftMode.value = "contacts"; opts.peopleListMode.value = "contacts"; const message = opts.commItems.value.find((entry) => entry.id === item.entityId); if (message?.contact) { opts.openCommunicationThread(message.contact); } opts.focusedCalendarEventId.value = ""; syncPathFromUi(push); return; } if (item.entity === "workspace_document" && item.entityId) { opts.selectedTab.value = "documents"; opts.selectedDocumentId.value = item.entityId; opts.focusedCalendarEventId.value = ""; syncPathFromUi(push); return; } opts.peopleLeftMode.value = "contacts"; opts.focusedCalendarEventId.value = ""; syncPathFromUi(push); } // --------------------------------------------------------------------------- // Lifecycle init / cleanup // --------------------------------------------------------------------------- function initRouting() { uiPathSyncLocked.value = true; try { const params = new URLSearchParams(window.location.search); const tgLinkToken = String(params.get("tg_link_token") ?? "").trim(); if (tgLinkToken) { void opts.completeTelegramBusinessConnectFromToken(tgLinkToken); params.delete("tg_link_token"); const nextSearch = params.toString(); window.history.replaceState({}, "", `${window.location.pathname}${nextSearch ? `?${nextSearch}` : ""}`); } applyPathToUi(window.location.pathname, window.location.search); } finally { uiPathSyncLocked.value = false; } syncPathFromUi(false); popstateHandler = () => { uiPathSyncLocked.value = true; try { applyPathToUi(window.location.pathname, window.location.search); } finally { uiPathSyncLocked.value = false; } }; window.addEventListener("popstate", popstateHandler); } function cleanupRouting() { if (popstateHandler) { window.removeEventListener("popstate", popstateHandler); popstateHandler = null; } } return { uiPathSyncLocked, currentUiPath, applyPathToUi, syncPathFromUi, applyReviewStepToUi, withReviewQuery, initRouting, cleanupRouting, }; }