import { ref, computed, watch, type ComputedRef, type Ref } from "vue"; import { useQuery } from "@vue/apollo-composable"; import { DealsQueryDocument } from "~~/graphql/generated"; import type { Deal, DealStep, CalendarEvent, Contact } from "~/composables/crm-types"; import { safeTrim, formatDay } from "~/composables/crm-types"; export function useDeals(opts: { apolloAuthReady: ComputedRef; contacts: Ref; calendarEvents: Ref; }) { const { result: dealsResult, refetch: refetchDeals } = useQuery( DealsQueryDocument, null, { enabled: opts.apolloAuthReady }, ); const deals = ref([]); const selectedDealId = ref(deals.value[0]?.id ?? ""); const selectedDealStepsExpanded = ref(false); watch(() => dealsResult.value?.deals, (v) => { if (v) deals.value = v as Deal[]; }, { immediate: true }); const sortedEvents = computed(() => [...opts.calendarEvents.value].sort((a, b) => a.start.localeCompare(b.start))); const selectedWorkspaceDeal = computed(() => { const explicit = deals.value.find((deal) => deal.id === selectedDealId.value); if (explicit) return explicit; const contactName = opts.contacts.value[0]?.name; if (contactName) { const linked = deals.value.find((deal) => deal.contact === contactName); if (linked) return linked; } return null; }); function formatDealHeadline(deal: Deal) { const title = safeTrim(deal.title); const amountRaw = safeTrim(deal.amount); if (!amountRaw) return title; const normalized = amountRaw.replace(/\s+/g, "").replace(",", "."); if (/^\d+(\.\d+)?$/.test(normalized)) { return `${title} за ${new Intl.NumberFormat("ru-RU").format(Number(normalized))} $`; } return `${title} за ${amountRaw}`; } function getDealCurrentStep(deal: Deal) { if (!deal.steps?.length) return null; if (deal.currentStepId) { const explicit = deal.steps.find((step) => step.id === deal.currentStepId); if (explicit) return explicit; } const inProgress = deal.steps.find((step) => step.status === "in_progress"); if (inProgress) return inProgress; const nextTodo = deal.steps.find((step) => step.status !== "done"); return nextTodo ?? deal.steps[deal.steps.length - 1]; } function getDealCurrentStepLabel(deal: Deal) { return safeTrim(getDealCurrentStep(deal)?.title) || safeTrim(deal.nextStep) || safeTrim(deal.stage) || "Без шага"; } function parseDateFromText(input: string) { const text = input.trim(); if (!text) return null; const isoMatch = text.match(/\b(\d{4})-(\d{2})-(\d{2})\b/); if (isoMatch) { const [, y, m, d] = isoMatch; const parsed = new Date(Number(y), Number(m) - 1, Number(d)); if (!Number.isNaN(parsed.getTime())) return parsed; } const ruMatch = text.match(/\b(\d{1,2})[./](\d{1,2})[./](\d{4})\b/); if (ruMatch) { const [, d, m, y] = ruMatch; const parsed = new Date(Number(y), Number(m) - 1, Number(d)); if (!Number.isNaN(parsed.getTime())) return parsed; } return null; } function pluralizeRuDays(days: number) { const mod10 = days % 10; const mod100 = days % 100; if (mod10 === 1 && mod100 !== 11) return "день"; if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return "дня"; return "дней"; } function formatDealDeadline(dueDate: Date) { const today = new Date(); const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const startOfDue = new Date(dueDate.getFullYear(), dueDate.getMonth(), dueDate.getDate()); const dayDiff = Math.round((startOfDue.getTime() - startOfToday.getTime()) / 86_400_000); if (dayDiff < 0) { const overdue = Math.abs(dayDiff); return `просрочено на ${overdue} ${pluralizeRuDays(overdue)}`; } if (dayDiff === 0) return "сегодня"; if (dayDiff === 1) return "завтра"; return `через ${dayDiff} ${pluralizeRuDays(dayDiff)}`; } function isDealStepDone(step: DealStep) { return step.status === "done"; } function formatDealStepMeta(step: DealStep) { if (step.status === "done") return "выполнено"; if (step.status === "blocked") return "заблокировано"; if (!step.dueAt) { if (step.status === "in_progress") return "в работе"; return "без дедлайна"; } const parsed = new Date(step.dueAt); if (Number.isNaN(parsed.getTime())) return "без дедлайна"; return formatDealDeadline(parsed); } function getDealCurrentStepMeta(deal: Deal) { const step = getDealCurrentStep(deal); if (!step) return ""; return formatDealStepMeta(step); } const selectedWorkspaceDealDueDate = computed(() => { const deal = selectedWorkspaceDeal.value; if (!deal) return null; const currentStep = getDealCurrentStep(deal); if (currentStep?.dueAt) { const parsed = new Date(currentStep.dueAt); if (!Number.isNaN(parsed.getTime())) return parsed; } const fromNextStep = parseDateFromText(currentStep?.title || deal.nextStep); if (fromNextStep) return fromNextStep; const now = Date.now(); const contactEvents = sortedEvents.value .filter((event) => event.contact === deal.contact) .map((event) => new Date(event.start)) .filter((date) => !Number.isNaN(date.getTime())) .sort((a, b) => a.getTime() - b.getTime()); const nextUpcoming = contactEvents.find((date) => date.getTime() >= now); if (nextUpcoming) return nextUpcoming; return contactEvents.length ? contactEvents[contactEvents.length - 1] : null; }); const selectedWorkspaceDealSubtitle = computed(() => { const deal = selectedWorkspaceDeal.value; if (!deal) return ""; const stepLabel = getDealCurrentStepLabel(deal); const dueDate = selectedWorkspaceDealDueDate.value; if (!dueDate) return `${stepLabel} · без дедлайна`; return `${stepLabel} · ${formatDealDeadline(dueDate)}`; }); const selectedWorkspaceDealSteps = computed(() => { const deal = selectedWorkspaceDeal.value; if (!deal?.steps?.length) return []; return [...deal.steps].sort((a, b) => a.order - b.order); }); watch( () => selectedWorkspaceDeal.value?.id ?? "", () => { selectedDealStepsExpanded.value = false; }, ); return { deals, selectedDealId, selectedDealStepsExpanded, selectedWorkspaceDeal, selectedWorkspaceDealDueDate, selectedWorkspaceDealSubtitle, selectedWorkspaceDealSteps, formatDealHeadline, getDealCurrentStep, getDealCurrentStepLabel, getDealCurrentStepMeta, formatDealDeadline, isDealStepDone, formatDealStepMeta, refetchDeals, }; }