feat(calendar): replace CSS-transform zoom with GSAP flying-rect animation and scope data to year

- Add CalendarDateRange input to GraphQL schema; server resolver now accepts from/to params
- Frontend query sends year-scoped date range variables reactively
- Rewrite zoom-in/zoom-out animations using GSAP flying-rect overlay (650ms vs 2400ms)
- Add flying-rect element to CrmCalendarPanel with proper CSS
- Remove old calendarSceneTransformStyle CSS-transition approach
- Add calendarKillTweens cleanup in onBeforeUnmount

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-02-24 11:41:35 +07:00
parent 638652b4d8
commit 227030b9ae
6 changed files with 217 additions and 181 deletions

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import gsap from "gsap";
import { nextTick, onBeforeUnmount, onMounted } from "vue";
import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.vue";
import CrmCalendarPanel from "~~/app/components/workspace/calendar/CrmCalendarPanel.vue";
@@ -596,9 +597,14 @@ const { result: contactInboxesResult, refetch: refetchContactInboxes } = useQuer
{ enabled: apolloAuthReady },
);
const calendarQueryYear = ref(new Date().getFullYear());
const calendarQueryVars = computed(() => ({
from: new Date(calendarQueryYear.value, 0, 1).toISOString(),
to: new Date(calendarQueryYear.value + 1, 0, 1).toISOString(),
}));
const { result: calendarResult, refetch: refetchCalendar } = useQuery(
CalendarQueryDocument,
null,
calendarQueryVars,
{ enabled: apolloAuthReady },
);
@@ -2395,6 +2401,7 @@ onBeforeUnmount(() => {
calendarViewportResizeObserver = null;
}
clearCalendarZoomPrime();
calendarKillTweens();
});
const calendarView = ref<CalendarView>("year");
@@ -2444,7 +2451,10 @@ type CalendarRect = { left: number; top: number; width: number; height: number }
const calendarContentWrapRef = ref<HTMLElement | null>(null);
const calendarContentScrollRef = ref<HTMLElement | null>(null);
const calendarSceneRef = ref<HTMLElement | null>(null);
const calendarFlyRectRef = ref<HTMLDivElement | null>(null);
const calendarFlyVisible = ref(false);
const calendarViewportHeight = ref(0);
let calendarActiveTweens: gsap.core.Tween[] = [];
const calendarHoveredMonthIndex = ref<number | null>(null);
const calendarHoveredWeekStartKey = ref("");
const calendarHoveredDayKey = ref("");
@@ -2454,6 +2464,28 @@ function setCalendarContentWrapRef(element: HTMLElement | null) {
calendarContentWrapRef.value = element;
}
function setCalendarFlyRectRef(element: HTMLDivElement | null) {
calendarFlyRectRef.value = element;
}
function calendarTweenTo(target: gsap.TweenTarget, vars: gsap.TweenVars): Promise<void> {
return new Promise((resolve) => {
const t = gsap.to(target, {
...vars,
onComplete: () => {
calendarActiveTweens = calendarActiveTweens.filter((tw) => tw !== t);
resolve();
},
});
calendarActiveTweens.push(t);
});
}
function calendarKillTweens() {
for (const t of calendarActiveTweens) t.kill();
calendarActiveTweens = [];
}
function setCalendarContentScrollRef(element: HTMLElement | null) {
if (calendarViewportResizeObserver) {
calendarViewportResizeObserver.disconnect();
@@ -2492,20 +2524,16 @@ function onCalendarSceneMouseLeave() {
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_DURATION_MS = 650;
const CALENDAR_FLY_DURATION = 0.65;
const CALENDAR_FADE_DURATION = 0.18;
const CALENDAR_EASE = "power3.inOut";
const CALENDAR_ZOOM_PRIME_STEPS = 2;
const CALENDAR_ZOOM_PRIME_MAX_SCALE = 1.05;
const CALENDAR_ZOOM_PRIME_RESET_MS = 900;
@@ -2515,18 +2543,6 @@ 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) {
@@ -2662,119 +2678,78 @@ function nextAnimationFrame() {
});
}
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,
_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;
calendarKillTweens();
const flyEl = calendarFlyRectRef.value;
const wrapEl = calendarContentWrapRef.value;
const sceneEl = calendarSceneRef.value;
if (!flyEl || !wrapEl) {
apply();
calendarZoomBusy.value = false;
return;
}
try {
const sourceRect = sourceElement?.getBoundingClientRect() ?? null;
const wrapRect = wrapEl.getBoundingClientRect();
// 1. Fade out current content
if (sceneEl) {
await calendarTweenTo(sceneEl, { opacity: 0, duration: CALENDAR_FADE_DURATION, ease: "power2.in" });
}
// 2. Position fly rect at full viewport
const pad = 4;
gsap.set(flyEl, {
left: pad,
top: pad,
width: wrapRect.width - pad * 2,
height: wrapRect.height - pad * 2,
opacity: 1,
borderRadius: 14,
});
calendarFlyVisible.value = true;
// 3. Switch to parent view
apply();
await nextTick();
// 4. Find target element in new view
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,
};
if (targetElement && targetRect && targetRect.width >= 2 && targetRect.height >= 2) {
const tgtLeft = targetRect.left - wrapRect.left;
const tgtTop = targetRect.top - wrapRect.top;
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;
// 5. Animate fly rect → target element
await calendarTweenTo(flyEl, {
left: tgtLeft,
top: tgtTop,
width: targetRect.width,
height: targetRect.height,
borderRadius: 12,
duration: CALENDAR_FLY_DURATION,
ease: CALENDAR_EASE,
});
}
restoreSiblings();
// 6. Hide fly rect, fade in content
calendarFlyVisible.value = false;
if (sceneEl) {
gsap.set(sceneEl, { opacity: 0 });
await calendarTweenTo(sceneEl, { opacity: 1, duration: 0.25, ease: "power2.out" });
}
} finally {
calendarFlyVisible.value = false;
calendarZoomBusy.value = false;
}
}
@@ -2785,60 +2760,75 @@ async function animateCalendarZoomIntoSource(
) {
clearCalendarZoomPrime();
calendarZoomBusy.value = true;
let restoreSiblings = () => {};
let snapshot: {
transform: string;
transition: string;
transformOrigin: string;
willChange: string;
zIndex: string;
} | null = null;
calendarKillTweens();
const flyEl = calendarFlyRectRef.value;
const wrapEl = calendarContentWrapRef.value;
const scrollEl = calendarContentScrollRef.value;
const sceneEl = calendarSceneRef.value;
if (!sourceElement || !flyEl || !wrapEl || !scrollEl) {
apply();
calendarZoomBusy.value = false;
return;
}
try {
const viewportRect = calendarContentScrollRef.value?.getBoundingClientRect() ?? null;
const sourceRect = sourceElement?.getBoundingClientRect() ?? null;
if (!sourceElement || !isRenderableRect(viewportRect) || !isRenderableRect(sourceRect)) {
const wrapRect = wrapEl.getBoundingClientRect();
const sourceRect = sourceElement.getBoundingClientRect();
if (sourceRect.width < 2 || sourceRect.height < 2) {
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,
};
// 1. Fade out siblings
const siblings = Array.from(
sceneEl?.querySelectorAll<HTMLElement>(".calendar-hover-targetable") ?? [],
).filter((el) => el !== sourceElement && !sourceElement.contains(el) && !el.contains(sourceElement));
await calendarTweenTo(siblings, { opacity: 0, duration: CALENDAR_FADE_DURATION, ease: "power2.in" });
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);
// 2. Position fly rect at source element
const srcLeft = sourceRect.left - wrapRect.left;
const srcTop = sourceRect.top - wrapRect.top;
gsap.set(flyEl, {
left: srcLeft,
top: srcTop,
width: sourceRect.width,
height: sourceRect.height,
opacity: 1,
borderRadius: 12,
});
calendarFlyVisible.value = true;
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);
// 3. Animate fly rect → full viewport
const pad = 4;
await calendarTweenTo(flyEl, {
left: pad,
top: pad,
width: wrapRect.width - pad * 2,
height: wrapRect.height - pad * 2,
borderRadius: 14,
duration: CALENDAR_FLY_DURATION,
ease: CALENDAR_EASE,
});
// 4. Switch content
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;
// 5. Hide fly rect, fade in new content
calendarFlyVisible.value = false;
if (sceneEl) {
gsap.set(sceneEl, { opacity: 0 });
await calendarTweenTo(sceneEl, { opacity: 1, duration: 0.25, ease: "power2.out" });
}
restoreSiblings();
// 6. Restore sibling opacity
for (const el of siblings) {
el.style.opacity = "";
}
} finally {
calendarFlyVisible.value = false;
calendarZoomBusy.value = false;
}
}
@@ -4876,7 +4866,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
:set-calendar-scene-ref="setCalendarSceneRef"
:calendar-viewport-height="calendarViewportHeight"
:normalized-calendar-view="normalizedCalendarView"
:calendar-scene-transform-style="calendarSceneTransformStyle"
:calendar-fly-visible="calendarFlyVisible"
:set-calendar-fly-rect-ref="setCalendarFlyRectRef"
:on-calendar-scene-mouse-leave="onCalendarSceneMouseLeave"
:calendar-view="calendarView"
:year-months="yearMonths"

