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>
1130 lines
40 KiB
TypeScript
1130 lines
40 KiB
TypeScript
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, type ComputedRef } from "vue";
|
||
import { useQuery, useMutation } from "@vue/apollo-composable";
|
||
import {
|
||
CalendarQueryDocument,
|
||
CreateCalendarEventMutationDocument,
|
||
ArchiveCalendarEventMutationDocument,
|
||
} from "~~/graphql/generated";
|
||
import type { CalendarEvent, CalendarView } from "~/composables/crm-types";
|
||
import {
|
||
dayKey,
|
||
formatDay,
|
||
formatTime,
|
||
toInputDate,
|
||
toInputTime,
|
||
roundToNextQuarter,
|
||
roundToPrevQuarter,
|
||
isEventFinalStatus,
|
||
} from "~/composables/crm-types";
|
||
|
||
type CalendarHierarchyView = "year" | "month" | "week" | "day";
|
||
type CalendarRect = { left: number; top: number; width: number; height: number };
|
||
|
||
export function useCalendar(opts: { apolloAuthReady: ComputedRef<boolean> }) {
|
||
// ---------------------------------------------------------------------------
|
||
// Apollo query & mutation
|
||
// ---------------------------------------------------------------------------
|
||
const { result: calendarResult, refetch: refetchCalendar } = useQuery(
|
||
CalendarQueryDocument,
|
||
null,
|
||
{ enabled: opts.apolloAuthReady },
|
||
);
|
||
|
||
const { mutate: doCreateCalendarEvent } = useMutation(CreateCalendarEventMutationDocument, {
|
||
refetchQueries: [{ query: CalendarQueryDocument }],
|
||
});
|
||
const { mutate: doArchiveCalendarEvent } = useMutation(ArchiveCalendarEventMutationDocument, {
|
||
refetchQueries: [{ query: CalendarQueryDocument }],
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Core state
|
||
// ---------------------------------------------------------------------------
|
||
const calendarEvents = ref<CalendarEvent[]>([]);
|
||
const calendarView = ref<CalendarView>("year");
|
||
const calendarCursor = ref(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
|
||
const selectedDateKey = ref(dayKey(new Date()));
|
||
const focusedCalendarEventId = ref("");
|
||
|
||
const lifecycleNowMs = ref(Date.now());
|
||
let lifecycleClock: ReturnType<typeof setInterval> | null = null;
|
||
|
||
onMounted(() => {
|
||
lifecycleClock = setInterval(() => {
|
||
lifecycleNowMs.value = Date.now();
|
||
}, 15000);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (lifecycleClock) {
|
||
clearInterval(lifecycleClock);
|
||
lifecycleClock = null;
|
||
}
|
||
if (calendarViewportResizeObserver) {
|
||
calendarViewportResizeObserver.disconnect();
|
||
calendarViewportResizeObserver = null;
|
||
}
|
||
clearCalendarZoomPrime();
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Apollo → Ref watcher
|
||
// ---------------------------------------------------------------------------
|
||
watch(() => calendarResult.value?.calendar, (v) => {
|
||
if (v) calendarEvents.value = v as CalendarEvent[];
|
||
}, { immediate: true });
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Sorted events & derived computeds
|
||
// ---------------------------------------------------------------------------
|
||
const sortedEvents = computed(() => [...calendarEvents.value].sort((a, b) => a.start.localeCompare(b.start)));
|
||
const focusedCalendarEvent = computed(() => {
|
||
const id = (focusedCalendarEventId.value ?? "").trim();
|
||
if (!id) return null;
|
||
return sortedEvents.value.find((event) => event.id === id) ?? null;
|
||
});
|
||
|
||
const eventsByDate = computed(() => {
|
||
const map = new Map<string, CalendarEvent[]>();
|
||
|
||
for (const event of sortedEvents.value) {
|
||
const key = event.start.slice(0, 10);
|
||
if (!map.has(key)) {
|
||
map.set(key, []);
|
||
}
|
||
map.get(key)?.push(event);
|
||
}
|
||
|
||
return map;
|
||
});
|
||
|
||
function getEventsByDate(key: string) {
|
||
return eventsByDate.value.get(key) ?? [];
|
||
}
|
||
|
||
const monthLabel = computed(() =>
|
||
new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }).format(calendarCursor.value),
|
||
);
|
||
|
||
const calendarViewOptions: { value: CalendarView; label: string }[] = [
|
||
{ value: "day", label: "Day" },
|
||
{ value: "week", label: "Week" },
|
||
{ value: "month", label: "Month" },
|
||
{ value: "year", label: "Year" },
|
||
{ value: "agenda", label: "Agenda" },
|
||
];
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Zoom / camera state
|
||
// ---------------------------------------------------------------------------
|
||
const calendarContentWrapRef = ref<HTMLElement | null>(null);
|
||
const calendarContentScrollRef = ref<HTMLElement | null>(null);
|
||
const calendarSceneRef = ref<HTMLElement | null>(null);
|
||
const calendarViewportHeight = ref(0);
|
||
const calendarHoveredMonthIndex = ref<number | null>(null);
|
||
const calendarHoveredWeekStartKey = ref("");
|
||
const calendarHoveredDayKey = ref("");
|
||
let calendarViewportResizeObserver: ResizeObserver | null = null;
|
||
|
||
function setCalendarContentWrapRef(element: HTMLElement | null) {
|
||
calendarContentWrapRef.value = element;
|
||
}
|
||
|
||
function setCalendarContentScrollRef(element: HTMLElement | null) {
|
||
if (calendarViewportResizeObserver) {
|
||
calendarViewportResizeObserver.disconnect();
|
||
calendarViewportResizeObserver = null;
|
||
}
|
||
calendarContentScrollRef.value = element;
|
||
if (element && typeof ResizeObserver !== "undefined") {
|
||
calendarViewportResizeObserver = new ResizeObserver(() => {
|
||
calendarViewportHeight.value = Math.max(0, Math.round(element.clientHeight));
|
||
});
|
||
calendarViewportResizeObserver.observe(element);
|
||
calendarViewportHeight.value = Math.max(0, Math.round(element.clientHeight));
|
||
}
|
||
}
|
||
|
||
function setCalendarSceneRef(element: HTMLElement | null) {
|
||
calendarSceneRef.value = element;
|
||
}
|
||
|
||
function setCalendarHoveredMonthIndex(value: number | null) {
|
||
calendarHoveredMonthIndex.value = value;
|
||
}
|
||
|
||
function setCalendarHoveredWeekStartKey(value: string) {
|
||
calendarHoveredWeekStartKey.value = value;
|
||
}
|
||
|
||
function setCalendarHoveredDayKey(value: string) {
|
||
calendarHoveredDayKey.value = value;
|
||
}
|
||
|
||
function onCalendarSceneMouseLeave() {
|
||
calendarHoveredMonthIndex.value = null;
|
||
calendarHoveredWeekStartKey.value = "";
|
||
calendarHoveredDayKey.value = "";
|
||
clearCalendarZoomPrime();
|
||
}
|
||
|
||
const calendarZoomBusy = ref(false);
|
||
const calendarCameraState = ref({
|
||
active: false,
|
||
left: 0,
|
||
top: 0,
|
||
scale: 1,
|
||
durationMs: 0,
|
||
});
|
||
const calendarZoomPrimeToken = ref("");
|
||
const calendarZoomPrimeScale = ref(1);
|
||
const calendarZoomPrimeTicks = ref(0);
|
||
let calendarWheelLockUntil = 0;
|
||
let calendarZoomPrimeTimer: ReturnType<typeof setTimeout> | null = null;
|
||
let calendarZoomPrimeLastAt = 0;
|
||
const CALENDAR_ZOOM_DURATION_MS = 2400;
|
||
const CALENDAR_ZOOM_PRIME_STEPS = 2;
|
||
const CALENDAR_ZOOM_PRIME_MAX_SCALE = 1.05;
|
||
const CALENDAR_ZOOM_PRIME_RESET_MS = 900;
|
||
const calendarZoomOrder: CalendarHierarchyView[] = ["year", "month", "week", "day"];
|
||
|
||
const normalizedCalendarView = computed<CalendarHierarchyView>(() =>
|
||
calendarView.value === "agenda" ? "month" : calendarView.value,
|
||
);
|
||
const calendarZoomLevelIndex = computed(() => Math.max(0, calendarZoomOrder.indexOf(normalizedCalendarView.value)));
|
||
const calendarSceneTransformStyle = computed(() => {
|
||
if (!calendarCameraState.value.active) return undefined;
|
||
return {
|
||
transform: `translate(${calendarCameraState.value.left}px, ${calendarCameraState.value.top}px) scale(${calendarCameraState.value.scale})`,
|
||
transformOrigin: "0 0",
|
||
transition:
|
||
calendarCameraState.value.durationMs > 0
|
||
? `transform ${calendarCameraState.value.durationMs}ms cubic-bezier(0.16, 0.86, 0.18, 1)`
|
||
: "none",
|
||
willChange: "transform",
|
||
};
|
||
});
|
||
|
||
function clearCalendarZoomPrime() {
|
||
if (calendarZoomPrimeTimer) {
|
||
clearTimeout(calendarZoomPrimeTimer);
|
||
calendarZoomPrimeTimer = null;
|
||
}
|
||
calendarZoomPrimeToken.value = "";
|
||
calendarZoomPrimeScale.value = 1;
|
||
calendarZoomPrimeTicks.value = 0;
|
||
calendarZoomPrimeLastAt = 0;
|
||
}
|
||
|
||
function calendarPrimeMonthToken(monthIndex: number) {
|
||
return `year-month-${monthIndex}`;
|
||
}
|
||
|
||
function calendarPrimeWeekToken(startKey: string) {
|
||
return `month-week-${startKey}`;
|
||
}
|
||
|
||
function calendarPrimeDayToken(key: string) {
|
||
return `week-day-${key}`;
|
||
}
|
||
|
||
function calendarPrimeStyle(token: string) {
|
||
if (calendarZoomPrimeToken.value !== token) return undefined;
|
||
return {
|
||
transform: `scale(${calendarZoomPrimeScale.value})`,
|
||
};
|
||
}
|
||
|
||
function maybePrimeWheelZoom(event: WheelEvent | undefined, token: string) {
|
||
if (!event || event.deltaY >= 0) return false;
|
||
const now = Date.now();
|
||
if (calendarZoomPrimeToken.value !== token || now - calendarZoomPrimeLastAt > CALENDAR_ZOOM_PRIME_RESET_MS) {
|
||
calendarZoomPrimeTicks.value = 0;
|
||
}
|
||
|
||
calendarZoomPrimeToken.value = token;
|
||
calendarZoomPrimeTicks.value += 1;
|
||
calendarZoomPrimeLastAt = now;
|
||
|
||
if (calendarZoomPrimeTicks.value <= CALENDAR_ZOOM_PRIME_STEPS) {
|
||
const ratio = calendarZoomPrimeTicks.value / CALENDAR_ZOOM_PRIME_STEPS;
|
||
calendarZoomPrimeScale.value = 1 + (CALENDAR_ZOOM_PRIME_MAX_SCALE - 1) * ratio;
|
||
if (calendarZoomPrimeTimer) clearTimeout(calendarZoomPrimeTimer);
|
||
calendarZoomPrimeTimer = setTimeout(() => {
|
||
clearCalendarZoomPrime();
|
||
}, CALENDAR_ZOOM_PRIME_RESET_MS);
|
||
return true;
|
||
}
|
||
|
||
clearCalendarZoomPrime();
|
||
return false;
|
||
}
|
||
|
||
function queryCalendarElement(selector: string) {
|
||
return calendarContentWrapRef.value?.querySelector<HTMLElement>(selector) ?? null;
|
||
}
|
||
|
||
function getCalendarViewportRect(): CalendarRect | null {
|
||
const wrapRect = calendarContentWrapRef.value?.getBoundingClientRect();
|
||
if (!wrapRect) return null;
|
||
return {
|
||
left: 0,
|
||
top: 0,
|
||
width: Math.max(24, wrapRect.width),
|
||
height: Math.max(24, wrapRect.height),
|
||
};
|
||
}
|
||
|
||
function getCalendarCameraViewportRect() {
|
||
const viewport = calendarContentScrollRef.value?.getBoundingClientRect();
|
||
if (!viewport) return null;
|
||
return {
|
||
width: Math.max(24, viewport.width),
|
||
height: Math.max(24, viewport.height),
|
||
};
|
||
}
|
||
|
||
function getElementRectInCalendar(element: HTMLElement | null): CalendarRect | null {
|
||
if (!element) return null;
|
||
const wrapRect = calendarContentWrapRef.value?.getBoundingClientRect();
|
||
if (!wrapRect) return null;
|
||
const rect = element.getBoundingClientRect();
|
||
const left = Math.max(0, Math.min(rect.left - wrapRect.left, wrapRect.width));
|
||
const top = Math.max(0, Math.min(rect.top - wrapRect.top, wrapRect.height));
|
||
const right = Math.max(0, Math.min(rect.right - wrapRect.left, wrapRect.width));
|
||
const bottom = Math.max(0, Math.min(rect.bottom - wrapRect.top, wrapRect.height));
|
||
const visibleWidth = right - left;
|
||
const visibleHeight = bottom - top;
|
||
if (visibleWidth < 2 || visibleHeight < 2) return null;
|
||
const width = Math.min(Math.max(24, visibleWidth), wrapRect.width - left);
|
||
const height = Math.min(Math.max(24, visibleHeight), wrapRect.height - top);
|
||
return { left, top, width, height };
|
||
}
|
||
|
||
function getElementRectInScene(element: HTMLElement | null): CalendarRect | null {
|
||
if (!element) return null;
|
||
const sceneRect = calendarSceneRef.value?.getBoundingClientRect();
|
||
if (!sceneRect) return null;
|
||
const rect = element.getBoundingClientRect();
|
||
const left = rect.left - sceneRect.left;
|
||
const top = rect.top - sceneRect.top;
|
||
const width = Math.max(24, rect.width);
|
||
const height = Math.max(24, rect.height);
|
||
return { left, top, width, height };
|
||
}
|
||
|
||
function fallbackZoomOriginRectInScene(): CalendarRect | null {
|
||
const viewport = getCalendarCameraViewportRect();
|
||
const scroll = calendarContentScrollRef.value;
|
||
if (!viewport || !scroll) return null;
|
||
const width = Math.max(96, Math.round(viewport.width * 0.28));
|
||
const height = Math.max(64, Math.round(viewport.height * 0.24));
|
||
return {
|
||
left: scroll.scrollLeft + Math.max(0, (viewport.width - width) / 2),
|
||
top: scroll.scrollTop + Math.max(0, (viewport.height - height) / 2),
|
||
width,
|
||
height,
|
||
};
|
||
}
|
||
|
||
function weekRowStartForDate(key: string) {
|
||
const date = new Date(`${key}T00:00:00`);
|
||
date.setDate(date.getDate() - date.getDay());
|
||
return dayKey(date);
|
||
}
|
||
|
||
function nextAnimationFrame() {
|
||
return new Promise<void>((resolve) => {
|
||
requestAnimationFrame(() => resolve());
|
||
});
|
||
}
|
||
|
||
function waitForTransformTransition(element: HTMLElement) {
|
||
return new Promise<void>((resolve) => {
|
||
let settled = false;
|
||
const finish = () => {
|
||
if (settled) return;
|
||
settled = true;
|
||
element.removeEventListener("transitionend", onTransitionEnd);
|
||
clearTimeout(fallbackTimer);
|
||
resolve();
|
||
};
|
||
const onTransitionEnd = (event: TransitionEvent) => {
|
||
if (event.target !== element) return;
|
||
if (event.propertyName !== "transform") return;
|
||
finish();
|
||
};
|
||
const fallbackTimer = setTimeout(() => finish(), CALENDAR_ZOOM_DURATION_MS + 160);
|
||
element.addEventListener("transitionend", onTransitionEnd);
|
||
});
|
||
}
|
||
|
||
function fadeOutCalendarSiblings(sourceElement: HTMLElement) {
|
||
const scene = calendarSceneRef.value;
|
||
if (!scene) return () => {};
|
||
const targets = Array.from(scene.querySelectorAll<HTMLElement>(".calendar-hover-targetable"));
|
||
const siblings = targets.filter((element) => {
|
||
if (element === sourceElement) return false;
|
||
if (sourceElement.contains(element)) return false;
|
||
if (element.contains(sourceElement)) return false;
|
||
return true;
|
||
});
|
||
const snapshots = siblings.map((element) => ({
|
||
element,
|
||
opacity: element.style.opacity,
|
||
pointerEvents: element.style.pointerEvents,
|
||
transition: element.style.transition,
|
||
}));
|
||
for (const { element } of snapshots) {
|
||
element.style.transition = "opacity 180ms ease";
|
||
element.style.opacity = "0";
|
||
element.style.pointerEvents = "none";
|
||
}
|
||
return () => {
|
||
for (const snapshot of snapshots) {
|
||
snapshot.element.style.opacity = snapshot.opacity;
|
||
snapshot.element.style.pointerEvents = snapshot.pointerEvents;
|
||
snapshot.element.style.transition = snapshot.transition;
|
||
}
|
||
};
|
||
}
|
||
|
||
function isRenderableRect(rect: DOMRect | null) {
|
||
return Boolean(rect && rect.width >= 2 && rect.height >= 2);
|
||
}
|
||
|
||
async function animateCalendarFlipTransition(
|
||
sourceElement: HTMLElement | null,
|
||
apply: () => void,
|
||
resolveTarget: () => HTMLElement | null,
|
||
) {
|
||
clearCalendarZoomPrime();
|
||
calendarZoomBusy.value = true;
|
||
let restoreSiblings = () => {};
|
||
let animatedElement: HTMLElement | null = null;
|
||
let snapshot: {
|
||
transform: string;
|
||
transition: string;
|
||
transformOrigin: string;
|
||
willChange: string;
|
||
zIndex: string;
|
||
} | null = null;
|
||
try {
|
||
const sourceRect = sourceElement?.getBoundingClientRect() ?? null;
|
||
apply();
|
||
await nextTick();
|
||
const targetElement = resolveTarget();
|
||
const targetRect = targetElement?.getBoundingClientRect() ?? null;
|
||
if (!targetElement || !isRenderableRect(sourceRect) || !isRenderableRect(targetRect)) return;
|
||
|
||
restoreSiblings = fadeOutCalendarSiblings(targetElement);
|
||
animatedElement = targetElement;
|
||
snapshot = {
|
||
transform: targetElement.style.transform,
|
||
transition: targetElement.style.transition,
|
||
transformOrigin: targetElement.style.transformOrigin,
|
||
willChange: targetElement.style.willChange,
|
||
zIndex: targetElement.style.zIndex,
|
||
};
|
||
|
||
const dx = sourceRect!.left - targetRect!.left;
|
||
const dy = sourceRect!.top - targetRect!.top;
|
||
const sx = Math.max(0.01, sourceRect!.width / targetRect!.width);
|
||
const sy = Math.max(0.01, sourceRect!.height / targetRect!.height);
|
||
|
||
targetElement.style.transformOrigin = "top left";
|
||
targetElement.style.willChange = "transform";
|
||
targetElement.style.zIndex = "24";
|
||
targetElement.style.transition = "none";
|
||
targetElement.style.transform = `translate3d(${dx}px, ${dy}px, 0px) scale(${sx}, ${sy})`;
|
||
targetElement.getBoundingClientRect();
|
||
await nextAnimationFrame();
|
||
|
||
targetElement.style.transition = `transform ${CALENDAR_ZOOM_DURATION_MS}ms cubic-bezier(0.16, 0.86, 0.18, 1)`;
|
||
targetElement.style.transform = "translate3d(0px, 0px, 0px) scale(1, 1)";
|
||
await waitForTransformTransition(targetElement);
|
||
} finally {
|
||
if (animatedElement && snapshot) {
|
||
animatedElement.style.transform = snapshot.transform;
|
||
animatedElement.style.transition = snapshot.transition;
|
||
animatedElement.style.transformOrigin = snapshot.transformOrigin;
|
||
animatedElement.style.willChange = snapshot.willChange;
|
||
animatedElement.style.zIndex = snapshot.zIndex;
|
||
}
|
||
restoreSiblings();
|
||
calendarZoomBusy.value = false;
|
||
}
|
||
}
|
||
|
||
async function animateCalendarZoomIntoSource(
|
||
sourceElement: HTMLElement | null,
|
||
apply: () => void,
|
||
) {
|
||
clearCalendarZoomPrime();
|
||
calendarZoomBusy.value = true;
|
||
let restoreSiblings = () => {};
|
||
let snapshot: {
|
||
transform: string;
|
||
transition: string;
|
||
transformOrigin: string;
|
||
willChange: string;
|
||
zIndex: string;
|
||
} | null = null;
|
||
try {
|
||
const viewportRect = calendarContentScrollRef.value?.getBoundingClientRect() ?? null;
|
||
const sourceRect = sourceElement?.getBoundingClientRect() ?? null;
|
||
if (!sourceElement || !isRenderableRect(viewportRect) || !isRenderableRect(sourceRect)) {
|
||
apply();
|
||
return;
|
||
}
|
||
|
||
restoreSiblings = fadeOutCalendarSiblings(sourceElement);
|
||
snapshot = {
|
||
transform: sourceElement.style.transform,
|
||
transition: sourceElement.style.transition,
|
||
transformOrigin: sourceElement.style.transformOrigin,
|
||
willChange: sourceElement.style.willChange,
|
||
zIndex: sourceElement.style.zIndex,
|
||
};
|
||
|
||
const dx = viewportRect!.left - sourceRect!.left;
|
||
const dy = viewportRect!.top - sourceRect!.top;
|
||
const sx = Math.max(0.01, viewportRect!.width / sourceRect!.width);
|
||
const sy = Math.max(0.01, viewportRect!.height / sourceRect!.height);
|
||
|
||
sourceElement.style.transformOrigin = "top left";
|
||
sourceElement.style.willChange = "transform";
|
||
sourceElement.style.zIndex = "24";
|
||
sourceElement.style.transition = "none";
|
||
sourceElement.style.transform = "translate3d(0px, 0px, 0px) scale(1, 1)";
|
||
sourceElement.getBoundingClientRect();
|
||
await nextAnimationFrame();
|
||
|
||
sourceElement.style.transition = `transform ${CALENDAR_ZOOM_DURATION_MS}ms cubic-bezier(0.16, 0.86, 0.18, 1)`;
|
||
sourceElement.style.transform = `translate3d(${dx}px, ${dy}px, 0px) scale(${sx}, ${sy})`;
|
||
await waitForTransformTransition(sourceElement);
|
||
|
||
apply();
|
||
await nextTick();
|
||
await nextAnimationFrame();
|
||
} finally {
|
||
if (sourceElement && snapshot) {
|
||
sourceElement.style.transform = snapshot.transform;
|
||
sourceElement.style.transition = snapshot.transition;
|
||
sourceElement.style.transformOrigin = snapshot.transformOrigin;
|
||
sourceElement.style.willChange = snapshot.willChange;
|
||
sourceElement.style.zIndex = snapshot.zIndex;
|
||
}
|
||
restoreSiblings();
|
||
calendarZoomBusy.value = false;
|
||
}
|
||
}
|
||
|
||
function resolveMonthAnchor(event?: WheelEvent) {
|
||
const target = event?.target as HTMLElement | null;
|
||
const monthAttr = target?.closest<HTMLElement>("[data-calendar-month-index]")?.dataset.calendarMonthIndex;
|
||
if (monthAttr) {
|
||
const parsed = Number(monthAttr);
|
||
if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 11) return parsed;
|
||
}
|
||
if (calendarHoveredMonthIndex.value !== null) return calendarHoveredMonthIndex.value;
|
||
return calendarCursor.value.getMonth();
|
||
}
|
||
|
||
function fallbackMonthGridAnchorKey() {
|
||
if (monthCells.value.some((cell) => cell.key === selectedDateKey.value)) return selectedDateKey.value;
|
||
const middle = dayKey(new Date(calendarCursor.value.getFullYear(), calendarCursor.value.getMonth(), 15));
|
||
if (monthCells.value.some((cell) => cell.key === middle)) return middle;
|
||
return monthCells.value.find((cell) => cell.inMonth)?.key ?? monthCells.value[0]?.key ?? selectedDateKey.value;
|
||
}
|
||
|
||
function resolveWeekAnchor(event?: WheelEvent) {
|
||
const target = event?.target as HTMLElement | null;
|
||
const weekKey = target?.closest<HTMLElement>("[data-calendar-week-start-key]")?.dataset.calendarWeekStartKey;
|
||
if (weekKey) return weekKey;
|
||
if (calendarHoveredWeekStartKey.value) return calendarHoveredWeekStartKey.value;
|
||
if (calendarHoveredDayKey.value) return calendarHoveredDayKey.value;
|
||
return fallbackMonthGridAnchorKey();
|
||
}
|
||
|
||
function resolveDayAnchor(event?: WheelEvent) {
|
||
const target = event?.target as HTMLElement | null;
|
||
const dayKeyAttr = target?.closest<HTMLElement>("[data-calendar-day-key]")?.dataset.calendarDayKey;
|
||
if (dayKeyAttr) return dayKeyAttr;
|
||
if (calendarHoveredDayKey.value) return calendarHoveredDayKey.value;
|
||
return weekDays.value[0]?.key ?? selectedDateKey.value;
|
||
}
|
||
|
||
async function zoomInCalendar(event?: Event) {
|
||
const wheelEvent = event instanceof WheelEvent ? event : undefined;
|
||
if (calendarView.value === "year") {
|
||
const monthIndex = resolveMonthAnchor(wheelEvent);
|
||
const sourceElement =
|
||
queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`) ??
|
||
queryCalendarElement("[data-calendar-month-index]");
|
||
if (maybePrimeWheelZoom(wheelEvent, calendarPrimeMonthToken(monthIndex))) return;
|
||
await animateCalendarZoomIntoSource(sourceElement, () => {
|
||
openYearMonth(monthIndex);
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (calendarView.value === "month" || calendarView.value === "agenda") {
|
||
const anchorDayKey = resolveWeekAnchor(wheelEvent);
|
||
const rowStartKey = weekRowStartForDate(anchorDayKey);
|
||
const sourceElement =
|
||
queryCalendarElement(`[data-calendar-layer="month"] [data-calendar-week-start-key="${rowStartKey}"]`) ??
|
||
queryCalendarElement(`[data-calendar-layer="month"] [data-calendar-day-key="${anchorDayKey}"]`) ??
|
||
queryCalendarElement(`[data-calendar-layer="month"] [data-calendar-week-start-key]`) ??
|
||
queryCalendarElement(`[data-calendar-layer="month"] [data-calendar-day-key]`);
|
||
if (maybePrimeWheelZoom(wheelEvent, calendarPrimeWeekToken(rowStartKey))) return;
|
||
await animateCalendarZoomIntoSource(sourceElement, () => {
|
||
openWeekView(anchorDayKey);
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (calendarView.value === "week") {
|
||
const dayAnchor = resolveDayAnchor(wheelEvent);
|
||
const sourceElement =
|
||
queryCalendarElement(`[data-calendar-layer="week"] [data-calendar-day-key="${dayAnchor}"]`) ??
|
||
queryCalendarElement(`[data-calendar-layer="week"] [data-calendar-day-key]`);
|
||
if (maybePrimeWheelZoom(wheelEvent, calendarPrimeDayToken(dayAnchor))) return;
|
||
await animateCalendarZoomIntoSource(sourceElement, () => {
|
||
openDayView(dayAnchor);
|
||
});
|
||
}
|
||
}
|
||
|
||
async function zoomToMonth(monthIndex: number) {
|
||
await animateCalendarZoomIntoSource(queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`), () => {
|
||
openYearMonth(monthIndex);
|
||
});
|
||
}
|
||
|
||
async function zoomOutCalendar() {
|
||
focusedCalendarEventId.value = "";
|
||
clearCalendarZoomPrime();
|
||
|
||
if (calendarView.value === "day") {
|
||
const targetDayKey = selectedDateKey.value;
|
||
await animateCalendarFlipTransition(
|
||
queryCalendarElement(`[data-calendar-month-index="${calendarCursor.value.getMonth()}"]`),
|
||
() => {
|
||
calendarView.value = "week";
|
||
},
|
||
() =>
|
||
queryCalendarElement(`[data-calendar-layer="week"] [data-calendar-day-key="${targetDayKey}"]`) ??
|
||
queryCalendarElement(`[data-calendar-layer="week"] [data-calendar-day-key]`),
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (calendarView.value === "week") {
|
||
const targetRowKey = weekRowStartForDate(selectedDateKey.value);
|
||
await animateCalendarFlipTransition(
|
||
queryCalendarElement(`[data-calendar-month-index="${calendarCursor.value.getMonth()}"]`),
|
||
() => {
|
||
calendarView.value = "month";
|
||
},
|
||
() =>
|
||
queryCalendarElement(`[data-calendar-layer="month"] [data-calendar-week-start-key="${targetRowKey}"]`) ??
|
||
queryCalendarElement(`[data-calendar-layer="month"] [data-calendar-day-key="${selectedDateKey.value}"]`) ??
|
||
queryCalendarElement(`[data-calendar-layer="month"] [data-calendar-week-start-key]`) ??
|
||
queryCalendarElement(`[data-calendar-layer="month"] [data-calendar-day-key]`),
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (calendarView.value === "month" || calendarView.value === "agenda") {
|
||
const targetMonthIndex = calendarCursor.value.getMonth();
|
||
await animateCalendarFlipTransition(
|
||
queryCalendarElement(`[data-calendar-month-index="${targetMonthIndex}"]`),
|
||
() => {
|
||
calendarView.value = "year";
|
||
},
|
||
() => queryCalendarElement(`[data-calendar-month-index="${targetMonthIndex}"]`),
|
||
);
|
||
}
|
||
}
|
||
|
||
function onCalendarHierarchyWheel(event: WheelEvent) {
|
||
const now = Date.now();
|
||
if (calendarZoomBusy.value) return;
|
||
if (now < calendarWheelLockUntil) return;
|
||
if (Math.abs(event.deltaY) < 5) return;
|
||
calendarWheelLockUntil = now + 140;
|
||
|
||
if (event.deltaY < 0) {
|
||
void zoomInCalendar(event);
|
||
return;
|
||
}
|
||
|
||
void zoomOutCalendar();
|
||
}
|
||
|
||
async function setCalendarZoomLevel(targetView: CalendarHierarchyView) {
|
||
let currentIndex = calendarZoomOrder.indexOf(normalizedCalendarView.value);
|
||
const targetIndex = calendarZoomOrder.indexOf(targetView);
|
||
if (currentIndex < 0 || targetIndex < 0 || currentIndex === targetIndex) return;
|
||
|
||
while (currentIndex !== targetIndex) {
|
||
if (targetIndex > currentIndex) {
|
||
await zoomInCalendar();
|
||
} else {
|
||
await zoomOutCalendar();
|
||
}
|
||
currentIndex = calendarZoomOrder.indexOf(normalizedCalendarView.value);
|
||
}
|
||
}
|
||
|
||
function onCalendarZoomSliderInput(event: Event) {
|
||
const value = Number((event.target as HTMLInputElement | null)?.value ?? NaN);
|
||
if (!Number.isFinite(value)) return;
|
||
const targetIndex = Math.max(0, Math.min(3, Math.round(value)));
|
||
const targetView = calendarZoomOrder[targetIndex];
|
||
if (!targetView) return;
|
||
void setCalendarZoomLevel(targetView);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Month cells, rows, week days
|
||
// ---------------------------------------------------------------------------
|
||
const monthCells = computed(() => {
|
||
const year = calendarCursor.value.getFullYear();
|
||
const month = calendarCursor.value.getMonth();
|
||
const first = new Date(year, month, 1);
|
||
const start = new Date(year, month, 1 - first.getDay());
|
||
|
||
return Array.from({ length: 42 }, (_, index) => {
|
||
const d = new Date(start);
|
||
d.setDate(start.getDate() + index);
|
||
const key = dayKey(d);
|
||
|
||
return {
|
||
key,
|
||
day: d.getDate(),
|
||
inMonth: d.getMonth() === month,
|
||
events: getEventsByDate(key),
|
||
};
|
||
});
|
||
});
|
||
|
||
const monthRows = computed(() => {
|
||
const rows: Array<{ key: string; startKey: string; cells: typeof monthCells.value }> = [];
|
||
for (let index = 0; index < monthCells.value.length; index += 7) {
|
||
const cells = monthCells.value.slice(index, index + 7);
|
||
if (!cells.length) continue;
|
||
rows.push({
|
||
key: `${cells[0]?.key ?? index}-week-row`,
|
||
startKey: cells[0]?.key ?? selectedDateKey.value,
|
||
cells,
|
||
});
|
||
}
|
||
return rows;
|
||
});
|
||
|
||
function monthCellHasFocusedEvent(events: CalendarEvent[]) {
|
||
const id = focusedCalendarEventId.value.trim();
|
||
if (!id) return false;
|
||
return events.some((event) => event.id === id);
|
||
}
|
||
|
||
function monthCellEvents(events: CalendarEvent[]) {
|
||
const id = focusedCalendarEventId.value.trim();
|
||
if (!id) return events.slice(0, 2);
|
||
const focused = events.find((event) => event.id === id);
|
||
if (!focused) return events.slice(0, 2);
|
||
const rest = events.filter((event) => event.id !== id).slice(0, 1);
|
||
return [focused, ...rest];
|
||
}
|
||
|
||
const weekDays = computed(() => {
|
||
const base = new Date(`${selectedDateKey.value}T00:00:00`);
|
||
const mondayOffset = (base.getDay() + 6) % 7;
|
||
const monday = new Date(base);
|
||
monday.setDate(base.getDate() - mondayOffset);
|
||
|
||
return Array.from({ length: 7 }, (_, index) => {
|
||
const d = new Date(monday);
|
||
d.setDate(monday.getDate() + index);
|
||
const key = dayKey(d);
|
||
|
||
return {
|
||
key,
|
||
label: new Intl.DateTimeFormat("en-US", { weekday: "short" }).format(d),
|
||
day: d.getDate(),
|
||
events: getEventsByDate(key),
|
||
};
|
||
});
|
||
});
|
||
|
||
const calendarPeriodLabel = computed(() => {
|
||
if (calendarView.value === "month") {
|
||
return monthLabel.value;
|
||
}
|
||
|
||
if (calendarView.value === "year") {
|
||
return String(calendarCursor.value.getFullYear());
|
||
}
|
||
|
||
if (calendarView.value === "week") {
|
||
const first = weekDays.value[0];
|
||
const last = weekDays.value[weekDays.value.length - 1];
|
||
if (!first || !last) return "";
|
||
return `${formatDay(`${first.key}T00:00:00`)} - ${formatDay(`${last.key}T00:00:00`)}`;
|
||
}
|
||
|
||
if (calendarView.value === "day") {
|
||
return formatDay(`${selectedDateKey.value}T00:00:00`);
|
||
}
|
||
|
||
return `Agenda · ${monthLabel.value}`;
|
||
});
|
||
|
||
const yearMonths = computed(() => {
|
||
const year = calendarCursor.value.getFullYear();
|
||
return Array.from({ length: 12 }, (_, monthIndex) => {
|
||
const monthStart = new Date(year, monthIndex, 1);
|
||
const monthEnd = new Date(year, monthIndex + 1, 1);
|
||
const items = sortedEvents.value.filter((event) => {
|
||
const d = new Date(event.start);
|
||
return d >= monthStart && d < monthEnd;
|
||
});
|
||
|
||
return {
|
||
monthIndex,
|
||
label: new Intl.DateTimeFormat("en-US", { month: "long" }).format(monthStart),
|
||
count: items.length,
|
||
first: items[0],
|
||
};
|
||
});
|
||
});
|
||
|
||
const selectedDayEvents = computed(() => getEventsByDate(selectedDateKey.value));
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Navigation helpers
|
||
// ---------------------------------------------------------------------------
|
||
function shiftCalendar(step: number) {
|
||
focusedCalendarEventId.value = "";
|
||
if (calendarView.value === "year") {
|
||
const next = new Date(calendarCursor.value);
|
||
next.setFullYear(next.getFullYear() + step);
|
||
calendarCursor.value = new Date(next.getFullYear(), next.getMonth(), 1);
|
||
const selected = new Date(`${selectedDateKey.value}T00:00:00`);
|
||
selected.setFullYear(selected.getFullYear() + step);
|
||
selectedDateKey.value = dayKey(selected);
|
||
return;
|
||
}
|
||
|
||
if (calendarView.value === "month" || calendarView.value === "agenda") {
|
||
const next = new Date(calendarCursor.value);
|
||
next.setMonth(next.getMonth() + step);
|
||
calendarCursor.value = new Date(next.getFullYear(), next.getMonth(), 1);
|
||
return;
|
||
}
|
||
|
||
const current = new Date(`${selectedDateKey.value}T00:00:00`);
|
||
const days = calendarView.value === "week" ? 7 : 1;
|
||
current.setDate(current.getDate() + days * step);
|
||
|
||
selectedDateKey.value = dayKey(current);
|
||
calendarCursor.value = new Date(current.getFullYear(), current.getMonth(), 1);
|
||
}
|
||
|
||
function setToday() {
|
||
focusedCalendarEventId.value = "";
|
||
const now = new Date();
|
||
selectedDateKey.value = dayKey(now);
|
||
calendarCursor.value = new Date(now.getFullYear(), now.getMonth(), 1);
|
||
}
|
||
|
||
function pickDate(key: string) {
|
||
focusedCalendarEventId.value = "";
|
||
selectedDateKey.value = key;
|
||
const d = new Date(`${key}T00:00:00`);
|
||
calendarCursor.value = new Date(d.getFullYear(), d.getMonth(), 1);
|
||
}
|
||
|
||
function openDayView(key: string) {
|
||
pickDate(key);
|
||
calendarView.value = "day";
|
||
}
|
||
|
||
function openWeekView(key: string) {
|
||
pickDate(key);
|
||
calendarView.value = "week";
|
||
}
|
||
|
||
function openYearMonth(monthIndex: number) {
|
||
focusedCalendarEventId.value = "";
|
||
const year = calendarCursor.value.getFullYear();
|
||
calendarCursor.value = new Date(year, monthIndex, 1);
|
||
selectedDateKey.value = dayKey(new Date(year, monthIndex, 1));
|
||
calendarView.value = "month";
|
||
}
|
||
|
||
function formatYearMonthFirst(item: { first?: CalendarEvent }) {
|
||
if (!item.first) return "";
|
||
return `${formatDay(item.first.start)} · ${item.first.title}`;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Event creation
|
||
// ---------------------------------------------------------------------------
|
||
const commEventForm = ref({
|
||
startDate: "",
|
||
startTime: "",
|
||
durationMinutes: 30,
|
||
});
|
||
const commEventMode = ref<"planned" | "logged">("planned");
|
||
const commEventSaving = ref(false);
|
||
const commEventError = ref("");
|
||
|
||
function setDefaultCommEventForm(mode: "planned" | "logged") {
|
||
const start = mode === "planned"
|
||
? roundToNextQuarter(new Date(Date.now() + 15 * 60 * 1000))
|
||
: roundToPrevQuarter(new Date(Date.now() - 30 * 60 * 1000));
|
||
commEventForm.value = {
|
||
startDate: toInputDate(start),
|
||
startTime: toInputTime(start),
|
||
durationMinutes: 30,
|
||
};
|
||
}
|
||
|
||
function buildCommEventTitle(text: string, mode: "planned" | "logged", 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 mode === "logged" ? `Отчёт по контакту ${contact}` : `Событие с ${contact}`;
|
||
}
|
||
|
||
function openCommEventModal(mode: "planned" | "logged", hasThread: boolean) {
|
||
if (!hasThread) return;
|
||
commEventMode.value = mode;
|
||
setDefaultCommEventForm(mode);
|
||
commEventError.value = "";
|
||
}
|
||
|
||
function closeCommEventModal() {
|
||
if (commEventSaving.value) return;
|
||
commEventError.value = "";
|
||
}
|
||
|
||
async function createCommEvent(contactName: string, draftText: string) {
|
||
if (!contactName || commEventSaving.value) return;
|
||
|
||
const note = draftText.trim();
|
||
const title = buildCommEventTitle(note, commEventMode.value, contactName);
|
||
const duration = Number(commEventForm.value.durationMinutes || 0);
|
||
if (!note) {
|
||
commEventError.value = "Текст события обязателен";
|
||
return;
|
||
}
|
||
|
||
if (!commEventForm.value.startDate || !commEventForm.value.startTime) {
|
||
commEventError.value = "Date and time are required";
|
||
return;
|
||
}
|
||
|
||
const start = new Date(`${commEventForm.value.startDate}T${commEventForm.value.startTime}:00`);
|
||
if (Number.isNaN(start.getTime())) {
|
||
commEventError.value = "Invalid date or time";
|
||
return;
|
||
}
|
||
|
||
const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 30;
|
||
const end = new Date(start);
|
||
end.setMinutes(end.getMinutes() + safeDuration);
|
||
|
||
commEventSaving.value = true;
|
||
commEventError.value = "";
|
||
try {
|
||
const res = await doCreateCalendarEvent({
|
||
input: {
|
||
title,
|
||
start: start.toISOString(),
|
||
end: end.toISOString(),
|
||
contact: contactName,
|
||
note,
|
||
archived: commEventMode.value === "logged",
|
||
archiveNote: commEventMode.value === "logged" ? note : undefined,
|
||
},
|
||
});
|
||
if (res?.data?.createCalendarEvent) {
|
||
calendarEvents.value = [res.data.createCalendarEvent as CalendarEvent, ...calendarEvents.value];
|
||
}
|
||
selectedDateKey.value = dayKey(start);
|
||
calendarCursor.value = new Date(start.getFullYear(), start.getMonth(), 1);
|
||
commEventError.value = "";
|
||
return true;
|
||
} catch (error: any) {
|
||
commEventError.value = String(error?.message ?? error ?? "Failed to create event");
|
||
return false;
|
||
} finally {
|
||
commEventSaving.value = false;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Event archival
|
||
// ---------------------------------------------------------------------------
|
||
const eventCloseOpen = ref<Record<string, boolean>>({});
|
||
const eventCloseDraft = ref<Record<string, string>>({});
|
||
const eventCloseSaving = ref<Record<string, boolean>>({});
|
||
const eventCloseError = ref<Record<string, string>>({});
|
||
|
||
function canManuallyCloseEvent(entry: { kind: string; event?: CalendarEvent; phase?: string }) {
|
||
if (entry.kind !== "eventLifecycle" || !entry.event) return false;
|
||
return !isEventFinalStatus(entry.event.isArchived);
|
||
}
|
||
|
||
function isEventCloseOpen(eventId: string) {
|
||
return Boolean(eventCloseOpen.value[eventId]);
|
||
}
|
||
|
||
function toggleEventClose(eventId: string) {
|
||
const next = !eventCloseOpen.value[eventId];
|
||
eventCloseOpen.value = { ...eventCloseOpen.value, [eventId]: next };
|
||
if (next && !eventCloseDraft.value[eventId]) {
|
||
eventCloseDraft.value = { ...eventCloseDraft.value, [eventId]: "" };
|
||
}
|
||
if (!next && eventCloseError.value[eventId]) {
|
||
eventCloseError.value = { ...eventCloseError.value, [eventId]: "" };
|
||
}
|
||
}
|
||
|
||
async function archiveEventManually(event: CalendarEvent) {
|
||
const eventId = event.id;
|
||
const archiveNote = String(eventCloseDraft.value[eventId] ?? "").trim();
|
||
if (eventCloseSaving.value[eventId]) return;
|
||
|
||
eventCloseSaving.value = { ...eventCloseSaving.value, [eventId]: true };
|
||
eventCloseError.value = { ...eventCloseError.value, [eventId]: "" };
|
||
try {
|
||
await doArchiveCalendarEvent({
|
||
input: {
|
||
id: eventId,
|
||
archiveNote: archiveNote || undefined,
|
||
},
|
||
});
|
||
eventCloseOpen.value = { ...eventCloseOpen.value, [eventId]: false };
|
||
eventCloseDraft.value = { ...eventCloseDraft.value, [eventId]: "" };
|
||
} catch (error: any) {
|
||
eventCloseError.value = { ...eventCloseError.value, [eventId]: String(error?.message ?? error ?? "Failed to archive event") };
|
||
} finally {
|
||
eventCloseSaving.value = { ...eventCloseSaving.value, [eventId]: false };
|
||
}
|
||
}
|
||
|
||
return {
|
||
// Core state
|
||
calendarEvents,
|
||
calendarView,
|
||
calendarCursor,
|
||
selectedDateKey,
|
||
focusedCalendarEventId,
|
||
lifecycleNowMs,
|
||
|
||
// Computeds
|
||
sortedEvents,
|
||
focusedCalendarEvent,
|
||
eventsByDate,
|
||
getEventsByDate,
|
||
monthLabel,
|
||
calendarViewOptions,
|
||
monthCells,
|
||
monthRows,
|
||
monthCellHasFocusedEvent,
|
||
monthCellEvents,
|
||
weekDays,
|
||
calendarPeriodLabel,
|
||
yearMonths,
|
||
selectedDayEvents,
|
||
|
||
// Zoom / camera
|
||
calendarContentWrapRef,
|
||
calendarContentScrollRef,
|
||
calendarSceneRef,
|
||
calendarViewportHeight,
|
||
calendarHoveredMonthIndex,
|
||
calendarHoveredWeekStartKey,
|
||
calendarHoveredDayKey,
|
||
calendarZoomBusy,
|
||
calendarCameraState,
|
||
calendarZoomPrimeToken,
|
||
calendarZoomPrimeScale,
|
||
calendarZoomPrimeTicks,
|
||
normalizedCalendarView,
|
||
calendarZoomLevelIndex,
|
||
calendarSceneTransformStyle,
|
||
calendarZoomOrder,
|
||
|
||
// Zoom / camera setters
|
||
setCalendarContentWrapRef,
|
||
setCalendarContentScrollRef,
|
||
setCalendarSceneRef,
|
||
setCalendarHoveredMonthIndex,
|
||
setCalendarHoveredWeekStartKey,
|
||
setCalendarHoveredDayKey,
|
||
onCalendarSceneMouseLeave,
|
||
clearCalendarZoomPrime,
|
||
calendarPrimeMonthToken,
|
||
calendarPrimeWeekToken,
|
||
calendarPrimeDayToken,
|
||
calendarPrimeStyle,
|
||
maybePrimeWheelZoom,
|
||
queryCalendarElement,
|
||
getCalendarViewportRect,
|
||
getCalendarCameraViewportRect,
|
||
getElementRectInCalendar,
|
||
getElementRectInScene,
|
||
fallbackZoomOriginRectInScene,
|
||
weekRowStartForDate,
|
||
|
||
// Zoom animations
|
||
zoomInCalendar,
|
||
zoomToMonth,
|
||
zoomOutCalendar,
|
||
onCalendarHierarchyWheel,
|
||
setCalendarZoomLevel,
|
||
onCalendarZoomSliderInput,
|
||
|
||
// Navigation
|
||
shiftCalendar,
|
||
setToday,
|
||
pickDate,
|
||
openDayView,
|
||
openWeekView,
|
||
openYearMonth,
|
||
formatYearMonthFirst,
|
||
|
||
// Event creation
|
||
commEventForm,
|
||
commEventMode,
|
||
commEventSaving,
|
||
commEventError,
|
||
createCommEvent,
|
||
openCommEventModal,
|
||
closeCommEventModal,
|
||
setDefaultCommEventForm,
|
||
buildCommEventTitle,
|
||
|
||
// Event archival
|
||
eventCloseOpen,
|
||
eventCloseDraft,
|
||
eventCloseSaving,
|
||
eventCloseError,
|
||
canManuallyCloseEvent,
|
||
isEventCloseOpen,
|
||
toggleEventClose,
|
||
archiveEventManually,
|
||
|
||
// Refetch
|
||
refetchCalendar,
|
||
};
|
||
}
|