From 227030b9ae632c096e73b850391d33d94f46a02d Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:41:35 +0700 Subject: [PATCH] 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 --- .../components/workspace/CrmWorkspaceApp.vue | 317 +++++++++--------- .../workspace/calendar/CrmCalendarPanel.vue | 22 +- frontend/graphql/generated.ts | 33 +- frontend/graphql/operations/calendar.graphql | 4 +- frontend/graphql/schema.graphql | 7 +- frontend/server/graphql/schema.ts | 15 +- 6 files changed, 217 insertions(+), 181 deletions(-) diff --git a/frontend/app/components/workspace/CrmWorkspaceApp.vue b/frontend/app/components/workspace/CrmWorkspaceApp.vue index e0da94a..40f3a6c 100644 --- a/frontend/app/components/workspace/CrmWorkspaceApp.vue +++ b/frontend/app/components/workspace/CrmWorkspaceApp.vue @@ -1,4 +1,5 @@ @@ -156,6 +157,14 @@ defineProps<{ > + + +
+
; + to?: InputMaybe; +}; + export type CalendarEvent = { __typename?: 'CalendarEvent'; archiveNote: Scalars['String']['output']; @@ -387,6 +392,11 @@ export type Query = { }; +export type QuerycalendarArgs = { + dateRange?: InputMaybe; +}; + + export type QuerygetClientTimelineArgs = { contactId: Scalars['ID']['input']; limit?: InputMaybe; @@ -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; + to?: InputMaybe; +}>; 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; 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 | VueCompositionApi.Ref> | ReactiveFunction> = {}) { - return VueApolloComposable.useQuery(CalendarQueryDocument, {}, options); +export function useCalendarQueryQuery(variables: CalendarQueryQueryVariables | VueCompositionApi.Ref | ReactiveFunction = {}, options: VueApolloComposable.UseQueryOptions | VueCompositionApi.Ref> | ReactiveFunction> = {}) { + return VueApolloComposable.useQuery(CalendarQueryDocument, variables, options); } -export function useCalendarQueryLazyQuery(options: VueApolloComposable.UseQueryOptions | VueCompositionApi.Ref> | ReactiveFunction> = {}) { - return VueApolloComposable.useLazyQuery(CalendarQueryDocument, {}, options); +export function useCalendarQueryLazyQuery(variables: CalendarQueryQueryVariables | VueCompositionApi.Ref | ReactiveFunction = {}, options: VueApolloComposable.UseQueryOptions | VueCompositionApi.Ref> | ReactiveFunction> = {}) { + return VueApolloComposable.useLazyQuery(CalendarQueryDocument, variables, options); } export type CalendarQueryQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn; export const ChatConversationsQueryDocument = gql` diff --git a/frontend/graphql/operations/calendar.graphql b/frontend/graphql/operations/calendar.graphql index 273f501..b965edd 100644 --- a/frontend/graphql/operations/calendar.graphql +++ b/frontend/graphql/operations/calendar.graphql @@ -1,5 +1,5 @@ -query CalendarQuery { - calendar { +query CalendarQuery($from: String, $to: String) { + calendar(dateRange: { from: $from, to: $to }) { id title start diff --git a/frontend/graphql/schema.graphql b/frontend/graphql/schema.graphql index 957049e..0beb1cd 100644 --- a/frontend/graphql/schema.graphql +++ b/frontend/graphql/schema.graphql @@ -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! diff --git a/frontend/server/graphql/schema.ts b/frontend/server/graphql/schema.ts index c3a17de..e115f41 100644 --- a/frontend/server/graphql/schema.ts +++ b/frontend/server/graphql/schema.ts @@ -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),