View File

@@ -54,7 +54,6 @@ defineProps<{
setCalendarSceneRef: (element: HTMLDivElement | null) => void;
calendarViewportHeight: number;
normalizedCalendarView: string;
calendarSceneTransformStyle: Record<string, string>;
onCalendarSceneMouseLeave: () => void;
calendarView: string;
yearMonths: YearMonthItem[];
@@ -80,6 +79,8 @@ defineProps<{
weekDays: WeekDay[];
calendarPrimeDayToken: (dayKey: string) => string;
selectedDayEvents: CalendarEvent[];
calendarFlyVisible: boolean;
setCalendarFlyRectRef: (element: HTMLDivElement | null) => void;
}>();
</script>
@@ -156,6 +157,14 @@ defineProps<{
>
<span></span>
</button>
<!-- GSAP flying rect (zoom transition overlay) -->
<div
v-show="calendarFlyVisible"
:ref="setCalendarFlyRectRef"
class="calendar-fly-rect"
/>
<div
:ref="setCalendarContentScrollRef"
class="calendar-content-scroll min-h-0 h-full overflow-y-auto pr-1"
@@ -167,7 +176,6 @@ defineProps<{
'calendar-scene',
normalizedCalendarView === 'day' ? 'cursor-zoom-out' : 'cursor-zoom-in',
]"
:style="calendarSceneTransformStyle"
@mouseleave="onCalendarSceneMouseLeave"
>
<div
@@ -441,6 +449,16 @@ defineProps<{
z-index: 2;
}
.calendar-fly-rect {
position: absolute;
border-radius: 12px;
border: 2px solid color-mix(in oklab, var(--color-primary) 70%, transparent);
background: color-mix(in oklab, var(--color-base-200) 60%, transparent);
z-index: 20;
pointer-events: none;
will-change: left, top, width, height;
}
.calendar-zoom-inline {
position: relative;
display: flex;

View File

@@ -23,6 +23,11 @@ export type ArchiveCalendarEventInput = {
id: Scalars['ID']['input'];
};
export type CalendarDateRange = {
from?: InputMaybe<Scalars['String']['input']>;
to?: InputMaybe<Scalars['String']['input']>;
};
export type CalendarEvent = {
__typename?: 'CalendarEvent';
archiveNote: Scalars['String']['output'];
@@ -387,6 +392,11 @@ export type Query = {
};
export type QuerycalendarArgs = {
dateRange?: InputMaybe<CalendarDateRange>;
};
export type QuerygetClientTimelineArgs = {
contactId: Scalars['ID']['input'];
limit?: InputMaybe<Scalars['Int']['input']>;
@@ -418,7 +428,10 @@ export type ArchiveChatConversationMutationMutationVariables = Exact<{
export type ArchiveChatConversationMutationMutation = { __typename?: 'Mutation', archiveChatConversation: { __typename?: 'MutationResult', ok: boolean } };
export type CalendarQueryQueryVariables = Exact<{ [key: string]: never; }>;
export type CalendarQueryQueryVariables = Exact<{
from?: InputMaybe<Scalars['String']['input']>;
to?: InputMaybe<Scalars['String']['input']>;
}>;
export type CalendarQueryQuery = { __typename?: 'Query', calendar: Array<{ __typename?: 'CalendarEvent', id: string, title: string, start: string, end: string, contact: string, note: string, isArchived: boolean, createdAt: string, archiveNote: string, archivedAt: string }> };
@@ -670,8 +683,8 @@ export function useArchiveChatConversationMutationMutation(options: VueApolloCom
}
export type ArchiveChatConversationMutationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<ArchiveChatConversationMutationMutation, ArchiveChatConversationMutationMutationVariables>;
export const CalendarQueryDocument = gql`
query CalendarQuery {
calendar {
query CalendarQuery($from: String, $to: String) {
calendar(dateRange: {from: $from, to: $to}) {
id
title
start
@@ -693,16 +706,20 @@ export const CalendarQueryDocument = gql`
* When your component renders, `useCalendarQueryQuery` returns an object from Apollo Client that contains result, loading and error properties
* you can use to render your UI.
*
* @param variables that will be passed into the query
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
*
* @example
* const { result, loading, error } = useCalendarQueryQuery();
* const { result, loading, error } = useCalendarQueryQuery({
* from: // value for 'from'
* to: // value for 'to'
* });
*/
export function useCalendarQueryQuery(options: VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> = {}) {
return VueApolloComposable.useQuery<CalendarQueryQuery, CalendarQueryQueryVariables>(CalendarQueryDocument, {}, options);
export function useCalendarQueryQuery(variables: CalendarQueryQueryVariables | VueCompositionApi.Ref<CalendarQueryQueryVariables> | ReactiveFunction<CalendarQueryQueryVariables> = {}, options: VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> = {}) {
return VueApolloComposable.useQuery<CalendarQueryQuery, CalendarQueryQueryVariables>(CalendarQueryDocument, variables, options);
}
export function useCalendarQueryLazyQuery(options: VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> = {}) {
return VueApolloComposable.useLazyQuery<CalendarQueryQuery, CalendarQueryQueryVariables>(CalendarQueryDocument, {}, options);
export function useCalendarQueryLazyQuery(variables: CalendarQueryQueryVariables | VueCompositionApi.Ref<CalendarQueryQueryVariables> | ReactiveFunction<CalendarQueryQueryVariables> = {}, options: VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> = {}) {
return VueApolloComposable.useLazyQuery<CalendarQueryQuery, CalendarQueryQueryVariables>(CalendarQueryDocument, variables, options);
}
export type CalendarQueryQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<CalendarQueryQuery, CalendarQueryQueryVariables>;
export const ChatConversationsQueryDocument = gql`

View File

@@ -1,5 +1,5 @@
query CalendarQuery {
calendar {
query CalendarQuery($from: String, $to: String) {
calendar(dateRange: { from: $from, to: $to }) {
id
title
start

View File

@@ -5,7 +5,7 @@ type Query {
contacts: [Contact!]!
communications: [CommItem!]!
contactInboxes: [ContactInbox!]!
calendar: [CalendarEvent!]!
calendar(dateRange: CalendarDateRange): [CalendarEvent!]!
deals: [Deal!]!
feed: [FeedCard!]!
pins: [CommPin!]!
@@ -49,6 +49,11 @@ type PinToggleResult {
pinned: Boolean!
}
input CalendarDateRange {
from: String
to: String
}
input CreateCalendarEventInput {
title: String!
start: String!

View File

@@ -675,10 +675,10 @@ async function getContactInboxes(auth: AuthContext | null) {
}));
}
async function getCalendar(auth: AuthContext | null) {
async function getCalendar(auth: AuthContext | null, dateRange?: { from?: string; to?: string }) {
const ctx = requireAuth(auth);
const from = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
const to = new Date(Date.now() + 1000 * 60 * 60 * 24 * 60);
const from = dateRange?.from ? new Date(dateRange.from) : new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
const to = dateRange?.to ? new Date(dateRange.to) : new Date(Date.now() + 1000 * 60 * 60 * 24 * 60);
const calendarRaw = await prisma.calendarEvent.findMany({
where: { teamId: ctx.teamId, startsAt: { gte: from, lte: to } },
@@ -1842,7 +1842,7 @@ export const crmGraphqlSchema = buildSchema(`
contacts: [Contact!]!
communications: [CommItem!]!
contactInboxes: [ContactInbox!]!
calendar: [CalendarEvent!]!
calendar(dateRange: CalendarDateRange): [CalendarEvent!]!
deals: [Deal!]!
feed: [FeedCard!]!
pins: [CommPin!]!
@@ -1886,6 +1886,11 @@ export const crmGraphqlSchema = buildSchema(`
pinned: Boolean!
}
input CalendarDateRange {
from: String
to: String
}
input CreateCalendarEventInput {
title: String!
start: String!
@@ -2113,7 +2118,7 @@ export const crmGraphqlRoot = {
contacts: async (_args: unknown, context: GraphQLContext) => getContacts(context.auth),
communications: async (_args: unknown, context: GraphQLContext) => getCommunications(context.auth),
contactInboxes: async (_args: unknown, context: GraphQLContext) => getContactInboxes(context.auth),
calendar: async (_args: unknown, context: GraphQLContext) => getCalendar(context.auth),
calendar: async (args: { dateRange?: { from?: string; to?: string } }, context: GraphQLContext) => getCalendar(context.auth, args.dateRange ?? undefined),
deals: async (_args: unknown, context: GraphQLContext) => getDeals(context.auth),
feed: async (_args: unknown, context: GraphQLContext) => getFeed(context.auth),
pins: async (_args: unknown, context: GraphQLContext) => getPins(context.auth),